Pragmatism in the real world

Customising Whoops in Expressive

I find the Whoops error handler page in Expressive quite hard to read and particularly dislike that the error message displayed in the top left is hidden if it’s more than a few words long.

To fix this, I discovered that you can provide a custom CSS file to the PrettyPrintHandler and then override to your heart’s content!

One way to do this is to add a delegator factory to add the additional functionality, so let’s do that.

Update configuration

The Whoops configuration is in config/autoload/development.local.php. This means that you need to change config/autoload/development.local.php.dist too as that’s the one that’s stored in git.

A delegator factory is a service within Zend-ServiceManager that allows you to modify an object created by another factory before it is released to the application. In development.local.php, the ServiceManager configuration looks like this:

    'dependencies' => [
        'factories'  => [
            ErrorResponseGenerator::class       => Container\WhoopsErrorResponseGeneratorFactory::class,
            'Zend\Expressive\Whoops'            => Container\WhoopsFactory::class,
            'Zend\Expressive\WhoopsPageHandler' => Container\WhoopsPageHandlerFactory::class,
        ],
    ],

The factories key lists the name of the service against the factory class responsible for creating it. In this case, the WhoopsPageHandlerFactory is responsible for creating a WhoopsPageHandler.

We can add our delegator factory to a new delegators section so that it looks like this:

    'dependencies' => [
        'factories'  => [
            ErrorResponseGenerator::class       => Container\WhoopsErrorResponseGeneratorFactory::class,
            'Zend\Expressive\Whoops'            => Container\WhoopsFactory::class,
            'Zend\Expressive\WhoopsPageHandler' => Container\WhoopsPageHandlerFactory::class,
        ],
        'delegators' => [
            'Zend\Expressive\WhoopsPageHandler' => [
                App\Factory\WhoopsPageHandlerDelegatorFactory::class,
            ]
        ],
    ],

(Don’t forget to update development.local.php.dist too!)

Note that the delegators key maps an array of delegator factories to the service name. This is because we can register more than one and each will be executed in turn.

Write a delegator factory

I’ve chosen to store WhoopsPageHandlerDelegatorFactory in the App\Factory namespace, so the file is src/App/src/Factory/WhoopsPageHandlerDelegatorFactory.php:

WhoopsPageHandlerDelegatorFactory.php:

<?php declare(strict_types=1);

namespace App\Factory;

use Interop\Container\ContainerInterface;
Use Whoops\Handler\PrettyPageHandler;
use Zend\ServiceManager\Factory\DelegatorFactoryInterface;

class WhoopsPageHandlerDelegatorFactory implements DelegatorFactoryInterface
{
    public function __invoke(ContainerInterface $container, $name, callable $callback, array $options = null)
    {
        $pageHandler = call_user_func($callback);

        if ($pageHandler instanceof PrettyPageHandler) {
            $pageHandler->addResourcePath(__DIR__ . '/../../templates/css');
            $pageHandler->addCustomCss('whoops.css');
        }

        return $pageHandler;
    }
}

The __invoke() function does the work here and we start by calling the $callback callable which will return the PageHandler object. In our case, it calls the registered factory, but if we had more delegator factories registered, then it may have called a previous one of those.

Having retrieved the $pageHandler, we can customise it by adding a custom CSS file name and the path to where it is. Rather unimaginatively, I’ve called the CSS file whoops.css and it lives in the App module’s templates/css folder.

Provide some CSS

The CSS can contain anything you need to override the Whoops page to your liking. In my case, I have moved the error message to be across the entire top of the page as it’s the most important thing I want to read. It also means that I no longer have to use a mouse to hover over it to view the entire message! I’ve also changed the colour of the file names in the stack trace to provide more contrast and added () after the function name.

templates/css/whoops.css:

.panel {
    margin-top: 200px;
}

header {
    max-height: 200px;
    position: fixed;
    top: 0;
    width: 100%;
    background-color: #333;
}

.frame-file {
    color: #333;
}

.frame-function::after {
    content: "()";
}

You can obviously make many more customisations should you wish to.

Fin

That’s it. We can now control the look of the Whoops error display. Through the use of a delegator factory, we didn’t need to touch the current factory which makes our change nicely self-contained.