http-interop / http-middleware

PSR-15 interfaces for HTTP Middleware
MIT License
73 stars 7 forks source link

MiddlewarePipe vs. MiddlewareStack #35

Closed schnittstabil closed 7 years ago

schnittstabil commented 7 years ago

Both concepts, the pipe and the stack are used to describe Middleware Containers. Both container types share the idea of a reusable middlewares. Currently, the interfaces are:

interface DelegateInterface
{
    public function process(RequestInterface $request);
}

interface ServerMiddlewareInterface
{
    public function process(ServerRequestInterface $request, DelegateInterface $delegate);
}

I will describe both types, how they are used and how they could be reused. Based on that, I will show their commonalities and propose new interface versions. I consider neither the method names (process vs dispatch vs __invoke) nor the name sharing. Hence, please, don't try to start a new debate about that here.

The Pipe(line)

The idea can be described as follows: Middlewares are small pipes, we plumb them together and pour a $request into top ending of the resulting pipe(line). The $request may pass all middlewares and run out at the bottom. Thus we need some $finalHandler to handle that:

$middlewares = [
   $m0,
   $m1,
   $m2,
   $m3,
];

$finalHandler = function ($request) {
   // throw an error or return virgin response or …
};

If we dispatch a $request it may pass all middlewares in the order: $m0 to $m3.

Similar to plumbing, the order of the pipes matters, but not the order we connect the pipes. Thus we could reuse plumbed pipes with other pipes:

$pipe = new Pipe([
   $m0,
   new Pipe([
      $m1,
      $m2,
   ]),
   $m3,
]);

If a $request runs out of $m0 the outer Pipe pours it into the inner one. Furthermore, if a $request runs out of the inner Pipe the outer Pipe can pour it into $m3.

This means dispatching a $request should probably look like:

$pipe->dispatch($request, $finalHandler);

The Stack

Stacks provide only two methods: push and pop. Thus, if we have a stack and want to access the bottom element, we need to pop all elements above it before. This leads to the onion style of the middleware stack: We start with a final handler, i.e. the core of the onion. Then we push middlewares onto the stack, i.e. we wrap the core with new scales. Hence, we cannot access the inner scales this leads to the following setup:

$stack = new Stack($finalHandler);
$stack->push($m3);
$stack->push($m2);
$stack->push($m1);
$stack->push($m0);

$stack->dispatch($request);

As we push middlewares onto the stack, the order must be reversed to get the same execution order as above.

Onions protect their inner scales, thus they could also be reused:

$stack0 = new Stack($finalHandler);
$stack0->push($m3);

$stack1 = new Stack($stack0);
$stack1->push($m2);
$stack1->push($m1);

$stack2 = new Stack($stack1);
$stack2->push($m0);

$stack2->dispatch($request);

Commonalities

The $finalHandler

Both use a $finalHandler with a signature, which can be described by:

function (ServerRequestInterface $request):ResponseInterface {}

In some pipe implementations the $finalHandler is only called in case of an error, i.e. middlewares produce the requested content and the $finalHandler handles the situation where no such content middleware is available.

In the world of middleware stacks, that is not need: creating a Stack without a $finalHandler is not possible. Thus, the $finalHandler is used to create the requested content.

But both $finalHandler versions have one thing in common: They handle a request and return a response. Therefore, we may use the following interface for both versions:

interface RequestHandlerInterface
{
    /**
     * Process a request and return the response.
     *
     * @param ServerRequestInterface $request
     *
     * @return ResponseInterface
     */
    public function process(ServerRequestInterface $request);
}

The DelegateInterface

To dispatch a middleware (pipe/stack), we need to create a delegate object which knows the current pipe/stack:

$delegate = new Delegate(…, $thisPipeOrStack,… );
$theNextMiddleware->process($request, $delegate);

Thus, the $theNextMiddleware can interact with a delegate of the pipe/stack and doesn't have to know how to dispatch the next middleware:

class AwesomeMiddleware implements ServerMiddlewareInterface
{
    /**
     * Process an incoming server request and return a response, optionally delegating
     * to the next middleware component to create the response.
     *
     * @param ServerRequestInterface $request
     * @param DelegateInterface $delegate
     *
     * @return ResponseInterface
     */
    public function process(ServerRequestInterface $request, DelegateInterface $delegate) {
        $response = $delegate->process($request);
        return $response->withHeader('X-PoweredBy', 'Unicorns');
    }
}

But, as the middleware delegates the $request to the pipe/stack, it essentially does:

$response = $delegate->delegate($request);

That's really confusing, because the first delegate means something quiet different than the second delegate. Furthermore, ServerMiddlewareInterface and DelegateInterface states that a delegate is somehow connected to a concrete middleware pipe/stack, but we can neither access push nor dispatch. Instead we have:

interface DelegateInterface
{
    public function process(RequestInterface $request):ResponseInterface;
}

That is the same functionality RequestHandlerInterface provides: process a request and return a response. Only the $request typehint differs for legacy reasons.

New versions

The observations above leads to more general versions:

interface RequestHandlerInterface
{
    /**
     * Process a request and return the response.
     *
     * @param ServerRequestInterface $request
     *
     * @return ResponseInterface
     */
    public function process(ServerRequestInterface $request);
}

interface ServerMiddlewareInterface
{
    /**
     * Process an incoming server request and return a response, optionally delegating
     * to the next request handler.
     *
     * @param ServerRequestInterface  $request
     * @param RequestHandlerInterface $next
     *
     * @return ResponseInterface
     */
    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $next
    );
}

Conclusion

The pros, I see:

  1. The concept of request handlers is easy to understand and well known.
  2. It is a more general concept – the middleware doesn't have to know that it is dealing with a container (and it shouldn't know as @shadowhand mentioned the other day).
  3. It better reflects the idea, that a middleware comes between a request and response (a viewpoint mentioned by @alamin3000).
  4. Pipes and stacks MAY implement ServerMiddlewareInterface or RequestHandlerInterface, respectively.

The cons, I see:

Best regards Michael

schnittstabil commented 7 years ago

Updated, sorry for the empty issue in the meanwhile. @weierophinney, @shadowhand, @mindplay-dk, @alamin3000: ping

shadowhand commented 7 years ago

I think we've already debated this to death and the current interfaces are fine.

Anyone else @http-interop/http-middleware-contributors ?