Add custom 404 and 500 error pages via ExceptionListener
- Create ExceptionListener that renders custom error templates in prod - Skip in dev environment to keep Symfony debug pages - 404: neo-brutalist page with large "404" text and return link - 500: generic server error page with status code display - Add 4 unit tests (dev skip, 404 render, 500 render, other HTTP codes) - Register kernel.environment parameter for ExceptionListener Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -34,6 +34,10 @@ services:
|
||||
key: '%env(S3_ACCESS_KEY)%'
|
||||
secret: '%env(S3_SECRET_KEY)%'
|
||||
|
||||
App\EventListener\ExceptionListener:
|
||||
arguments:
|
||||
$kernelEnvironment: '%kernel.environment%'
|
||||
|
||||
App\Twig\ViteAssetExtension:
|
||||
arguments:
|
||||
$manifest: '%kernel.project_dir%/public/build/.vite/manifest.json'
|
||||
|
||||
47
src/EventListener/ExceptionListener.php
Normal file
47
src/EventListener/ExceptionListener.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\EventListener;
|
||||
|
||||
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
|
||||
use Symfony\Component\HttpKernel\KernelEvents;
|
||||
use Twig\Environment;
|
||||
|
||||
#[AsEventListener(event: KernelEvents::EXCEPTION)]
|
||||
class ExceptionListener
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Environment $twig,
|
||||
private readonly string $kernelEnvironment,
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(ExceptionEvent $event): void
|
||||
{
|
||||
if ('dev' === $this->kernelEnvironment) {
|
||||
return;
|
||||
}
|
||||
|
||||
$exception = $event->getThrowable();
|
||||
$statusCode = $exception instanceof HttpExceptionInterface ? $exception->getStatusCode() : 500;
|
||||
|
||||
if (404 === $statusCode) {
|
||||
$template = 'error/404.html.twig';
|
||||
} else {
|
||||
$template = 'error/500.html.twig';
|
||||
}
|
||||
|
||||
$response = new Response(
|
||||
$this->twig->render($template, ['status_code' => $statusCode]),
|
||||
$statusCode,
|
||||
);
|
||||
|
||||
if ($exception instanceof HttpExceptionInterface) {
|
||||
$response->headers->add($exception->getHeaders());
|
||||
}
|
||||
|
||||
$event->setResponse($response);
|
||||
}
|
||||
}
|
||||
24
templates/error/404.html.twig
Normal file
24
templates/error/404.html.twig
Normal file
@@ -0,0 +1,24 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Page introuvable - E-Ticket{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="bg-[#fbfbfb] overflow-x-hidden italic font-sans">
|
||||
<section class="relative flex items-center justify-center px-4 pt-20 pb-16 min-h-[60vh]">
|
||||
<div class="absolute inset-0 opacity-[0.03] pointer-events-none select-none overflow-hidden">
|
||||
<span class="text-[20rem] font-black uppercase leading-none block -rotate-12 translate-y-10">404</span>
|
||||
</div>
|
||||
|
||||
<div class="max-w-2xl mx-auto relative z-10 text-center">
|
||||
<div class="border-4 border-gray-900 bg-white shadow-[6px_6px_0px_rgba(0,0,0,1)] p-12">
|
||||
<div class="text-8xl font-black uppercase tracking-tighter mb-4">404</div>
|
||||
<h1 class="text-3xl font-black uppercase tracking-tighter italic mb-4">Page introuvable</h1>
|
||||
<p class="font-bold text-gray-500 italic mb-8">La page que vous cherchez n'existe pas ou a ete deplacee.</p>
|
||||
<a href="/" style="padding:0.75rem 2rem;border:3px solid #111827;box-shadow:4px 4px 0 rgba(0,0,0,1);background:#fabf04;display:inline-block;" class="font-black uppercase text-sm tracking-widest hover:bg-indigo-600 hover:text-white transition-all">
|
||||
Retour a l'accueil
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
{% endblock %}
|
||||
24
templates/error/500.html.twig
Normal file
24
templates/error/500.html.twig
Normal file
@@ -0,0 +1,24 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Erreur serveur - E-Ticket{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="bg-[#fbfbfb] overflow-x-hidden italic font-sans">
|
||||
<section class="relative flex items-center justify-center px-4 pt-20 pb-16 min-h-[60vh]">
|
||||
<div class="absolute inset-0 opacity-[0.03] pointer-events-none select-none overflow-hidden">
|
||||
<span class="text-[20rem] font-black uppercase leading-none block -rotate-12 translate-y-10">500</span>
|
||||
</div>
|
||||
|
||||
<div class="max-w-2xl mx-auto relative z-10 text-center">
|
||||
<div class="border-4 border-gray-900 bg-white shadow-[6px_6px_0px_rgba(0,0,0,1)] p-12">
|
||||
<div class="text-8xl font-black uppercase tracking-tighter mb-4">{{ status_code }}</div>
|
||||
<h1 class="text-3xl font-black uppercase tracking-tighter italic mb-4">Erreur serveur</h1>
|
||||
<p class="font-bold text-gray-500 italic mb-8">Une erreur inattendue s'est produite. Nos equipes ont ete notifiees.</p>
|
||||
<a href="/" style="padding:0.75rem 2rem;border:3px solid #111827;box-shadow:4px 4px 0 rgba(0,0,0,1);background:#fabf04;display:inline-block;" class="font-black uppercase text-sm tracking-widest hover:bg-indigo-600 hover:text-white transition-all">
|
||||
Retour a l'accueil
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
{% endblock %}
|
||||
86
tests/EventListener/ExceptionListenerTest.php
Normal file
86
tests/EventListener/ExceptionListenerTest.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\EventListener;
|
||||
|
||||
use App\EventListener\ExceptionListener;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\HttpKernel\HttpKernelInterface;
|
||||
use Twig\Environment;
|
||||
|
||||
class ExceptionListenerTest extends TestCase
|
||||
{
|
||||
public function testDoesNothingInDevEnvironment(): void
|
||||
{
|
||||
$twig = $this->createMock(Environment::class);
|
||||
$twig->expects(self::never())->method('render');
|
||||
|
||||
$listener = new ExceptionListener($twig, 'dev');
|
||||
$event = $this->createEvent(new \RuntimeException('test'));
|
||||
|
||||
$listener($event);
|
||||
|
||||
self::assertNull($event->getResponse());
|
||||
}
|
||||
|
||||
public function testRenders404Template(): void
|
||||
{
|
||||
$twig = $this->createMock(Environment::class);
|
||||
$twig->expects(self::once())
|
||||
->method('render')
|
||||
->with('error/404.html.twig', ['status_code' => 404])
|
||||
->willReturn('<h1>404</h1>');
|
||||
|
||||
$listener = new ExceptionListener($twig, 'prod');
|
||||
$event = $this->createEvent(new NotFoundHttpException('Not found'));
|
||||
|
||||
$listener($event);
|
||||
|
||||
self::assertNotNull($event->getResponse());
|
||||
self::assertSame(404, $event->getResponse()->getStatusCode());
|
||||
self::assertSame('<h1>404</h1>', $event->getResponse()->getContent());
|
||||
}
|
||||
|
||||
public function testRenders500TemplateForGenericException(): void
|
||||
{
|
||||
$twig = $this->createMock(Environment::class);
|
||||
$twig->expects(self::once())
|
||||
->method('render')
|
||||
->with('error/500.html.twig', ['status_code' => 500])
|
||||
->willReturn('<h1>500</h1>');
|
||||
|
||||
$listener = new ExceptionListener($twig, 'prod');
|
||||
$event = $this->createEvent(new \RuntimeException('Server error'));
|
||||
|
||||
$listener($event);
|
||||
|
||||
self::assertNotNull($event->getResponse());
|
||||
self::assertSame(500, $event->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
public function testRenders500TemplateForOtherHttpCodes(): void
|
||||
{
|
||||
$twig = $this->createMock(Environment::class);
|
||||
$twig->expects(self::once())
|
||||
->method('render')
|
||||
->with('error/500.html.twig', ['status_code' => 403])
|
||||
->willReturn('<h1>403</h1>');
|
||||
|
||||
$listener = new ExceptionListener($twig, 'prod');
|
||||
$event = $this->createEvent(new \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException('Forbidden'));
|
||||
|
||||
$listener($event);
|
||||
|
||||
self::assertNotNull($event->getResponse());
|
||||
self::assertSame(403, $event->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
private function createEvent(\Throwable $exception): ExceptionEvent
|
||||
{
|
||||
$kernel = $this->createMock(HttpKernelInterface::class);
|
||||
|
||||
return new ExceptionEvent($kernel, new Request(), HttpKernelInterface::MAIN_REQUEST, $exception);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user