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(); + } +}