From cdc9b34129a4224072b2517d8be620a1f3ad47b3 Mon Sep 17 00:00:00 2001 From: soyuka Date: Tue, 16 Jun 2026 13:43:01 +0200 Subject: [PATCH] feat(symfony): add routePriority to control route matching order Expose Symfony RouteCollection::add() third argument via a new nullable ?int $routePriority operation attribute, distinct from the existing $priority (which sorts operations within a resource). Wired through all HttpOperation subclasses (Get, GetCollection, Post, Put, Patch, Delete, NotExposed, McpResource, McpTool) and the YAML/XML extractors + XSD schema so the attribute is usable from every config format. Closes #8135 --- src/Metadata/Delete.php | 2 ++ src/Metadata/Extractor/XmlResourceExtractor.php | 1 + src/Metadata/Extractor/YamlResourceExtractor.php | 1 + src/Metadata/Extractor/schema/resources.xsd | 1 + src/Metadata/Get.php | 2 ++ src/Metadata/GetCollection.php | 2 ++ src/Metadata/HttpOperation.php | 2 ++ src/Metadata/McpResource.php | 2 ++ src/Metadata/McpTool.php | 2 ++ src/Metadata/NotExposed.php | 2 ++ src/Metadata/Operation.php | 14 ++++++++++++++ src/Metadata/Patch.php | 2 ++ src/Metadata/Post.php | 2 ++ src/Metadata/Put.php | 2 ++ src/Symfony/Routing/ApiLoader.php | 2 +- tests/Symfony/Routing/ApiLoaderTest.php | 15 +++++++++++++++ 16 files changed, 53 insertions(+), 1 deletion(-) diff --git a/src/Metadata/Delete.php b/src/Metadata/Delete.php index 61744470fd..8a4f994829 100644 --- a/src/Metadata/Delete.php +++ b/src/Metadata/Delete.php @@ -91,6 +91,7 @@ public function __construct( ?bool $fetchPartial = null, ?bool $forceEager = null, ?int $priority = null, + ?int $routePriority = null, ?string $name = null, $provider = null, $processor = null, @@ -176,6 +177,7 @@ class: $class, fetchPartial: $fetchPartial, forceEager: $forceEager, priority: $priority, + routePriority: $routePriority, name: $name, provider: $provider, processor: $processor, diff --git a/src/Metadata/Extractor/XmlResourceExtractor.php b/src/Metadata/Extractor/XmlResourceExtractor.php index d439c1e981..bf3539233c 100644 --- a/src/Metadata/Extractor/XmlResourceExtractor.php +++ b/src/Metadata/Extractor/XmlResourceExtractor.php @@ -428,6 +428,7 @@ private function buildOperations(\SimpleXMLElement $resource, array $root): ?arr 'serialize' => $this->phpize($operation, 'serialize', 'bool'), 'queryParameterValidate' => $this->phpize($operation, 'queryParameterValidate', 'bool'), 'priority' => $this->phpize($operation, 'priority', 'integer'), + 'routePriority' => $this->phpize($operation, 'routePriority', 'integer'), 'name' => $this->phpize($operation, 'name', 'string'), 'routeName' => $this->phpize($operation, 'routeName', 'string'), ]); diff --git a/src/Metadata/Extractor/YamlResourceExtractor.php b/src/Metadata/Extractor/YamlResourceExtractor.php index 38ac28e057..068c99d02c 100644 --- a/src/Metadata/Extractor/YamlResourceExtractor.php +++ b/src/Metadata/Extractor/YamlResourceExtractor.php @@ -357,6 +357,7 @@ private function buildOperations(array $resource, array $root): ?array 'strictQueryParameterValidation' => $this->phpize($operation, 'strictQueryParameterValidation', 'bool'), 'hideHydraOperation' => $this->phpize($resource, 'hideHydraOperation', 'bool'), 'priority' => $this->phpize($operation, 'priority', 'integer'), + 'routePriority' => $this->phpize($operation, 'routePriority', 'integer'), 'name' => $this->phpize($operation, 'name', 'string'), 'class' => (string) $class, ]); diff --git a/src/Metadata/Extractor/schema/resources.xsd b/src/Metadata/Extractor/schema/resources.xsd index 6019722d6b..ab5652c084 100644 --- a/src/Metadata/Extractor/schema/resources.xsd +++ b/src/Metadata/Extractor/schema/resources.xsd @@ -46,6 +46,7 @@ + diff --git a/src/Metadata/Get.php b/src/Metadata/Get.php index 82a01f83dc..0c245a2db7 100644 --- a/src/Metadata/Get.php +++ b/src/Metadata/Get.php @@ -91,6 +91,7 @@ public function __construct( ?bool $fetchPartial = null, ?bool $forceEager = null, ?int $priority = null, + ?int $routePriority = null, ?string $name = null, $provider = null, $processor = null, @@ -176,6 +177,7 @@ class: $class, fetchPartial: $fetchPartial, forceEager: $forceEager, priority: $priority, + routePriority: $routePriority, name: $name, provider: $provider, processor: $processor, diff --git a/src/Metadata/GetCollection.php b/src/Metadata/GetCollection.php index a94ae24099..9810442359 100644 --- a/src/Metadata/GetCollection.php +++ b/src/Metadata/GetCollection.php @@ -91,6 +91,7 @@ public function __construct( ?bool $fetchPartial = null, ?bool $forceEager = null, ?int $priority = null, + ?int $routePriority = null, ?string $name = null, $provider = null, $processor = null, @@ -177,6 +178,7 @@ class: $class, fetchPartial: $fetchPartial, forceEager: $forceEager, priority: $priority, + routePriority: $routePriority, name: $name, provider: $provider, processor: $processor, diff --git a/src/Metadata/HttpOperation.php b/src/Metadata/HttpOperation.php index a8f28f22d8..a7fb0d0f49 100644 --- a/src/Metadata/HttpOperation.php +++ b/src/Metadata/HttpOperation.php @@ -212,6 +212,7 @@ public function __construct( ?bool $fetchPartial = null, ?bool $forceEager = null, ?int $priority = null, + ?int $routePriority = null, ?string $name = null, $provider = null, $processor = null, @@ -272,6 +273,7 @@ class: $class, fetchPartial: $fetchPartial, forceEager: $forceEager, priority: $priority, + routePriority: $routePriority, name: $name, provider: $provider, processor: $processor, diff --git a/src/Metadata/McpResource.php b/src/Metadata/McpResource.php index 5be8ab91f3..dedd081693 100644 --- a/src/Metadata/McpResource.php +++ b/src/Metadata/McpResource.php @@ -173,6 +173,7 @@ public function __construct( ?bool $fetchPartial = null, ?bool $forceEager = null, ?int $priority = null, + ?int $routePriority = null, $provider = null, $processor = null, ?OptionsInterface $stateOptions = null, @@ -257,6 +258,7 @@ class: $class, fetchPartial: $fetchPartial, forceEager: $forceEager, priority: $priority, + routePriority: $routePriority, name: $name, provider: $provider, processor: $processor, diff --git a/src/Metadata/McpTool.php b/src/Metadata/McpTool.php index f46f7a297d..40d87315e3 100644 --- a/src/Metadata/McpTool.php +++ b/src/Metadata/McpTool.php @@ -169,6 +169,7 @@ public function __construct( ?bool $fetchPartial = null, ?bool $forceEager = null, ?int $priority = null, + ?int $routePriority = null, $provider = null, $processor = null, ?OptionsInterface $stateOptions = null, @@ -253,6 +254,7 @@ class: $class, fetchPartial: $fetchPartial, forceEager: $forceEager, priority: $priority, + routePriority: $routePriority, name: $name, provider: $provider, processor: $processor, diff --git a/src/Metadata/NotExposed.php b/src/Metadata/NotExposed.php index c3422bac24..e7537218df 100644 --- a/src/Metadata/NotExposed.php +++ b/src/Metadata/NotExposed.php @@ -104,6 +104,7 @@ public function __construct( ?bool $fetchPartial = null, ?bool $forceEager = null, ?int $priority = null, + ?int $routePriority = null, ?string $name = null, $provider = null, $processor = null, @@ -182,6 +183,7 @@ class: $class, fetchPartial: $fetchPartial, forceEager: $forceEager, priority: $priority, + routePriority: $routePriority, name: $name, provider: $provider, processor: $processor, diff --git a/src/Metadata/Operation.php b/src/Metadata/Operation.php index 343915a673..197cf570bf 100644 --- a/src/Metadata/Operation.php +++ b/src/Metadata/Operation.php @@ -802,6 +802,7 @@ public function __construct( * Sort is ascendant: a lower priority comes first in the list. */ protected ?int $priority = null, + protected ?int $routePriority = null, protected ?string $name = null, protected $provider = null, protected $processor = null, @@ -965,6 +966,19 @@ public function withPriority(int $priority = 0): static return $self; } + public function getRoutePriority(): ?int + { + return $this->routePriority; + } + + public function withRoutePriority(int $routePriority): static + { + $self = clone $this; + $self->routePriority = $routePriority; + + return $self; + } + public function getName(): ?string { return $this->name; diff --git a/src/Metadata/Patch.php b/src/Metadata/Patch.php index 100ac370e7..118c1887ea 100644 --- a/src/Metadata/Patch.php +++ b/src/Metadata/Patch.php @@ -91,6 +91,7 @@ public function __construct( ?bool $fetchPartial = null, ?bool $forceEager = null, ?int $priority = null, + ?int $routePriority = null, ?string $name = null, $provider = null, $processor = null, @@ -177,6 +178,7 @@ class: $class, fetchPartial: $fetchPartial, forceEager: $forceEager, priority: $priority, + routePriority: $routePriority, name: $name, provider: $provider, processor: $processor, diff --git a/src/Metadata/Post.php b/src/Metadata/Post.php index 61a4a059c7..990cb88a9c 100644 --- a/src/Metadata/Post.php +++ b/src/Metadata/Post.php @@ -91,6 +91,7 @@ public function __construct( ?bool $fetchPartial = null, ?bool $forceEager = null, ?int $priority = null, + ?int $routePriority = null, ?string $name = null, $provider = null, $processor = null, @@ -178,6 +179,7 @@ class: $class, fetchPartial: $fetchPartial, forceEager: $forceEager, priority: $priority, + routePriority: $routePriority, name: $name, provider: $provider, processor: $processor, diff --git a/src/Metadata/Put.php b/src/Metadata/Put.php index 87529e9587..3cf5c3b368 100644 --- a/src/Metadata/Put.php +++ b/src/Metadata/Put.php @@ -91,6 +91,7 @@ public function __construct( ?bool $fetchPartial = null, ?bool $forceEager = null, ?int $priority = null, + ?int $routePriority = null, ?string $name = null, $provider = null, $processor = null, @@ -178,6 +179,7 @@ class: $class, fetchPartial: $fetchPartial, forceEager: $forceEager, priority: $priority, + routePriority: $routePriority, name: $name, provider: $provider, processor: $processor, diff --git a/src/Symfony/Routing/ApiLoader.php b/src/Symfony/Routing/ApiLoader.php index 1fa70ab296..259fd61347 100644 --- a/src/Symfony/Routing/ApiLoader.php +++ b/src/Symfony/Routing/ApiLoader.php @@ -126,7 +126,7 @@ public function load(mixed $data, ?string $type = null): RouteCollection $operation->getCondition() ?? '' ); - $routeCollection->add($operationName, $route); + $routeCollection->add($operationName, $route, $operation->getRoutePriority() ?? 0); } } } diff --git a/tests/Symfony/Routing/ApiLoaderTest.php b/tests/Symfony/Routing/ApiLoaderTest.php index 38734a6e62..e9b98aed77 100644 --- a/tests/Symfony/Routing/ApiLoaderTest.php +++ b/tests/Symfony/Routing/ApiLoaderTest.php @@ -282,6 +282,21 @@ public function testApiLoaderIrisTypeRegistersItemRoutesWithNotExposedController $this->assertNotNull($routeCollection->get('api_genid')); } + public function testApiLoaderWithRoutePriority(): void + { + $resourceCollection = new ResourceMetadataCollection(Dummy::class, [ + (new ApiResource())->withShortName('dummy')->withOperations(new Operations([ + 'api_dummies_get_item' => (new Get())->withUriTemplate('/dummies/{id}')->withRoutePriority(10), + 'api_dummies_get_collection' => (new GetCollection())->withUriTemplate('/dummies'), + ])), + ]); + + $routeCollection = $this->getApiLoaderWithResourceMetadataCollection($resourceCollection)->load(null); + + $this->assertSame(10, $routeCollection->getPriority('api_dummies_get_item')); + $this->assertNull($routeCollection->getPriority('api_dummies_get_collection')); + } + public function testApiLoaderWithUndefinedControllerService(): void { $this->expectExceptionObject(new \RuntimeException('Operation "api_dummies_my_undefined_controller_method_item" is defining an unknown service as controller "Foo\\Bar\\MyUndefinedController". Make sure it is properly registered in the dependency injection container.'));