Create templated 404 page in Slim 4 with CallableResolver
In Slim 4 Skeleton, the 404 response handled in App\Application\Handlers\HttpErrorHandler::respond()
which check against Slim\Exception\HttpNotFoundException
. We can create a templated 404 page for it with utilize Slim\CallableResolver
via callableResolver
property which can resolve the callable handler. I assume that we are using Twig template engine, and already has setup of Twig service like in my previous post.
We can create a not found handler like the following in src/Application/Handlers/NotFoundHandler.php
<?php // src/Application/Handlers/NotFoundHandler.php declare(strict_types=1); namespace App\Application\Handlers; use Psr\Http\Message\ResponseInterface; use Slim\Views\Twig; use function compact; class NotFoundHandler { private $view; public function __construct(Twig $view) { $this->view = $view; } public function __invoke( ResponseInterface $response, string $message ): ResponseInterface { return $this->view->render($response, '404.html.twig', compact('message')); } }
We can create a view based on it like the following at templates/404.html.twig
:
{# templates/404.html.twig #} {% extends "layout.html.twig" %} {% block title '404 - '~parent() %} {% block body %} {{ message }} {% endblock %}
Now, in App\Application\Handlers\HttpErrorHandler::respond()
, we can check when $exception instanceof HttpNotFoundException to make a self called resolved NotFoundHandler.
// src/Application/Handlers/HttpErrorHandler.php // ... protected function respond(): Response { // ... if ($exception instanceof HttpNotFoundException) { $response = $this->responseFactory->createResponse($statusCode); return ($this->callableResolver->resolve(NotFoundHandler::class)( $response, $exception->getMessage() )); } // ... } // ...
So, when the Slim\Exception\HttpNotFoundException
thrown, it will shown the 404 page with brought the message passed into it like the following:
Bonus
We can create a handling against Request as well, eg: show 404 templated page only when request doesn’t has Accept: application/json or X-Requested-With:XmlHttpRequest header, so, we can modify like the following:
// src/Application/Handlers/HttpErrorHandler.php // ... protected function respond(): Response { // ... if ($exception instanceof HttpNotFoundException) { $isAppJsonAccept = $this->request->getHeaderLine('Accept') === 'application/json'; $isXmlHttpRequest = $this->request->getHeaderLine('X-Requested-With') === 'XmlHttpRequest'; if (! $isAppJsonAccept && ! $isXmlHttpRequest) { $response = $this->responseFactory->createResponse($statusCode); return ($this->callableResolver->resolve(NotFoundHandler::class)( $response, $exception->getMessage() )); } // already return early, no need else $error->setType(ActionError::RESOURCE_NOT_FOUND); } // ... } // ...
So, for example, when called via ajax, it will show like the following:
Create “Abstract Factory” service in PHP-DI with RequestedEntry and Wildcards
If you’re familiar with Zend Framework’s servicemanager, you may already used the abstract factory which acts as limbo when service not registered. When the pattern checked is matched, it will try to create service based on it, automatically.
We can do that in PHP-DI as well. For example, we are experimenting with my previous post on DDD in slim 4 framework:
├───Application ├───Domain │ ├───DomainException │ │ DomainException.php │ │ DomainRecordNotFoundException.php │ │ │ ├───Post │ │ Post.php │ │ PostNotFoundException.php │ │ PostRepository.php │ └───Infrastructure └───Persistence ├───Post │ ZendDbPostRepository.php
We are going to create service with pattern “App\Domain\Post\PostRepository” from App\Infrastructure\Persistence
namespace with as an object from “App\Infrastructure\Persistence\Post\ZendDbPostRepository”. In PHP-DI, we can combine both RequestedEntry and Wildcards definitions, the service definition will be like the following:
<?php declare(strict_types=1); use DI\ContainerBuilder; use Psr\Container\ContainerInterface; use Zend\Db\Adapter\AdapterInterface; use Zend\Db\ResultSet\HydratingResultSet; use Zend\Db\TableGateway\TableGateway; use Zend\Hydrator\ObjectPropertyHydrator; use DI\Factory\RequestedEntry; return function (ContainerBuilder $containerBuilder) { $containerBuilder->addDefinitions([ 'App\Domain\*\*Repository' => function (RequestedEntry $entry, ContainerInterface $c) { // get entity class name, // eg: "Post" by service named "App\Domain\Post\PostRepository" preg_match( '/(?<=App\\\\Domain\\\\)([A-Z][a-z]{1,})(?=\\\\\1Repository)/', $entry->getName(), $matches ); $entity = current($matches); $fullEntityClass = 'App\Domain' . str_repeat('\\' . $entity, 2); $fullRepoClass = 'App\Infrastructure\Persistence' . '\\' . $entity . '\ZendDb' . $entity . 'Repository'; $tableGateway = new TableGateway( $fullEntityClass::TABLE, $c->get(AdapterInterface::class), null, new HydratingResultSet(new ObjectPropertyHydrator(), new $fullEntityClass) ); return new $fullRepoClass($tableGateway); }, ]); };
That’s it!
leave a comment