Three Ways to Handle 404 Errors in Mezzio

Three Ways to Handle 404 Errors in Mezzio

When you need to handle a 404 (not found) error in Mezzio, how do you do it? It’s trivial in Laravel, Symfony, CakePHP, and other, larger PHP frameworks. But, it’s not so clear in Mezzio. In this tutorial, I’ll show you three ways to do so.


Recently, while building a small app using Mezzio, I realised that I had forgotten how handle 404 (Not Found) errors. Doing so is pretty normal for web apps and APIs, so I’m surprised that I was so unsure about how to do so with Mezzio – especially when it’s pretty trivial (and baked in) in other PHP frameworks.

For example, in Laravel, you’d do this:

abort(404);

In a Symfony controller action, you’d do either:

throw $this->createNotFoundException('<Your 404 message>');

Or, you’d do this.

throw new NotFoundHttpException('<Your 404 message>');

I could list several other examples, but I think you get the point.

So how do you do it in Mezzio? Call me out if I’m missing something, but it’s not clear (yet) in the Mezzio documentation, and searching around didn’t turn up much either.

But, after asking in the Laminas Slack, answers a plenty came my way. So, I thought that I’d share them, along with some reasonably copy+pasteable code examples.

Let’s begin!

Option 1 - Redirect to a route handled by Mezzio’s NotFoundHandler

While I was searching through Mezzio’s source (and after creating a small class that was going, largely, in the same direction) I came across \Mezzio\Handler\NotFoundHandler.

Without digging into it in too much depth, the class returns a ResponseInterface object with:

  • Its body set to either a text response, or an HTML Response rendered with Mezzio’s 404 template (error::404), which is usually templates/error/404.html.
  • Its status code set to HTTP 404 Not Found.

To make use of it, without it becoming a dependency of one or more of your route handlers, you’d first register a route which, when requested, dispatches to the NotFoundHandler, such as the following which you could add to config/routes.php.

$app->get('/404', \Mezzio\Handler\NotFoundHandler::class);

Then, in your handler’s handle() function, you’d return a RedirectResponse to the route, as in the example below.

return new RedirectResponse('/404');

If you’re unfamiliar with the RedirectResponse object, they are a programmatic way of performing either an HTTP 301 or HTTP 302 response.

To me, this is the simplest and quickest approach to implement.

Option 2 - Use the NotFoundHandler directly

Now, you could use the NotFoundHandler directly if you prefer. As it implements RequestHandlerInterface, like your handlers, it has to implement the handle() function which accepts a ServerRequestInterface object.

Given that, you could refactor your handler class’ constructor to take a NotFoundHandler instance as a constructor parameter. Then, you’d call the instance’s handle() function, providing it the current request, and return the result.

Here’s a short example showing how to do so.

<?php

declare(strict_types=1);

namespace App\Handler;

use Mezzio\Handler\NotFoundHandler;
use Mezzio\Template\TemplateRendererInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;

class HomePageHandler implements RequestHandlerInterface
{
    public function __construct(
        private NotFoundHandler $handler,
        private ?TemplateRendererInterface $template = null
    ) {}

    public function handle(
        ServerRequestInterface $request
    ): ResponseInterface
    {
        return $this->handler->handle($request);
    }
}

Two things are important to note in this approach:

  1. You’ll have to refactor the handler’s factory to pass it a NotFoundHandler instance during instantiation. Alternatively, you could use the Reflection-based Abstract Factory, and not need to implement or refactor an instantiating factory class.
  2. Your DI container needs to have a service that returns a TemplateRendererInterface instance, as it’s a required constructor parameter of the NotFoundHandler.

To be fair, both of these tasks are fairly trivial with Mezzio though.

I’ve been looking at this approach for a little while now, and, while I like it, I feel that it’s both a bit of work to implement, and when used it may not be the most intuitive as to what’s going on.

You could make it slightly more intuitive, though, by naming the NotFoundHandler instance better than I have in the example above. For example $notFoundHandler instead of $handler. But, still, it’s a little unclear. What do you think?

