Slim 3 Custom Not Found Handler and Template

I have been using the Slim micro-framework (version 2) for small web projects for some time now. Now that Slim version 3 is out, I thought I would give it a try on a new project.

Slim 3 is new, like shiny new, and I found it is also very different from version 2. It's more like learning a new framework than a simple upgrade.

In Slim 2, creating your own 404 handler was easy. You registered your own notFound handler in the app container to overwrite the default one.

// Register custom 404 not found handler
$app->notFound(function () use ($app) {
    // Render 404 page
    $app->twig->display('notFound.html');
});

But the Slim 3 request and response objects are all PSR-7 compliant and what seemed like a simple task ended up taking more effort to make work. But in the end, the solution works rather nicely.

Slim 3 has a Slim\Handlers\NotFound class. Depending on the request Content-Type, the __invoke() method will return the status 404 code with an appropriate response message. It is in this class that the HTML response is generated.

To use my own custom not found template, I extended this class and overwrote the renderHtmlNotFoundOutput() method with my own, which loads my custom (Twig) template. Then in my __invoke() method, I call the parent::__invoke() method. This way, other non-text/html requests get the default 404 handling, and only text/html requests get the custom template.

Since I now have a custom class to manage 404's, I also added some logging (I inject the logger in the class constructor, along with Twig). I also added a quick check to make sure we're not bothering with generating a custom response page for any missing images or files.

<?php

/**
 * Not Found Handler
 *
 * Extends the Slim NotFound handler
 */

declare(strict_types=1);

namespace App\Extensions;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

class NotFound extends \Slim\Handlers\NotFound
{
    /**
     * Twig View Handler
     */
    protected $view;

    /**
     * Monolog Logger
     */
    protected $logger;

    /**
     * Constructor
     *
     * @param Slim\Views\Twig $view Slim Twig view handler
     */
    public function __construct(\Slim\Views\Twig $view, \Monolog\Logger $logger)
    {
        $this->view = $view;
        $this->logger = $logger;
    }

    /**
     * Invoke not found handler
     *
     * @param  ServerRequestInterface $request  The most recent Request object
     * @param  ResponseInterface      $response The most recent Response object
     *
     * @return ResponseInterface
     */
    public function __invoke(ServerRequestInterface $request, ResponseInterface $response)
    {
        // Log request
        $path = $request->getUri()->getPath();
        $this->logger->info("Not Found (404): {$request->getMethod()} {$path}");

        // If request is for a file or image then just return and do no more
        if (preg_match('/^.*\.(jpg|jpeg|png|gif)$/i', $path)) {
            return $response->withStatus(404);
        }

        // Return status and template
        return parent::__invoke($request, $response);
    }

    /**
     * Return a response for text/html content not found - Overwrite Parent Method
     *
     * @param  ServerRequestInterface $request  The most recent Request object
     * @param  ResponseInterface      $response The most recent Response object
     *
     * @return ResponseInterface
     */
    protected function renderHtmlNotFoundOutput(ServerRequestInterface $request)
    {
        // Render and return template as string
        return $this->view->fetch('notFound.html');
    }
}

To use the custom NotFound class, you register this class in the Slim DI container.

// Override the default Not Found Handler
$container['notFoundHandler'] = function ($c) {
    return new App\Extensions\NotFound($c->get('view'), $c->get('logger'));
};

This works great when no matching route is found, but what if you need to deliberately call this in the normal application flow, such as when a page record is not returned from the database?

The Slim simply gets the notFoundHandler when it's needed, and then executes it passing in the request and response objects. Instead of having extra code in my various controllers to get and use the service, I added a notFound() method to my BaseController, which all other controllers extend.

<?php

/**
 * Base Controller
 *
 * All other controllers should extend this class.
 * Loads the Slim Container to $this->container
 */
namespace App\Controllers;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

class BaseController
{
    /**
     * DIC
     */
    protected $container;

    /**
     * Constructor
     *
     * @param ContainerInterface $container
     */
    public function __construct($container)
    {
        $this->container = $container;
    }

    /**
     * Show Page Not Found
     *
     * Returns status 404 Not Found and custom template
     * @param  ServerRequestInterface $request  The most recent Request object
     * @param  ResponseInterface      $response The most recent Response object
     */
    protected function notFound(ServerRequestInterface $request, ResponseInterface $response)
    {
        $notFound = $this->container->get('notFoundHandler');
        return $notFound($request, $response);
    }
}

Then, in any child class that extends this base class, I can simply call the notFound method conditionally.

// Was anything found?
if (empty($records)) {
    return $this->notFound($request, $response);
}

Slim 3 is a great lightweight framework and is fun to develop.

What Do You Think?

* Required