Skip to content

Instantly share code, notes, and snippets.

@zanbaldwin
Created May 15, 2025 12:02
Show Gist options
  • Save zanbaldwin/cea8b72301c3f3f802234a81b59b620d to your computer and use it in GitHub Desktop.
Save zanbaldwin/cea8b72301c3f3f802234a81b59b620d to your computer and use it in GitHub Desktop.
Symfony phased migration strategy listener
<?php
declare(strict_types=1);
namespace App\Listener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Routing\Exception\MethodNotAllowedException;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
/**
* This listener is a key component in a phased migration strategy using an AB-proxy.
*
* When migrating from one (legacy) application to another (newer) application
* (placing this listener in the newer application), the proxy will initially
* direct traffic to this new application. If this application responds with 404
* and the header `X-Not-Found-Reason: routing` (different to the route being
* implemented and the controller returning 404 without the header), the proxy
* will assume the request is asking for a page that has not been implemented
* yet in the new application and will forward the request to the legacy application.
*/
final class NoRoutingListener implements EventSubscriberInterface
{
public const array ROUTING_EXCEPTIONS = [ResourceNotFoundException::class, MethodNotAllowedException::class];
public const array ROUTING_STATUS_CODES = [Response::HTTP_NOT_FOUND, Response::HTTP_METHOD_NOT_ALLOWED];
public const string HEADER_NAME = 'X-Not-Found-Reason';
public const string EXCEPTION_REASON_ROUTING = 'routing';
public const string EXCEPTION_REASON_APPLICATION = 'application';
private bool $isRoutingException = false;
public static function getSubscribedEvents(): array
{
return [
KernelEvents::EXCEPTION => ['onKernelException', 1024],
KernelEvents::RESPONSE => ['onKernelResponse', -1024],
];
}
public function onKernelException(ExceptionEvent $event): void
{
$this->isRoutingException = $this->isRoutingException($event->getThrowable());
}
public function onKernelResponse(ResponseEvent $event): void
{
$response = $event->getResponse();
if (in_array($response->getStatusCode(), self::ROUTING_STATUS_CODES, true)) {
$reason = $this->isRoutingException ? self::EXCEPTION_REASON_ROUTING : self::EXCEPTION_REASON_APPLICATION;
$response->headers->set(self::HEADER_NAME, $reason);
}
}
private function isRoutingException(\Throwable $exception): bool
{
do {
if (in_array($exception::class, self::ROUTING_EXCEPTIONS, true)) {
return true;
}
} while (null !== $exception = $exception->getPrevious());
return false;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment