diff --git a/src/Doctrine/Odm/NestedPropertyHelperTrait.php b/src/Doctrine/Odm/NestedPropertyHelperTrait.php index e713382bece..d3a92ecdf0b 100644 --- a/src/Doctrine/Odm/NestedPropertyHelperTrait.php +++ b/src/Doctrine/Odm/NestedPropertyHelperTrait.php @@ -39,7 +39,7 @@ protected function addNestedParameterLookups(string $property, Builder $aggregat $nestedInfo = ($extraProperties['nested_properties_info'] ?? []) ? reset($extraProperties['nested_properties_info']) : null; if (!$nestedInfo) { - return $property; + return $extraProperties['query_property'] ?? $property; } $odmSegments = $nestedInfo['odm_segments'] ?? []; diff --git a/src/Doctrine/Orm/NestedPropertyHelperTrait.php b/src/Doctrine/Orm/NestedPropertyHelperTrait.php index e93f17d46f8..f8f0e1e52e0 100644 --- a/src/Doctrine/Orm/NestedPropertyHelperTrait.php +++ b/src/Doctrine/Orm/NestedPropertyHelperTrait.php @@ -43,7 +43,7 @@ protected function addNestedParameterJoins( $nestedInfo = ($extraProperties['nested_properties_info'] ?? []) ? reset($extraProperties['nested_properties_info']) : null; if (!$nestedInfo) { - return [$alias, $property]; + return [$alias, $extraProperties['query_property'] ?? $property]; } $relationClasses = $nestedInfo['relation_classes'] ?? []; diff --git a/src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php index f55734196e5..3972b1e8978 100644 --- a/src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php +++ b/src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php @@ -405,7 +405,10 @@ private function setDefaults(string $key, Parameter $parameter, ?object $filter, // Skip name conversion if we already have nested property info $paramExtraProps = $parameter->getExtraProperties(); if (!isset($paramExtraProps['nested_properties_info'])) { - $parameter = $parameter->withProperty($this->nameConverter->normalize($property)); + // Keep the original (non name-converted) property to build the query while the public property matches the API naming convention. + $parameter = $parameter + ->withExtraProperties([...$paramExtraProps, 'query_property' => $property]) + ->withProperty($this->nameConverter->normalize($property)); } } diff --git a/tests/Fixtures/TestBundle/Document/ConvertedFilterParameter.php b/tests/Fixtures/TestBundle/Document/ConvertedFilterParameter.php new file mode 100644 index 00000000000..9a44bc34e0a --- /dev/null +++ b/tests/Fixtures/TestBundle/Document/ConvertedFilterParameter.php @@ -0,0 +1,57 @@ + + * + * 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\Document; + +use ApiPlatform\Doctrine\Odm\Filter\ExactFilter; +use ApiPlatform\Doctrine\Odm\Filter\SortFilter; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\QueryParameter; +use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; + +/** + * Tests that modern parameter filters (ExactFilter, SortFilter) work with + * QueryParameter on a multi-word property when a nameConverter is configured + * (issue #8380). + */ +#[ApiResource( + operations: [ + new GetCollection( + parameters: [ + 'nameConverted' => new QueryParameter( + filter: new ExactFilter(), + property: 'nameConverted', + ), + 'orderNameConverted' => new QueryParameter( + filter: new SortFilter(), + property: 'nameConverted', + ), + ], + ), + ], +)] +#[ODM\Document] +class ConvertedFilterParameter +{ + #[ODM\Id(strategy: 'INCREMENT', type: 'int')] + private ?int $id = null; + + #[ODM\Field(type: 'string')] + public string $nameConverted = ''; + + public function getId(): ?int + { + return $this->id; + } +} diff --git a/tests/Fixtures/TestBundle/Entity/ConvertedFilterParameter.php b/tests/Fixtures/TestBundle/Entity/ConvertedFilterParameter.php new file mode 100644 index 00000000000..302a2c63e10 --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/ConvertedFilterParameter.php @@ -0,0 +1,59 @@ + + * + * 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; + +use ApiPlatform\Doctrine\Orm\Filter\ExactFilter; +use ApiPlatform\Doctrine\Orm\Filter\SortFilter; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\QueryParameter; +use Doctrine\ORM\Mapping as ORM; + +/** + * Tests that modern parameter filters (ExactFilter, SortFilter) work with + * QueryParameter on a multi-word property when a nameConverter is configured + * (issue #8380). + */ +#[ApiResource( + operations: [ + new GetCollection( + parameters: [ + 'nameConverted' => new QueryParameter( + filter: new ExactFilter(), + property: 'nameConverted', + ), + 'orderNameConverted' => new QueryParameter( + filter: new SortFilter(), + property: 'nameConverted', + ), + ], + ), + ], +)] +#[ORM\Entity] +class ConvertedFilterParameter +{ + #[ORM\Column(type: 'integer', nullable: true)] + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'AUTO')] + private ?int $id = null; + + #[ORM\Column(type: 'string')] + public string $nameConverted = ''; + + public function getId(): ?int + { + return $this->id; + } +} diff --git a/tests/Functional/Parameters/NameConverterModernFilterTest.php b/tests/Functional/Parameters/NameConverterModernFilterTest.php new file mode 100644 index 00000000000..a2f3c939141 --- /dev/null +++ b/tests/Functional/Parameters/NameConverterModernFilterTest.php @@ -0,0 +1,84 @@ + + * + * 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\Symfony\Bundle\Test\ApiTestCase; +use ApiPlatform\Tests\Fixtures\TestBundle\Document\ConvertedFilterParameter as ConvertedFilterParameterDocument; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\ConvertedFilterParameter; +use ApiPlatform\Tests\RecreateSchemaTrait; +use ApiPlatform\Tests\SetupClassResourcesTrait; + +/** + * Tests that modern parameter filters (ExactFilter, SortFilter) work with + * QueryParameter on a multi-word property when a nameConverter is configured. + * + * @see https://github.com/api-platform/core/issues/8380 + */ +final class NameConverterModernFilterTest extends ApiTestCase +{ + use RecreateSchemaTrait; + use SetupClassResourcesTrait; + + protected static ?bool $alwaysBootKernel = false; + + /** + * @return class-string[] + */ + public static function getResources(): array + { + return [ConvertedFilterParameter::class]; + } + + protected function setUp(): void + { + $entityClass = $this->isMongoDB() ? ConvertedFilterParameterDocument::class : ConvertedFilterParameter::class; + + $this->recreateSchema([$entityClass]); + $this->loadFixtures($entityClass); + } + + public function testExactFilterWithNameConverter(): void + { + $response = self::createClient()->request('GET', '/converted_filter_parameters?nameConverted=bar'); + $this->assertResponseIsSuccessful(); + $members = $response->toArray()['hydra:member']; + $this->assertCount(1, $members); + $this->assertSame('bar', $members[0]['name_converted']); + } + + public function testSortFilterWithNameConverter(): void + { + $response = self::createClient()->request('GET', '/converted_filter_parameters?orderNameConverted=desc'); + $this->assertResponseIsSuccessful(); + $members = $response->toArray()['hydra:member']; + $names = array_map(static fn (array $m): string => $m['name_converted'], $members); + $this->assertSame(['foo', 'baz', 'bar'], $names); + } + + /** + * @param class-string $entityClass + */ + private function loadFixtures(string $entityClass): void + { + $manager = $this->getManager(); + + foreach (['bar', 'baz', 'foo'] as $name) { + $entity = new $entityClass(); + $entity->nameConverted = $name; + $manager->persist($entity); + } + + $manager->flush(); + } +}