Logging errors in Slim 3
Slim Framework 3 is being actively developed at the moment and has a number of changes in it, including the use of the Pimple DI container and an overhaul of pretty much everything else! In this post, I’m going to look at error handling.
The default error handler in Slim 3 is Slim\Handlers\Error. It’s fairly simple and renders the error quite nicely, setting the HTTP status to 500.
I want to log these errors via monolog.
Firstly, we set up a logger in the DIC:
$app['Logger'] = function($container) { $logger = new Monolog\Logger('logger'); $filename = _DIR__ . '/../log/error.log'; $stream = new Monolog\Handler\StreamHandler($filename, Monolog\Logger::DEBUG); $fingersCrossed = new Monolog\Handler\FingersCrossedHandler( $stream, Monolog\Logger::ERROR); $logger->pushHandler($fingersCrossed); return $logger; };
Now, we can create our own error handler which extends the standard Slim one as all we want to do is add logging.
<?php namespace App\Handlers; use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\ResponseInterface as Response; use Monolog\Logger; final class Error extends \Slim\Handlers\Error { protected $logger; public function __construct(Logger $logger) { $this->logger = $logger; } public function __invoke(Request $request, Response $response, \Exception $exception) { // Log the message $this->logger->critical($exception->getMessage()); return parent::__invoke($request, $response, $exception); } }
The error handler implements __invoke(), so our new class overrides this function, extracts the message and logs it as a critical. To get the Logger into the error handler, we use standard Dependency Injection techniques and write a constructor that takes the configured logger as a parameter.
All we need to do now is register our new error handler which we can do in index.php:
$app['errorHandler'] = function ($c) { return new App\Handlers\Error($c['Logger']); };
Again, this is standard Pimple, so the 'errorHandler' key takes a closure which receives an instance of the container, $c. We instantiate a new App\Handlers\Error object and then retrieve the Logger from the container as we have already registered that with Pimple, so it knows how to create one for us.
With this done, we now have a new error handler in place. From the user’s point of view, there’s no difference, but we now get a message in our log file when something goes wrong.
Other error handlers
Obviously, we can use this technique to replace the entire error handler for situations when we don’t want to display a comprehensive developer-friendly error to the user. Another case would be if we are writing an API, we may not want to respond with an HTML error page.
In these cases, we do exactly the same thing. For example, if we’re writing a JSON API, then a suitable error handler looks like this:
<?php namespace App\Handlers; use Psr\Http\Message\ ServerRequestInterface as Request; use Psr\Http\Message\ResponseInterface as Response; use Monolog\Logger; final class ApiError extends \Slim\Handlers\Error { protected $logger; public function __construct(Logger $logger) { $this->logger = $logger; } public function __invoke(Request $request, Response $response, \Exception $exception) { // Log the message $this->logger->critical($exception->getMessage()); // create a JSON error string for the Response body $body = json_encode([ 'error' => $exception->getMessage(), 'code' => $exception->getCode(), ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); return $response ->withStatus(500) ->withHeader('Content-type', 'application/json') ->withBody(new Body(fopen('php://temp', 'r+'))) ->write($body); } }
This time we construct a JSON string for our response and then use Slim’s PSR-7-compatible Response object to create a new one with the correct information in it, which we then return to the client.
Fin
As you can see, it’s really easy to manipulate and control error handling in Slim 3. Compared to Slim 2, the best bit is that the PrettyExceptions middleware is not automatically added which had always annoyed me when writing APIs.
Thanks for this post, Rob – I was building my first Slim app this weekend and was struggling to work out how to do this with Slim 3. To be fair, most of my confusion comes from not having used a DIC before. Am I right in thinking that the setting of $app['Logger'] tells the DIC that when it is calling a class with a dependency of type "Logger", it should use the provided factory callable and inject it?
Gareth, Yes :)
In first example should be
use Psr\Http\Message\ServerRequestInterface as Request;
instead of
use Psr\Http\Message\RequestInterface as Request;
because it generates warning.
Fixed. Thanks.
Seems not to work with slim 3.4.0 alway get a "Catchable fatal error: Argument 3 passed to App\Handlers\Error::__invoke() must be an instance of Exception, instance of Closure given". is there an update to your code to reflect latest changes in slim? thanks :)
Tom,
I'm not seeing this. Can you gist some code over at http://discourse.slimframework.com?
What is "new Body" referencing in the line withBody(new Body(fopen('php://temp', 'r+')))? Is that a place holder for a streaminterface like \Slim\Http\Stream?
Body is Slim's implementation of PSR-7's StreamInterface
Has anything relevant been changed since this article was written? I'm using Slim/3.3 and I can't assign anything to `$app` using square brackets:
Fatal error: Cannot use object of type Slim\App as array
Yes. You need to use the container nowadays:
Oh, yes, thank you. Nothing is obvious when you face a new tool :)
It seems array key case has changed as well:
(No idea of how to format code, I had assumed MarkDown would work…)
The case thing is almost certainly an error in my original post!
(You format code here using <pre> tags.)
What if I want to also direct the user to a 500 error page (twig view)?
Inject the Twig view into the handler and then render your 500 view template and return it in a Response object.
hi,
to maintain the displayErrorDetails funccionality working the first param should be $displayErrorDetails to be passed to the parent constructor.
public function __construct($displayErrorDetails, Logger $logger)
{
parent::__construct($displayErrorDetails);
$this->logger = $logger;
}
I have done the same as guided by you, but I don't get any output in log file.
Upon echoing $container['errorHandler'], I get the code below. Everything seems okay. But when I throw a 'new Exception('TEST');' I get no output. Please help!
App\Handlers\Error Object
(
[logger:protected] => Monolog\Logger Object
(
[name:protected] => logger
[handlers:protected] => Array
(
[0] => Monolog\Handler\FingersCrossedHandler Object
(
[handler:protected] => Monolog\Handler\StreamHandler Object
(
[stream:protected] =>
[url:protected] => /var/www/html/slim-api/public/app.log
[errorMessage:Monolog\Handler\StreamHandler:private] =>
[filePermission:protected] =>
[useLocking:protected] =>
[dirCreated:Monolog\Handler\StreamHandler:private] =>
[level:protected] => 100
[bubble:protected] => 1
[formatter:protected] =>
[processors:protected] => Array
(
)
)
[activationStrategy:protected] => Monolog\Handler\FingersCrossed\ErrorLevelActivationStrategy Object
(
[actionLevel:Monolog\Handler\FingersCrossed\ErrorLevelActivationStrategy:private] => 400
)
[buffering:protected] => 1
[bufferSize:protected] => 0
[buffer:protected] => Array
(
)
[stopBuffering:protected] => 1
[passthruLevel:protected] =>
[level:protected] => 100
[bubble:protected] => 1
[formatter:protected] =>
[processors:protected] => Array
(
)
)
)
[processors:protected] => Array
(
)
[microsecondTimestamps:protected] => 1
[exceptionHandler:protected] =>
)
[displayErrorDetails:protected] =>
[knownContentTypes:protected] => Array
(
[0] => application/json
[1] => application/xml
[2] => text/xml
[3] => text/html
)
)