Option 3 - Use a custom helper

Now, as Mezzio has a number of existing helpers, this might be an approach that makes a lot of sense to you.

To take this approach, you’d first need to create the helper class, which might look like the example below.

<?php

declare(strict_types=1);

namespace App\Helper;

use Fig\Http\Message\StatusCodeInterface;
use Laminas\Diactoros\Response\HtmlResponse;
use Mezzio\Template\TemplateRendererInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

readonly class NotFoundHelper
{
    public function __construct(
        private TemplateRendererInterface $templateRenderer
    ) {
    }

    public function notFound(
        ServerRequestInterface $request,
        string $message = ''
    ): ResponseInterface
    {
        $template = 'error::404';
        return new HtmlResponse(
            $this->renderBody($request, $template, $message),
            StatusCodeInterface::STATUS_NOT_FOUND,
        );
    }

    private function renderBody(
        ServerRequestInterface $request,
        string $template,
        string $message
    ): string
    {
        $data = [
            'request' => $request,
            'message' => $message,
        ];
        return $this->templateRenderer->render($template, $data);
    }
}

You can see that it takes a TemplateRendererInterface object in the constructor which the NotFoundHandler requires. It then defines a notFound() method which returns an HtmlResponse object, where the body is the result of rendering Mezzio’s default 404 error template, using the string ($message) provided.

Full credit, I borrowed the example code above from Thomas in the Laminas Slack. Thanks, Thomas!

Then, in your handler, you could instantiate the NotFoundHelper directly and return the result of calling its notFound() function, as in the example below.

<?php

declare(strict_types=1);

namespace App\Handler;

use App\Helper\NotFoundHelper;
use Mezzio\Template\TemplateRendererInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;

class HomePageHandler implements RequestHandlerInterface
{
    public function __construct(
        private ?TemplateRendererInterface $template = null
    ) {}

    public function handle(
        ServerRequestInterface $request
    ): ResponseInterface
    {
        return new NotFoundHelper($this->template)
            ->notFound($request, "Oh no! It's a 404.");
    }
}

You could also require the NotFoundHelper as a constructor parameter. Your choice.

To me, this is the second most intuitive approach, but likely the most amount of work, as you’d have to determine the helper’s requirements, develop and maintain it. That said, it’d work exactly as you need it to.

That’s how to handle 404s in Mezzio

While there isn’t one, baked-in, approach in Mezzio, as there are in other PHP frameworks, Mezzio offers you a range of options for handling 404 errors. By doing so, you can choose the approach that makes the most sense for your application.

Which approach do you prefer?

Want to Learn More About Mezzio?

Mezzio Essentials teaches you the fundamentals of PHP's Mezzio framework. It's a practical, hands-on approach, which shows you just enough of about the underlying principles and concepts before stepping you through the process of creating an application.

You might also be interested in these tutorials too...

What’s the difference between PSR-15 Handlers and Middleware?
Mon, Jan 6, 2025

What’s the difference between PSR-15 Handlers and Middleware?

While building a simple user manager for Mezzio projects, recently, it turns out I’d gotten my understanding of PSR-15 Request Handlers and Middleware a little mixed up. Some friends set me straight about the difference, so I want to talk about that today.

Enable Mezzio Modules with laminas-component-installer
Mon, Dec 16, 2024

Enable Mezzio Modules with laminas-component-installer

When building reusable Mezzio packages, such as for user management, payments, and authentication, do users have to enable them manually, or are you automating it for them? In this short tutorial, I’ll show you how to enable them almost automatically, saving your users time and effort.


Want more tutorials like this?

If so, enter your email address in the field below and click subscribe.

You can unsubscribe at any time by clicking the link in the footer of the emails you'll receive. Here's my privacy policy, if you'd like to know more. I use Mailchimp to send emails. You can learn more about their privacy practices here.

Join the discussion

comments powered by Disqus