Pragmatism in the real world

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.

17 thoughts on “Logging errors in Slim 3

  1. 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?

  2. 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.

  3. 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 :)

  4. 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?

  5. 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

    1. Yes. You need to use the container nowadays:

      $container = $app->getContainer();
      $container['Logger'] = function($container) { 
      ...
      
      1. Oh, yes, thank you. Nothing is obvious when you face a new tool :)

        It seems array key case has changed as well:

        $container['logger'] = function(Slim\Container $c) {
            // ...
        };
            
        $container['errorHandler'] = function (Slim\Container $c) {
            // ...
        };
        
  6. 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;
    }

  7. 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
    )

    )

Comments are closed.