@Sergey_Telshevsky The spot you want to look at for the whole ($request, $response, $next)
[double pass] vs ($request, $delegate)
[single, or lambda, pass] is the proposed PSR-15 standard.
@matthew can most concisely describe the change, but the main difference between the two philosophies is: When do you “decorate” the $response
object? In double pass, the standard way to call the next middleware in the pipeline (assuming you aren’t returning an actual response) is with
//Decorate the $response object before passing along to next stage in the pipeline
return $next($request, $response);
//We can't check the $response for changes!
This means that you are modifying the $response
as you go into the pipeline. This can lead to unexpected consequences if one of the middleware further into the pipeline also modifies the $response
object. The idea behind single pass is that you modify the $response
object on the way out of the pipeline. Remember that a middleware pipeline is kinda like a recursive function in that it calls the first middleware class that then calls the next “deeper” middleware and so on until we exhaust the pipeline with a middleware that returns a $response
object. At this point that object is returned back “up” or “out” of the pipeline stack. In the traditional double pass of $next($request, $response)
the middleware never gets to see that response object as its just immediately passed up and out. Yes, you could very well do
//Decorate the $response object before passing along to next stage in the pipeline
$response = $next($request, $response);
//Also decorate here?
return $response;
but then what was the purpose of passing a $response down into the pipeline in the first place? Single pass methodology forces you to only deal with the $response
on the way up and out the pipeline:
//We can modify the request, but not the response
$response = $delegate->process($middlewareModifiedRequest);
//Decorate the $response object here
return $response;
Decorating the $response
on the way out of the pipeline makes more sense as you actually know the final disposition of the request - Did I succeed (200)? Did I fail? (400-500)? Is there a body to the response?
The linked PSR proposal goes into why they aren’t doing __invoke()
anymore. Sorry if you knew all this already, but if nothing else, it helps firm it up in my mind a bit more!
How to handle errors in a single pass middleware stack?
There are a few perfectly legitimate ways of doing this.
- Create the error response at the point of failure. An
ErrorHandler
class is only to decorate that on the way up/out of the pipeline and to catch programatic problems that throw
something (can’t connect to DB, etc). To use your “user not found” example:
//Abbreviated pipeline
$api->get('/user', [
\App\Middleware\ErrorHandler::class,
\App\Action\User::class
]);
//User.php
public function process(ServerRequestInterface $request, DelegateInterface $delegate): ResponseInterface
{
$user = $this->getFromDb($request);
if (!$user) {
return new TextResponse('No User Found', 401);
}
$response = $this->formatUser($user);
return $response;
}
I know @matthew prefers this way as a routed action will always return a $response
object in normal use and will always pass back up the entire pipeline.
-
throw
everything and have it caught by the ErrorHandler
. In this philosophy, every error or exception (400-500 response codes) is a Throwable
and the $response
object creation is handled by the ErrorHandler
.
//Abbreviated pipeline
$api->get('/user', [
\App\Middleware\ErrorHandler::class,
\App\Action\User::class
]);
//User.php
public function process(ServerRequestInterface $request, DelegateInterface $delegate): ResponseInterface
{
$user = $this->getFromDb($request);
if (!$user) {
throw new InvalidArgumentException('No User Found', 401);
}
$response = $this->formatUser($user);
return $response;
}
I like doing this for a few reasons. a) All my error response object generation is in one spot regardless of if its a user or programmatic error. b) Some of my middleware has lots of calls to various external service classes and methods. I can throw
the problem from anywhere, and it will “short-circuit” the processing and immediately get caught by the ErrorHandler
class. I don’t have to check each return from every method to see if I’m valid to move on to the next piece of logic. The downside is that it does skip the pipeline al the way back up to the ErrorHandler
class. If you have $response
decorators in that pipeline they will not be called. For my programming style, that’s not an issue, but it may be in yours.
I hope this isn’t too redundant!
–Mark
EDIT: Matthew is a faster typer than I, or got to this first. I’ll leave this up anyways as it may help. -M