28 May 2017 — Comments
Sometimes the nature of an application requires you to change the default framework's way to structure error responses (like 404 and 405).
On this article I'm going to explain how to customize those responses when working with Zend Expressive 2.
In Expressive 1, error handling was different.
There used to be an element called error handler, which was responsible of handling uncaught exceptions, but also, other errors, like the ones above (404 and 405).
When using the error handler it was easier to have an standard way to generate error responses for any kind of situation, like unmatched routes (404), routes requested with an incorrect HTTP verb (405) or uncaught exceptions (500), as well as other errors.
In expressive 2, the error handler is gone, replaced by a middleware which catches exceptions and lets you generate error responses. However, this middleware does not deal with 404 and 405 errors anymore. Now, those errors are handled by two new elements.
NotFoundDelegate
, which as its name suggests, returns a response with status 404.The problem with this two elements is the response body.
The first one returns an empty body by default, and the second one returns an error template (if a renderer service was registered) or a plain text response otherwise.
If your API is returning JSON responses (for example), this behavior is inconsistent and should be fixed.
Both the RouteMiddleware
and the NotFoundDelegate
allow a PSR-7 response to be injected on them, to be used as the response prototype.
This way, we can define a consistent response that has the same structure regardless the error.
For the "not found" error, we have to define and register our own factory for the default delegate, which gets injected our customized response prototype:
<?php
namespace App\Delegate;
use Psr\Container\ContainerInterface;
use Zend\Diactoros\Response\JsonResponse;
use Zend\Expressive\Delegate\NotFoundDelegate;
class DefaultDelegateFactory
{
public function __invoke(ContainerInterface $container)
{
$myResponsePrototype = new JsonResponse([
'foo' => 'bar',
'message' => 'Not found',
], 404); // We don't really need to set the status, since the NotFoundDelegate will set it for us
return new NotFoundDelegate($myResponsePrototype);
}
}
Now register it on the config/autoload/dependencies.global.php
file:
<?php
use App\Delegate\DefaultDelegateFactory;
return [
'factories' => [
'Zend\Expressive\Delegate\DefaultDelegate' => DefaultDelegateFactory::class,
],
// [...]
];
And that should be it.
Of course, in this case that we always want to return the same response, we could just define another delegate which returns it, instead of using the built in implementation.
The "method not allowed" case is very similar, but with the RouteMiddleware
instead of the NotFoundDelegate
. However, there is one thing to take into account that we'll see later.
First, let's create our own factory:
<?php
namespace App\Middleware;
use Psr\Container\ContainerInterface;
use Zend\Diactoros\Response\JsonResponse;
use Zend\Expressive\Middleware\RouteMiddleware;
use Zend\Expressive\Router\RouterInterface;
class RouteMiddlewareFactory
{
public function __invoke(ContainerInterface $container)
{
$myResponsePrototype = new JsonResponse([
'bar' => 'foo',
'message' => 'Method not allowed',
], 405); // Again, we don't need to set the status because the RouteMiddleware will set it
return new RouteMiddleware($container->get(RouterInterface::class), $myResponsePrototype);
}
}
Now we should register the middleware service and pipe it, but here's the "problem".
When doing this, my first approach was trying to override the Application::ROUTING_MIDDLEWARE
name, but it didn't work.
Internally, when expressive detects that middleware name, it instantiates a RouteMiddleware
instance of its own, without the custom response prototype.
I have already created a question in ZF's forum, because I'm not sure if that's the intended behavior: https://discourse.zendframework.com/t/customize-response-prototype-on-routemiddleware/129
In the meantime, we are going to use a different middleware/service name, so let's register it.
<?php
use App\Middleware\RouteMiddlewareFactory;
return [
'factories' => [
'my_routing_middleware' => RouteMiddlewareFactory::class,
],
];
Now, we have to pipe our middleware instead of the built in Application::ROUTING_MIDDLEWARE
. Do it on config\pipeline.php
;
<?php
// [..]
// Remove this, which pipes the Application::ROUTING_MIDDLEWARE
//$app->pipeRoutingMiddleware();
// Add this instead so that our custom RouteMiddleware is used
$app->pipe('my_routing_middleware');
// [...]
And that's all.
If in the end, the "problem" described above is fixed somehow, I'll update this article.
This demonstrates how flexible expressive is.
We have many possible approaches. Overwrite built-in components, customize its behavior... We are always in control of the application, and there's no black magic involved.