Created
May 15, 2025 12:02
-
-
Save zanbaldwin/cea8b72301c3f3f802234a81b59b620d to your computer and use it in GitHub Desktop.
Symfony phased migration strategy listener
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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