Caution
The documentation you are viewing is for an older version of this component.
Switch to the latest (v3) version.
Features
Error Handling
We recommend that your code raise exceptions for conditions where it cannot
gracefully recover. Additionally, we recommend that you have a reasonable PHP
error_reporting
setting that includes warnings and fatal errors:
error_reporting(E_ALL & ~E_USER_DEPRECATED & ~E_DEPRECATED & ~E_STRICT & ~E_NOTICE);
If you follow these guidelines, you can then write or use middleware that does the following:
- sets an error handler that converts PHP errors to
ErrorException
instances. - wraps execution of the delegate (
$delegate->process()
) with a try/catch block.
As an example:
function ($request, DelegateInterface $delegate)
{
set_error_handler(function ($errno, $errstr, $errfile, $errline) {
if (! (error_reporting() & $errno)) {
// Error is not in mask
return;
}
throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
});
try {
$response = $delegate->process($request);
return $response;
} catch (Throwable $e) {
} catch (Exception $e) {
}
restore_error_handler();
$response = new TextResponse(sprintf(
"[%d] %s\n\n%s",
$e->getCode(),
$e->getMessage(),
$e->getTraceAsString()
), 500);
}
You would then pipe this as the outermost (or close to outermost) layer of your application:
$app->pipe($errorMiddleware);
So that you do not need to do this, we provide an error handler for you, via
zend-stratigility: Zend\Stratigility\Middleware\ErrorHandler
.
This implementation allows you to both:
- provide a response generator, invoked when an error is caught; and
- register listeners to trigger when errors are caught.
We provide the factory Zend\Expressive\Container\ErrorHandlerFactory
for
generating the instance; it should be mapped to the service
Zend\Stratigility\Middleware\ErrorHandler
.
We provide two error response generators for you:
-
Zend\Expressive\Middleware\ErrorResponseGenerator
, which optionally will accept aZend\Expressive\Template\TemplateRendererInterface
instance, and a template name. When present, these will be used to generate response content; otherwise, a plain text response is generated that notes the request method and URI. -
Zend\Expressive\Middleware\WhoopsErrorResponseGenerator
, which uses whoops to present detailed exception and request information; this implementation is intended for development purposes.
Each also has an accompanying factory for generating the instance:
Zend\Expressive\Container\ErrorResponseGeneratorFactory
Zend\Expressive\Container\WhoopsErrorResponseGeneratorFactory
Map the service Zend\Expressive\Middleware\ErrorResponseGenerator
to one of
these two factories in your configuration:
use Zend\Expressive\Container;
use Zend\Expressive\Middleware;
use Zend\Stratigility\Middleware\ErrorHandler;
return [
'dependencies' => [
'factories' => [
ErrorHandler::class => Container\ErrorHandlerFactory::class,
Middleware\ErrorResponseGenerator::class => Container\ErrorResponseGeneratorFactory::class,
],
],
];
Use development mode configuration to enable whoops
You can specify the above in one of your
config/autoload/*.global.php
files, to ensure you have a production-capable error response generator.If you are using zf-development-mode in your application (which is provided by default in the skeleton application), you can toggle usage of whoops by adding configuration to the file
config/autoload/development.local.php.dist
:use Zend\Expressive\Container; use Zend\Expressive\Middleware; return [ 'dependencies' => [ 'factories' => [ Middleware\WhoopsErrorResponseGenerator::class => Container\WhoopsErrorResponseGeneratorFactory::class, ], ], ];
When you enable development mode, whoops will then be enabled; when you disable development mode, you'll be using your production generator.
If you are not using zf-development-mode, you can define a
config/autoload/*.local.php
file with the above configuration whenever you want to enable whoops.
Listening for errors
When errors occur, you may want to listen for them in order to provide
features such as logging. Zend\Stratigility\Middleware\ErrorHandler
provides
the ability to do so via its attachListener()
method.
This method accepts a callable with the following signature:
function (
Throwable|Exception $error,
ServerRequestInterface $request,
ResponseInterface $response
) : void
The response provided is the response returned by your error response generator, allowing the listener the ability to introspect the generated response as well.
As an example, you could create a logging listener as follows:
namespace Acme;
use Exception;
use Psr\Log\LoggerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Throwable;
class LoggingErrorListener
{
/**
* Log format for messages:
*
* STATUS [METHOD] path: message
*/
const LOG_FORMAT = '%d [%s] %s: %s';
private $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
public function __invoke($error, ServerRequestInterface $request, ResponseInterface $response)
{
$this->logger->error(sprintf(
self::LOG_FORMAT,
$response->getStatusCode(),
$request->getMethod(),
(string) $request->getUri(),
$error->getMessage()
));
}
}
You could then use a delegator factory to create your logger listener and attach it to your error handler:
namespace Acme;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use Zend\Stratigility\Middleware\ErrorHandler;
class LoggingErrorListenerDelegatorFactory
{
/**
* @param ContainerInterface $container
* @param string $name
* @param callable $callback
* @return ErrorHandler
*/
public function __invoke(ContainerInterface $container, $name, callable $callback)
{
$listener = new LoggingErrorListener($container->get(LoggerInterface::class));
$errorHandler = $callback();
$errorHandler->attachListener($listener);
return $errorHandler;
}
}
Handling more specific error types
You could also write more specific error handlers. As an example, you might want
to catch UnauthorizedException
instances specifically, and display a login
page:
function ($request, DelegateInterface $delegate) use ($renderer)
{
try {
$response = $delegate->process($request);
return $response;
} catch (UnauthorizedException $e) {
}
return new HtmlResponse(
$renderer->render('error::unauthorized'),
401
);
}
You could then push this into a middleware pipe only when it's needed:
$app->get('/dashboard', [
$unauthorizedHandlerMiddleware,
$middlewareThatChecksForAuthorization,
$middlewareBehindAuthorizationWall,
], 'dashboard');
Default delegates
Zend\Expressive\Application
manages an internal middleware pipeline; when you
call $delegate->process()
, Application
is popping off the next middleware in
the queue and dispatching it.
What happens when that queue is exhausted?
That situation indicates an error condition: no middleware was capable of returning a response. This could either mean a problem with the request (HTTP 400 "Bad Request" status) or inability to route the request (HTTP 404 "Not Found" status).
In order to report that information, Zend\Expressive\Application
composes a
"default delegate": a delegate it will invoke once the queue is exhausted and no
response returned. By default, it uses a custom implementation,
Zend\Expressive\Delegate\NotFoundDelegate
, which will report a 404 response,
optionally using a composed template renderer to do so.
We provide a factory, Zend\Expressive\Container\NotFoundDelegateFactory
, for
creating an instance, and this should be mapped to the
Zend\Expressive\Delegate\NotFoundDelegate
service, and aliased to the
Zend\Expressive\Delegate\DefaultDelegate
service:
use Zend\Expressive\Container;
use Zend\Expressive\Delegate;
return [
'dependencies' => [
'aliases' => [
'Zend\Expressive\Delegate\DefaultDelegate' => Delegate\NotFoundDelegate::class,
],
'factories' => [
Delegate\NotFoundDelegate::class => Container\NotFoundDelegateFactory::class,
],
],
];
The factory will consume the following services:
-
Zend\Expressive\Template\TemplateRendererInterface
(optional): if present, the renderer will be used to render a template for use as the response content. -
config
(optional): if present, it will use the$config['zend-expressive']['error_handler']['template_404']
value as the template to use when rendering; if not provided, defaults toerror::404
.
If you wish to provide an alternate response status or use a canned response,
you should provide your own default delegate, and expose it via the
Zend\Expressive\Delegate\DefaultDelegate
service.
Page not found
Error handlers work at the outermost layer, and are used to catch exceptions and errors in your application. At the innermost layer of your application, you should ensure you have middleware that is guaranteed to return a response; this will prevent the default delegate from needing to execute by ensuring that the middleware queue never fully depletes. This in turn allows you to fully craft what sort of response is returned.
Generally speaking, reaching the innermost middleware layer indicates that no middleware was capable of handling the request, and thus an HTTP 404 Not Found condition.
To simplify such responses, we provide Zend\Expressive\Middleware\NotFoundHandler
,
with an accompanying Zend\Expressive\Container\NotFoundHandlerFactory
. This
middleware composes and proxies to the NotFoundDelegate
detailed in the
previous section, and, as such, requires that that service be present.
use Zend\Expressive\Container;
use Zend\Expressive\Delegate;
use Zend\Expressive\Middleware;
return [
'factories' => [
Delegate\NotFoundDelegate::class => Container\NotFoundDelegateFactory::class,
Middleware\NotFoundHandler::class => Container\NotFoundHandlerFactory::class,
],
];
When registered, you should then pipe it as the innermost layer of your application:
// A basic application:
$app->pipe(ErrorHandler::class);
$app->pipeRoutingMiddleware();
$app->pipeDispatchMiddleware();
$app->pipe(NotFoundHandler::class);
Found a mistake or want to contribute to the documentation? Edit this page on GitHub!