From 2458ceb71f98b8b6fae80651a14a33e1c2ebb8ad Mon Sep 17 00:00:00 2001 From: Amp Date: Tue, 13 Oct 2026 14:53:14 +0000 Subject: [PATCH] Add opt-in priority for the generated QR code route Preserve existing route precedence by default while allowing applications to place the QR code route ahead of catch-all routes. Keep explicit routes and two-argument loader construction unchanged. Fixes #95 Amp-Thread-ID: https://ampcode.com/threads/T-01a135b3-5c83-75eb-aef1-0c8b4f44f342 Co-authored-by: Jeroen van den Enden --- README.md | 12 ++++ src/EndroidQrCodeBundle.php | 3 +- src/Routing/QrCodeRouteLoader.php | 3 +- tests/Routing/QrCodeRouteLoaderTest.php | 60 +++++++++++++++++++ .../config/packages/endroid_qr_code.yaml | 1 + .../config/routes/endroid_qr_code_twig.yaml | 7 +++ 6 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 tests/Routing/QrCodeRouteLoaderTest.php diff --git a/README.md b/README.md index fd53205..2aedb1f 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,18 @@ endroid_qr_code: This makes the route available at `/my-custom-prefix/{builder}/{data}`. +The generated route has priority `0` by default, preserving the existing route order. +If an application catch-all route intercepts QR code URLs, set a higher priority: + +```yaml +endroid_qr_code: + route_priority: 1 +``` + +Higher priorities are matched first, so choose a value above your catch-all's priority. +This only affects the automatically registered route; an explicitly defined +`qr_code_generate` route is left unchanged. + ## Builder Configuration The bundle makes use of builders to create QR codes. The default parameters diff --git a/src/EndroidQrCodeBundle.php b/src/EndroidQrCodeBundle.php index f13cbb3..ad11b0d 100644 --- a/src/EndroidQrCodeBundle.php +++ b/src/EndroidQrCodeBundle.php @@ -49,6 +49,7 @@ public function configure(DefinitionConfigurator $definition): void $children->scalarNode('route_prefix')->defaultValue('/qr-code'); $children->booleanNode('route_enabled')->defaultTrue(); + $children->integerNode('route_priority')->defaultValue(0); $children ->arrayNode('builders') ->useAttributeAsKey('name') @@ -106,7 +107,7 @@ public function loadExtension(array $config, ContainerConfigurator $configurator // This ensures routes work on all Symfony versions without user configuration $services ->set('endroid_qr_code.route_loader', QrCodeRouteLoader::class) - ->args(['', $routePrefix]) + ->args(['', $routePrefix, $config['route_priority'] ?? 0]) ->tag('routing.route_loader'); } diff --git a/src/Routing/QrCodeRouteLoader.php b/src/Routing/QrCodeRouteLoader.php index 2a01cfc..4270882 100644 --- a/src/Routing/QrCodeRouteLoader.php +++ b/src/Routing/QrCodeRouteLoader.php @@ -14,6 +14,7 @@ public function __construct( private string $originalResource, private string $routePrefix, + private int $routePriority = 0, ) {} public function __invoke(LoaderInterface $loader, ?string $_env): RouteCollection @@ -30,7 +31,7 @@ public function __invoke(LoaderInterface $loader, ?string $_env): RouteCollectio ['data' => '[\w\W]+'], ); $route->setOption('utf8', true); - $collection->add('qr_code_generate', $route); + $collection->add('qr_code_generate', $route, $this->routePriority); } return $collection; diff --git a/tests/Routing/QrCodeRouteLoaderTest.php b/tests/Routing/QrCodeRouteLoaderTest.php new file mode 100644 index 0000000..a0ea0a3 --- /dev/null +++ b/tests/Routing/QrCodeRouteLoaderTest.php @@ -0,0 +1,60 @@ +add('catch_all', new Route('/{slug}', [], ['slug' => '.*']), $catchAllPriority); + + $loader = $this->createStub(LoaderInterface::class); + $loader->method('load')->willReturn($routes); + + $routeLoader = $priority === null + ? new QrCodeRouteLoader('app', '/2fa/qr') + : new QrCodeRouteLoader('app', '/2fa/qr', $priority); + $matcher = new UrlMatcher($routeLoader($loader, null), new RequestContext()); + + self::assertSame($expectedRoute, $matcher->match('/2fa/qr/default/test')['_route']); + self::assertSame('catch_all', $matcher->match('/other/page')['_route']); + } + + public static function providePriorities(): iterable + { + yield 'omitted priority preserves application precedence' => [null, 0, 'catch_all']; + yield 'equal priority preserves application precedence' => [0, 0, 'catch_all']; + yield 'higher priority beats catch-all' => [1, 0, 'qr_code_generate']; + yield 'higher application priority still wins' => [1, 2, 'catch_all']; + yield 'negative priorities are respected' => [-2, -1, 'catch_all']; + } + + public function testExplicitRouteIsPreserved(): void + { + $routes = new RouteCollection(); + $routes->add('catch_all', new Route('/{slug}', [], ['slug' => '.*'])); + $routes->add('qr_code_generate', new Route('/manual/{data}'), 1); + + $loader = $this->createStub(LoaderInterface::class); + $loader->method('load')->willReturn($routes); + + $routeLoader = new QrCodeRouteLoader('app', '/2fa/qr', -1); + $matcher = new UrlMatcher($routeLoader($loader, null), new RequestContext()); + + self::assertSame('qr_code_generate', $matcher->match('/manual/test')['_route']); + self::assertSame('catch_all', $matcher->match('/2fa/qr/default/test')['_route']); + } +} diff --git a/tests/application/config/packages/endroid_qr_code.yaml b/tests/application/config/packages/endroid_qr_code.yaml index 14fa2ae..8665016 100644 --- a/tests/application/config/packages/endroid_qr_code.yaml +++ b/tests/application/config/packages/endroid_qr_code.yaml @@ -1,4 +1,5 @@ endroid_qr_code: + route_priority: 1 builders: default: writer: Endroid\QrCode\Writer\PngWriter diff --git a/tests/application/config/routes/endroid_qr_code_twig.yaml b/tests/application/config/routes/endroid_qr_code_twig.yaml index 6bca6b8..812ab57 100644 --- a/tests/application/config/routes/endroid_qr_code_twig.yaml +++ b/tests/application/config/routes/endroid_qr_code_twig.yaml @@ -7,3 +7,10 @@ endroid_qr_code_twig_invalid_option: path: /twig-invalid-option controller: Symfony\Bundle\FrameworkBundle\Controller\TemplateController defaults: { template: 'twig-invalid-option.html.twig' } + +# The QR code route must outrank an earlier catch-all when configured to do so. +catch_all: + path: /{slug} + requirements: { slug: '.*' } + controller: Symfony\Bundle\FrameworkBundle\Controller\TemplateController + defaults: { template: 'twig.html.twig' }