From a61370efd46ff95e0695e92df95808f4bac86ece Mon Sep 17 00:00:00 2001 From: wuchen90 Date: Thu, 1 Oct 2026 12:37:12 +0200 Subject: [PATCH] fix(symfony): process parameters declared in resource files The parameter metadata factories (core, validator, Doctrine ORM and ODM) decorated the resource metadata collection factory with a higher priority than the YAML, XML, PHP file and concerns factories (800), so they only saw parameters declared with attributes. Parameters declared in resource files got no property default, no filter schema or OpenAPI parameters and no Doctrine nested property information, which broke modern filters on nested properties with a semantical error. Decorate them right below the file factories instead, keeping their relative order. Fixes #7655 --- .../Resources/config/doctrine_mongodb_odm.php | 2 +- .../Bundle/Resources/config/doctrine_orm.php | 2 +- .../Resources/config/metadata/resource.php | 3 +- .../Resources/config/validator/validator.php | 2 +- .../TestBundle/Entity/Issue7655/Author.php | 51 ++++++++++ .../TestBundle/Entity/Issue7655/Book.php | 69 +++++++++++++ .../config/api_resources_orm/resources.yaml | 12 +++ tests/Fixtures/app/config/config_doctrine.yml | 4 + .../Issue7655FileDeclaredParameterTest.php | 99 +++++++++++++++++++ 9 files changed, 240 insertions(+), 4 deletions(-) create mode 100644 tests/Fixtures/TestBundle/Entity/Issue7655/Author.php create mode 100644 tests/Fixtures/TestBundle/Entity/Issue7655/Book.php create mode 100644 tests/Functional/Parameters/Issue7655FileDeclaredParameterTest.php diff --git a/src/Symfony/Bundle/Resources/config/doctrine_mongodb_odm.php b/src/Symfony/Bundle/Resources/config/doctrine_mongodb_odm.php index 8e7625bf4af..f8f261c50d9 100644 --- a/src/Symfony/Bundle/Resources/config/doctrine_mongodb_odm.php +++ b/src/Symfony/Bundle/Resources/config/doctrine_mongodb_odm.php @@ -239,7 +239,7 @@ ]); $services->set('api_platform.doctrine.odm.metadata.resource.parameter_metadata_collection_factory', DoctrineMongoDbOdmParameterResourceMetadataCollectionFactory::class) - ->decorate('api_platform.metadata.resource.metadata_collection_factory', null, 999) + ->decorate('api_platform.metadata.resource.metadata_collection_factory', null, 789) ->args([ service('doctrine_mongodb'), service('api_platform.doctrine.odm.metadata.resource.parameter_metadata_collection_factory.inner'), diff --git a/src/Symfony/Bundle/Resources/config/doctrine_orm.php b/src/Symfony/Bundle/Resources/config/doctrine_orm.php index 13806ccf0fb..9936955a95c 100644 --- a/src/Symfony/Bundle/Resources/config/doctrine_orm.php +++ b/src/Symfony/Bundle/Resources/config/doctrine_orm.php @@ -290,7 +290,7 @@ ]); $services->set('api_platform.doctrine.orm.metadata.resource.parameter_metadata_collection_factory', DoctrineOrmParameterResourceMetadataCollectionFactory::class) - ->decorate('api_platform.metadata.resource.metadata_collection_factory', null, 998) + ->decorate('api_platform.metadata.resource.metadata_collection_factory', null, 788) ->args([ service('doctrine'), service('api_platform.doctrine.orm.metadata.resource.parameter_metadata_collection_factory.inner'), diff --git a/src/Symfony/Bundle/Resources/config/metadata/resource.php b/src/Symfony/Bundle/Resources/config/metadata/resource.php index 137ad0515de..66cd67f32fe 100644 --- a/src/Symfony/Bundle/Resources/config/metadata/resource.php +++ b/src/Symfony/Bundle/Resources/config/metadata/resource.php @@ -151,8 +151,9 @@ ->decorate('api_platform.metadata.resource.metadata_collection_factory', null, 200) ->args([service('api_platform.metadata.resource.metadata_collection_factory.alternate_uri.inner')]); + // Parameter factories decorate below the YAML, XML and PHP file factories (800) so they also see parameters declared in resource files $services->set('api_platform.metadata.resource.metadata_collection_factory.parameter', ParameterResourceMetadataCollectionFactory::class) - ->decorate('api_platform.metadata.resource.metadata_collection_factory', null, 1000) + ->decorate('api_platform.metadata.resource.metadata_collection_factory', null, 790) ->args([ service('api_platform.metadata.property.name_collection_factory'), service('api_platform.metadata.property.metadata_factory'), diff --git a/src/Symfony/Bundle/Resources/config/validator/validator.php b/src/Symfony/Bundle/Resources/config/validator/validator.php index 9372bdb2346..0616c64f686 100644 --- a/src/Symfony/Bundle/Resources/config/validator/validator.php +++ b/src/Symfony/Bundle/Resources/config/validator/validator.php @@ -33,7 +33,7 @@ ->tag('api_platform.state_provider', ['key' => 'api_platform.validator.state.error_provider']); $services->set('api_platform.validator.metadata.resource.metadata_collection_factory.parameter', ParameterValidationResourceMetadataCollectionFactory::class) - ->decorate('api_platform.metadata.resource.metadata_collection_factory', null, 1000) + ->decorate('api_platform.metadata.resource.metadata_collection_factory', null, 790) ->args([ service('api_platform.validator.metadata.resource.metadata_collection_factory.parameter.inner'), service('api_platform.filter_locator'), diff --git a/tests/Fixtures/TestBundle/Entity/Issue7655/Author.php b/tests/Fixtures/TestBundle/Entity/Issue7655/Author.php new file mode 100644 index 00000000000..bd27eba3025 --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/Issue7655/Author.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue7655; + +use Doctrine\ORM\Mapping as ORM; + +/** + * Author entity for issue #7655, related to the Book resource declared in YAML. + * + * @see https://github.com/api-platform/core/issues/7655 + */ +#[ORM\Entity] +#[ORM\Table(name: 'issue_7655_author')] +class Author +{ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column] + private ?int $id = null; + + #[ORM\Column(length: 50)] + private string $name = ''; + + public function getId(): ?int + { + return $this->id; + } + + public function getName(): string + { + return $this->name; + } + + public function setName(string $name): self + { + $this->name = $name; + + return $this; + } +} diff --git a/tests/Fixtures/TestBundle/Entity/Issue7655/Book.php b/tests/Fixtures/TestBundle/Entity/Issue7655/Book.php new file mode 100644 index 00000000000..e0ea99584bd --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/Issue7655/Book.php @@ -0,0 +1,69 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue7655; + +use Doctrine\ORM\Mapping as ORM; + +/** + * Book entity for issue #7655. + * + * Its resource and query parameters are declared in YAML (see api_resources_orm/resources.yaml), not with attributes. + * + * @see https://github.com/api-platform/core/issues/7655 + */ +#[ORM\Entity] +#[ORM\Table(name: 'issue_7655_book')] +class Book +{ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column] + private ?int $id = null; + + #[ORM\Column(length: 50)] + private string $title = ''; + + #[ORM\ManyToOne(targetEntity: Author::class)] + #[ORM\JoinColumn(nullable: false)] + private ?Author $author = null; + + public function getId(): ?int + { + return $this->id; + } + + public function getTitle(): string + { + return $this->title; + } + + public function setTitle(string $title): self + { + $this->title = $title; + + return $this; + } + + public function getAuthor(): ?Author + { + return $this->author; + } + + public function setAuthor(?Author $author): self + { + $this->author = $author; + + return $this; + } +} diff --git a/tests/Fixtures/TestBundle/Resources/config/api_resources_orm/resources.yaml b/tests/Fixtures/TestBundle/Resources/config/api_resources_orm/resources.yaml index 7c69c2b79b4..b2138fd19b1 100644 --- a/tests/Fixtures/TestBundle/Resources/config/api_resources_orm/resources.yaml +++ b/tests/Fixtures/TestBundle/Resources/config/api_resources_orm/resources.yaml @@ -24,6 +24,18 @@ resources: toProperty: author id: [ApiPlatform\Tests\Fixtures\TestBundle\Entity\Program, id] + ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue7655\Book: + shortName: Issue7655Book + operations: + ApiPlatform\Metadata\GetCollection: + uriTemplate: /issue7655_books + parameters: + title: + filter: app_issue7655_exact_filter + authorName: + property: author.name + filter: app_issue7655_exact_filter + ApiPlatform\Tests\Fixtures\TestBundle\Entity\SingleFileConfigDummy: shortName: single_file_config description: File configured resource diff --git a/tests/Fixtures/app/config/config_doctrine.yml b/tests/Fixtures/app/config/config_doctrine.yml index 1a80b605985..fca22b7da7b 100644 --- a/tests/Fixtures/app/config/config_doctrine.yml +++ b/tests/Fixtures/app/config/config_doctrine.yml @@ -157,3 +157,7 @@ services: ApiPlatform\Tests\Fixtures\TestBundle\Filter\QueryParameterFilter: tags: [ 'api_platform.filter' ] + + app_issue7655_exact_filter: + class: 'ApiPlatform\Doctrine\Orm\Filter\ExactFilter' + tags: [ 'api_platform.filter' ] diff --git a/tests/Functional/Parameters/Issue7655FileDeclaredParameterTest.php b/tests/Functional/Parameters/Issue7655FileDeclaredParameterTest.php new file mode 100644 index 00000000000..2461dafc726 --- /dev/null +++ b/tests/Functional/Parameters/Issue7655FileDeclaredParameterTest.php @@ -0,0 +1,99 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Functional\Parameters; + +use ApiPlatform\Test\ApiTestCase; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue7655\Author; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue7655\Book; +use ApiPlatform\Tests\RecreateSchemaTrait; +use ApiPlatform\Tests\SetupClassResourcesTrait; + +/** + * Parameters declared in resource configuration files (YAML here) must get the same metadata as + * parameters declared with attributes: property defaults, filter schema and OpenAPI parameters, + * and Doctrine nested property information. + * + * @see https://github.com/api-platform/core/issues/7655 + * + * @group issue-7655 + */ +final class Issue7655FileDeclaredParameterTest extends ApiTestCase +{ + use RecreateSchemaTrait; + use SetupClassResourcesTrait; + + protected static ?bool $alwaysBootKernel = false; + + /** + * @return class-string[] + */ + public static function getResources(): array + { + return [Book::class]; + } + + protected function setUp(): void + { + if ($this->isMongoDB()) { + $this->markTestSkipped('The Book resource is declared in the ORM resource configuration only.'); + } + + $this->recreateSchema([Book::class, Author::class]); + $this->loadFixtures(); + } + + public function testParameterPropertyDefaultsToTheParameterKey(): void + { + $response = self::createClient()->request('GET', '/issue7655_books?title=Dune'); + $this->assertResponseIsSuccessful(); + + $titles = array_map(static fn (array $book): string => $book['title'], $response->toArray()['hydra:member']); + $this->assertSame(['Dune'], $titles); + } + + public function testFilterOnNestedProperty(): void + { + $response = self::createClient()->request('GET', '/issue7655_books?authorName=Ursula'); + $this->assertResponseIsSuccessful(); + + $titles = array_map(static fn (array $book): string => $book['title'], $response->toArray()['hydra:member']); + $this->assertSame(['The Dispossessed'], $titles); + } + + public function testOpenApiDocumentsTheFilterParameters(): void + { + $response = self::createClient()->request('GET', '/docs', [ + 'headers' => ['Accept' => 'application/vnd.openapi+json'], + ]); + $this->assertResponseIsSuccessful(); + + $parameterNames = array_column($response->toArray()['paths']['/issue7655_books']['get']['parameters'], 'name'); + $this->assertContains('authorName', $parameterNames); + $this->assertContains('authorName[]', $parameterNames); + } + + private function loadFixtures(): void + { + $manager = $this->getManager(); + + $frank = (new Author())->setName('Frank'); + $ursula = (new Author())->setName('Ursula'); + $manager->persist($frank); + $manager->persist($ursula); + + $manager->persist((new Book())->setTitle('Dune')->setAuthor($frank)); + $manager->persist((new Book())->setTitle('The Dispossessed')->setAuthor($ursula)); + $manager->flush(); + } +}