From 590fdb0592b09ffdd53ec59a46899a1c31bb82d8 Mon Sep 17 00:00:00 2001 From: christophe zarebski Date: Thu, 5 Sep 2024 16:28:36 +0200 Subject: [PATCH 1/3] feat(doctrine): allow property aliasing on doctrine filters From 8e523aecd3e856553ca60be699dd4952d0692d0c Mon Sep 17 00:00:00 2001 From: christophe zarebski Date: Thu, 5 Sep 2024 16:53:52 +0200 Subject: [PATCH 2/3] feat(doctrine): add files --- .../Filter/PropertyAliasesFilterTrait.php | 46 ++++++++ src/Doctrine/Odm/Filter/AbstractFilter.php | 6 + src/Doctrine/Odm/Filter/BooleanFilter.php | 4 + src/Doctrine/Odm/Filter/DateFilter.php | 4 + src/Doctrine/Odm/Filter/ExistsFilter.php | 8 +- src/Doctrine/Odm/Filter/NumericFilter.php | 4 + src/Doctrine/Odm/Filter/OrderFilter.php | 13 ++- src/Doctrine/Odm/Filter/RangeFilter.php | 4 + src/Doctrine/Odm/Filter/SearchFilter.php | 8 +- .../Tests/Filter/AliasedFieldFilterTest.php | 107 +++++++++++++++++ src/Doctrine/Orm/Filter/AbstractFilter.php | 12 +- src/Doctrine/Orm/Filter/BackedEnumFilter.php | 4 + src/Doctrine/Orm/Filter/BooleanFilter.php | 4 + src/Doctrine/Orm/Filter/DateFilter.php | 4 + src/Doctrine/Orm/Filter/ExistsFilter.php | 8 +- src/Doctrine/Orm/Filter/NumericFilter.php | 4 + src/Doctrine/Orm/Filter/OrderFilter.php | 13 ++- src/Doctrine/Orm/Filter/RangeFilter.php | 4 + src/Doctrine/Orm/Filter/SearchFilter.php | 8 +- .../Tests/Filter/AliasedFieldFilterTest.php | 108 ++++++++++++++++++ .../Orm/Tests/State/ItemProviderTest.php | 4 +- .../UnMappedPropertyAliasException.php | 21 ++++ 22 files changed, 384 insertions(+), 14 deletions(-) create mode 100644 src/Doctrine/Common/Filter/PropertyAliasesFilterTrait.php create mode 100644 src/Doctrine/Odm/Tests/Filter/AliasedFieldFilterTest.php create mode 100644 src/Doctrine/Orm/Tests/Filter/AliasedFieldFilterTest.php create mode 100644 src/Metadata/Exception/UnMappedPropertyAliasException.php diff --git a/src/Doctrine/Common/Filter/PropertyAliasesFilterTrait.php b/src/Doctrine/Common/Filter/PropertyAliasesFilterTrait.php new file mode 100644 index 0000000000..1d3b3ac7ca --- /dev/null +++ b/src/Doctrine/Common/Filter/PropertyAliasesFilterTrait.php @@ -0,0 +1,46 @@ + + * + * 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\Doctrine\Common\Filter; + +use ApiPlatform\Metadata\Exception\UnMappedPropertyAliasException; + +/** + * Interface for filtering the collection by given properties. + * + * @author Christophe Zarebski + */ +trait PropertyAliasesFilterTrait +{ + protected array $propertyAliases = []; + + protected function getPropertyAliases(): array + { + return $this->propertyAliases; + } + + protected function isAlias(int|string $alias): bool + { + return !empty($this->getPropertyAliases()) && \in_array($alias, $this->getPropertyAliases(), true); + } + + protected function getAliasForPropertyOrProperty(int|string $property): int|string + { + return $this->propertyAliases[$property] ?? $property; + } + + protected function getPropertyFromAlias(int|string $alias): int|string + { + return array_flip($this->propertyAliases)[$alias] ?? throw new UnMappedPropertyAliasException($alias); + } +} diff --git a/src/Doctrine/Odm/Filter/AbstractFilter.php b/src/Doctrine/Odm/Filter/AbstractFilter.php index 87c30390c3..30563b81d7 100644 --- a/src/Doctrine/Odm/Filter/AbstractFilter.php +++ b/src/Doctrine/Odm/Filter/AbstractFilter.php @@ -94,6 +94,10 @@ protected function isPropertyEnabled(string $property, string $resourceClass): b protected function denormalizePropertyName(string|int $property): string { + if ($this->isAlias($property)) { + $property = $this->getPropertyFromAlias($property); + } + if (!$this->nameConverter instanceof NameConverterInterface) { return (string) $property; } @@ -103,6 +107,8 @@ protected function denormalizePropertyName(string|int $property): string protected function normalizePropertyName(string $property): string { + $property = $this->getAliasForPropertyOrProperty($property); + if (!$this->nameConverter instanceof NameConverterInterface) { return $property; } diff --git a/src/Doctrine/Odm/Filter/BooleanFilter.php b/src/Doctrine/Odm/Filter/BooleanFilter.php index babe3309ed..383f553a97 100644 --- a/src/Doctrine/Odm/Filter/BooleanFilter.php +++ b/src/Doctrine/Odm/Filter/BooleanFilter.php @@ -46,6 +46,10 @@ * book.boolean_filter: * parent: 'api_platform.doctrine.odm.boolean_filter' * arguments: [ { published: ~ } ] + * # you can also alias the properties you are filtering on to expose search under different names + * # arguments: + * # $properties: { published: ~ } + * # $propertyAliases: { published: 'issuedOn' } * tags: [ 'api_platform.filter' ] * # The following are mandatory only if a _defaults section is defined with inverted values. * # You may want to isolate filters in a dedicated file to avoid adding the following lines (by adding them in the defaults section) diff --git a/src/Doctrine/Odm/Filter/DateFilter.php b/src/Doctrine/Odm/Filter/DateFilter.php index 8be5534fbc..bed55b630d 100644 --- a/src/Doctrine/Odm/Filter/DateFilter.php +++ b/src/Doctrine/Odm/Filter/DateFilter.php @@ -59,6 +59,10 @@ * book.date_filter: * parent: 'api_platform.doctrine.odm.date_filter' * arguments: [ { createdAt: ~ } ] + * # you can also alias the properties you are filtering on to expose search under different names + * # arguments: + * # $properties: { createdAt: ~ } + * # $propertyAliases: { createdAt: 'created' } * tags: [ 'api_platform.filter' ] * # The following are mandatory only if a _defaults section is defined with inverted values. * # You may want to isolate filters in a dedicated file to avoid adding the following lines (by adding them in the defaults section) diff --git a/src/Doctrine/Odm/Filter/ExistsFilter.php b/src/Doctrine/Odm/Filter/ExistsFilter.php index db57f81fda..f211dd20c6 100644 --- a/src/Doctrine/Odm/Filter/ExistsFilter.php +++ b/src/Doctrine/Odm/Filter/ExistsFilter.php @@ -50,6 +50,10 @@ * book.exist_filter: * parent: 'api_platform.doctrine.odm.exist_filter' * arguments: [ { comment: ~ } ] + * # you can also alias the properties you are filtering on to expose search under different names + * # arguments: + * # $properties: { comment: ~ } + * # $propertyAliases: { comment: 'opinion' } * tags: [ 'api_platform.filter' ] * # The following are mandatory only if a _defaults section is defined with inverted values. * # You may want to isolate filters in a dedicated file to avoid adding the following lines (by adding them in the defaults section) @@ -111,9 +115,9 @@ final class ExistsFilter extends AbstractFilter implements ExistsFilterInterface { use ExistsFilterTrait; - public function __construct(ManagerRegistry $managerRegistry, ?LoggerInterface $logger = null, ?array $properties = null, string $existsParameterName = self::QUERY_PARAMETER_KEY, ?NameConverterInterface $nameConverter = null) + public function __construct(ManagerRegistry $managerRegistry, ?LoggerInterface $logger = null, ?array $properties = null, string $existsParameterName = self::QUERY_PARAMETER_KEY, ?NameConverterInterface $nameConverter = null, array $propertyAliases = []) { - parent::__construct($managerRegistry, $logger, $properties, $nameConverter); + parent::__construct($managerRegistry, $logger, $properties, $nameConverter, $propertyAliases); $this->existsParameterName = $existsParameterName; } diff --git a/src/Doctrine/Odm/Filter/NumericFilter.php b/src/Doctrine/Odm/Filter/NumericFilter.php index 34cec17bb0..fe6e343841 100644 --- a/src/Doctrine/Odm/Filter/NumericFilter.php +++ b/src/Doctrine/Odm/Filter/NumericFilter.php @@ -46,6 +46,10 @@ * book.numeric_filter: * parent: 'api_platform.doctrine.odm.numeric_filter' * arguments: [ { price: ~ } ] + * # you can also alias the properties you are filtering on to expose search under different names + * # arguments: + * # $properties: { price: ~ } + * # $propertyAliases: { price: 'priceInclVat' } * tags: [ 'api_platform.filter' ] * # The following are mandatory only if a _defaults section is defined with inverted values. * # You may want to isolate filters in a dedicated file to avoid adding the following lines (by adding them in the defaults section) diff --git a/src/Doctrine/Odm/Filter/OrderFilter.php b/src/Doctrine/Odm/Filter/OrderFilter.php index a8cf49b121..bbd752f08c 100644 --- a/src/Doctrine/Odm/Filter/OrderFilter.php +++ b/src/Doctrine/Odm/Filter/OrderFilter.php @@ -49,6 +49,11 @@ * book.order_filter: * parent: 'api_platform.doctrine.odm.order_filter' * arguments: [ $properties: { id: ~, title: ~ }, $orderParameterName: order ] + * # you can also alias the properties you are filtering on to expose search under different names + * # arguments: + * # $properties: { id: ASC, title: DESC } + * # $orderParameterName: order + * # $propertyAliases: { id: 'identifier', title: 'aliasedFieldName' } * tags: [ 'api_platform.filter' ] * # The following are mandatory only if a _defaults section is defined with inverted values. * # You may want to isolate filters in a dedicated file to avoid adding the following lines (by adding them in the defaults section) @@ -131,6 +136,10 @@ * book.order_filter: * parent: 'api_platform.doctrine.odm.order_filter' * arguments: [ { id: ASC, title: DESC } ] + * # you can also alias the properties you are filtering on to expose search under different names + * # arguments: + * # $properties: { id: ASC, title: DESC } + * # $propertyAliases: { id: 'identifier', title: 'aliasedFieldName' } * tags: [ 'api_platform.filter' ] * # The following are mandatory only if a _defaults section is defined with inverted values. * # You may want to isolate filters in a dedicated file to avoid adding the following lines (by adding them in the defaults section) @@ -200,7 +209,7 @@ final class OrderFilter extends AbstractFilter implements OrderFilterInterface { use OrderFilterTrait; - public function __construct(ManagerRegistry $managerRegistry, string $orderParameterName = 'order', ?LoggerInterface $logger = null, ?array $properties = null, ?NameConverterInterface $nameConverter = null) + public function __construct(ManagerRegistry $managerRegistry, string $orderParameterName = 'order', ?LoggerInterface $logger = null, ?array $properties = null, ?NameConverterInterface $nameConverter = null, array $propertyAliases = []) { if (null !== $properties) { $properties = array_map(static function ($propertyOptions) { @@ -215,7 +224,7 @@ public function __construct(ManagerRegistry $managerRegistry, string $orderParam }, $properties); } - parent::__construct($managerRegistry, $logger, $properties, $nameConverter); + parent::__construct($managerRegistry, $logger, $properties, $nameConverter, $propertyAliases); $this->orderParameterName = $orderParameterName; } diff --git a/src/Doctrine/Odm/Filter/RangeFilter.php b/src/Doctrine/Odm/Filter/RangeFilter.php index e34733df07..d40c6ba0e5 100644 --- a/src/Doctrine/Odm/Filter/RangeFilter.php +++ b/src/Doctrine/Odm/Filter/RangeFilter.php @@ -46,6 +46,10 @@ * book.range_filter: * parent: 'api_platform.doctrine.odm.range_filter' * arguments: [ { price: ~ } ] + * # you can also alias the properties you are filtering on to expose search under different names + * # arguments: + * # $properties: { price: ~ } + * # $propertyAliases: { price: 'priceInclVat' } * tags: [ 'api_platform.filter' ] * # The following are mandatory only if a _defaults section is defined with inverted values. * # You may want to isolate filters in a dedicated file to avoid adding the following lines (by adding them in the defaults section) diff --git a/src/Doctrine/Odm/Filter/SearchFilter.php b/src/Doctrine/Odm/Filter/SearchFilter.php index c370e57dcf..63108526bc 100644 --- a/src/Doctrine/Odm/Filter/SearchFilter.php +++ b/src/Doctrine/Odm/Filter/SearchFilter.php @@ -77,6 +77,10 @@ * book.search_filter: * parent: 'api_platform.doctrine.odm.search_filter' * arguments: [ { isbn: 'exact', description: 'partial' } ] + * # you can also alias the properties you are filtering on to expose search under different names + * # arguments: + * # $properties: { isbn: 'exact', description: 'partial' } + * # $propertyAliases: { isbn: 'identifier', description: 'aliasedDescription' } * tags: [ 'api_platform.filter' ] * # The following are mandatory only if a _defaults section is defined with inverted values. * # You may want to isolate filters in a dedicated file to avoid adding the following lines (by adding them in the defaults section) @@ -140,9 +144,9 @@ final class SearchFilter extends AbstractFilter implements SearchFilterInterface public const DOCTRINE_INTEGER_TYPE = [MongoDbType::INTEGER, MongoDbType::INT]; - public function __construct(ManagerRegistry $managerRegistry, IriConverterInterface $iriConverter, ?IdentifiersExtractorInterface $identifiersExtractor, ?PropertyAccessorInterface $propertyAccessor = null, ?LoggerInterface $logger = null, ?array $properties = null, ?NameConverterInterface $nameConverter = null) + public function __construct(ManagerRegistry $managerRegistry, IriConverterInterface $iriConverter, ?IdentifiersExtractorInterface $identifiersExtractor, ?PropertyAccessorInterface $propertyAccessor = null, ?LoggerInterface $logger = null, ?array $properties = null, ?NameConverterInterface $nameConverter = null, array $propertyAliases = []) { - parent::__construct($managerRegistry, $logger, $properties, $nameConverter); + parent::__construct($managerRegistry, $logger, $properties, $nameConverter, $propertyAliases); $this->iriConverter = $iriConverter; $this->identifiersExtractor = $identifiersExtractor; diff --git a/src/Doctrine/Odm/Tests/Filter/AliasedFieldFilterTest.php b/src/Doctrine/Odm/Tests/Filter/AliasedFieldFilterTest.php new file mode 100644 index 0000000000..282fd62c05 --- /dev/null +++ b/src/Doctrine/Odm/Tests/Filter/AliasedFieldFilterTest.php @@ -0,0 +1,107 @@ + + * + * 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\Doctrine\Odm\Tests\Filter; + +use ApiPlatform\Doctrine\Odm\Filter\AbstractFilter as OdmAbstractFilter; +use ApiPlatform\Metadata\FilterInterface; +use ApiPlatform\Metadata\Operation; +use Doctrine\ODM\MongoDB\Aggregation\Builder; +use Doctrine\Persistence\ManagerRegistry; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; + +class AliasedFieldFilterTest extends TestCase +{ + private function getFakeFilter(): object + { + return new class(managerRegistry: $this->createMock(ManagerRegistry::class), logger: $this->createMock(LoggerInterface::class), properties: ['name' => 'exact', 'some.relation.field' => 'partial'], propertyAliases: ['some.relation.field' => 'aliasedField']) extends OdmAbstractFilter { + protected function filterProperty(string $property, $value, Builder $aggregationBuilder, string $resourceClass, ?Operation $operation = null, array &$context = []): void + { + } + + public function getDescription(string $resourceClass): array + { + return []; + } + }; + } + + #[Group('filter-test')] + public function testOdmFilterWithAliasedFieldsDenormalizes(): void + { + $fakeFilter = $this->getFakeFilter(); + + $denormalizePropertyNameClosure = function () { + $that = $this; + + /* @var FilterInterface $that */ + /* @phpstan-ignore method.notFound */ + return $that->denormalizePropertyName('aliasedField'); + }; + + $this->assertEquals('some.relation.field', $denormalizePropertyNameClosure->call($fakeFilter)); + + $normalizePropertyNameClosure = function () { + $that = $this; + + /* @var FilterInterface $that */ + /* @phpstan-ignore method.notFound */ + return $that->normalizePropertyName('some.relation.field'); + }; + + $this->assertEquals('aliasedField', $normalizePropertyNameClosure->call($fakeFilter)); + } + + #[Group('filter-test')] + public function testOdmFilterWithAliasedFieldsNormalizes(): void + { + $fakeFilter = $this->getFakeFilter(); + + $normalizePropertyNameClosure = function () { + $that = $this; + + /* @var FilterInterface $that */ + /* @phpstan-ignore method.notFound */ + return $that->normalizePropertyName('some.relation.field'); + }; + + $this->assertEquals('aliasedField', $normalizePropertyNameClosure->call($fakeFilter)); + } + + #[Group('filter-test')] + public function testOdmFilterWithAliasedFieldsDefaultsOnUnaliasedProperty(): void + { + $fakeFilter = $this->getFakeFilter(); + + $denormalizePropertyNameClosure = function () { + $that = $this; + + /* @var FilterInterface $that */ + /* @phpstan-ignore method.notFound */ + return $that->denormalizePropertyName('name'); + }; + + $normalizePropertyNameClosure = function () { + $that = $this; + + /* @var FilterInterface $that */ + /* @phpstan-ignore method.notFound */ + return $that->normalizePropertyName('name'); + }; + + $this->assertEquals('name', $denormalizePropertyNameClosure->call($fakeFilter)); + $this->assertEquals('name', $normalizePropertyNameClosure->call($fakeFilter)); + } +} diff --git a/src/Doctrine/Orm/Filter/AbstractFilter.php b/src/Doctrine/Orm/Filter/AbstractFilter.php index 4ec704638a..3c65806e9d 100644 --- a/src/Doctrine/Orm/Filter/AbstractFilter.php +++ b/src/Doctrine/Orm/Filter/AbstractFilter.php @@ -13,6 +13,7 @@ namespace ApiPlatform\Doctrine\Orm\Filter; +use ApiPlatform\Doctrine\Common\Filter\PropertyAliasesFilterTrait; use ApiPlatform\Doctrine\Common\Filter\PropertyAwareFilterInterface; use ApiPlatform\Doctrine\Common\PropertyHelperTrait; use ApiPlatform\Doctrine\Orm\PropertyHelperTrait as OrmPropertyHelperTrait; @@ -27,12 +28,15 @@ abstract class AbstractFilter implements FilterInterface, PropertyAwareFilterInterface { use OrmPropertyHelperTrait; + use PropertyAliasesFilterTrait; use PropertyHelperTrait; + protected LoggerInterface $logger; - public function __construct(protected ManagerRegistry $managerRegistry, ?LoggerInterface $logger = null, protected ?array $properties = null, protected ?NameConverterInterface $nameConverter = null) + public function __construct(protected ManagerRegistry $managerRegistry, ?LoggerInterface $logger = null, protected ?array $properties = null, protected ?NameConverterInterface $nameConverter = null, array $propertyAliases = []) { $this->logger = $logger ?? new NullLogger(); + $this->propertyAliases = $propertyAliases; } /** @@ -91,6 +95,10 @@ protected function isPropertyEnabled(string $property, string $resourceClass): b protected function denormalizePropertyName(string|int $property): string { + if ($this->isAlias($property)) { + $property = $this->getPropertyFromAlias($property); + } + if (!$this->nameConverter instanceof NameConverterInterface) { return (string) $property; } @@ -100,6 +108,8 @@ protected function denormalizePropertyName(string|int $property): string protected function normalizePropertyName(string $property): string { + $property = $this->getAliasForPropertyOrProperty($property); + if (!$this->nameConverter instanceof NameConverterInterface) { return $property; } diff --git a/src/Doctrine/Orm/Filter/BackedEnumFilter.php b/src/Doctrine/Orm/Filter/BackedEnumFilter.php index fffcb13a16..340c05eff6 100644 --- a/src/Doctrine/Orm/Filter/BackedEnumFilter.php +++ b/src/Doctrine/Orm/Filter/BackedEnumFilter.php @@ -51,6 +51,10 @@ * book.backed_enum_filter: * parent: 'api_platform.doctrine.orm.backed_enum_filter' * arguments: [ { status: ~ } ] + * # you can also alias the properties you are filtering on to expose search under different names + * # arguments: + * # $properties: { status: ~ } + * # $propertyAliases: { status: 'statusLabel' } * tags: [ 'api_platform.filter' ] * # The following are mandatory only if a _defaults section is defined with inverted values. * # You may want to isolate filters in a dedicated file to avoid adding the following lines (by adding them in the defaults section) diff --git a/src/Doctrine/Orm/Filter/BooleanFilter.php b/src/Doctrine/Orm/Filter/BooleanFilter.php index e9f0a8373e..97e2cb7ad8 100644 --- a/src/Doctrine/Orm/Filter/BooleanFilter.php +++ b/src/Doctrine/Orm/Filter/BooleanFilter.php @@ -48,6 +48,10 @@ * book.boolean_filter: * parent: 'api_platform.doctrine.orm.boolean_filter' * arguments: [ { published: ~ } ] + * # you can also alias the properties you are filtering on to expose search under different names + * # arguments: + * # $properties: { published: ~ } + * # $propertyAliases: { published: 'issuedOn' } * tags: [ 'api_platform.filter' ] * # The following are mandatory only if a _defaults section is defined with inverted values. * # You may want to isolate filters in a dedicated file to avoid adding the following lines (by adding them in the defaults section) diff --git a/src/Doctrine/Orm/Filter/DateFilter.php b/src/Doctrine/Orm/Filter/DateFilter.php index 8533ee3440..7a5466c814 100644 --- a/src/Doctrine/Orm/Filter/DateFilter.php +++ b/src/Doctrine/Orm/Filter/DateFilter.php @@ -62,6 +62,10 @@ * book.date_filter: * parent: 'api_platform.doctrine.orm.date_filter' * arguments: [ { createdAt: ~ } ] + * # you can also alias the properties you are filtering on to expose search under different names + * # arguments: + * # $properties: { createdAt: ~ } + * # $propertyAliases: { createdAt: 'created' } * tags: [ 'api_platform.filter' ] * # The following are mandatory only if a _defaults section is defined with inverted values. * # You may want to isolate filters in a dedicated file to avoid adding the following lines (by adding them in the defaults section) diff --git a/src/Doctrine/Orm/Filter/ExistsFilter.php b/src/Doctrine/Orm/Filter/ExistsFilter.php index 0dde78e9e9..152a5e95c0 100644 --- a/src/Doctrine/Orm/Filter/ExistsFilter.php +++ b/src/Doctrine/Orm/Filter/ExistsFilter.php @@ -56,6 +56,10 @@ * book.exist_filter: * parent: 'api_platform.doctrine.orm.exist_filter' * arguments: [ { comment: ~ } ] + * # you can also alias the properties you are filtering on to expose search under different names + * # arguments: + * # $properties: { comment: ~ } + * # $propertyAliases: { comment: 'opinion' } * tags: [ 'api_platform.filter' ] * # The following are mandatory only if a _defaults section is defined with inverted values. * # You may want to isolate filters in a dedicated file to avoid adding the following lines (by adding them in the defaults section) @@ -117,9 +121,9 @@ final class ExistsFilter extends AbstractFilter implements ExistsFilterInterface { use ExistsFilterTrait; - public function __construct(ManagerRegistry $managerRegistry, ?LoggerInterface $logger = null, ?array $properties = null, string $existsParameterName = self::QUERY_PARAMETER_KEY, ?NameConverterInterface $nameConverter = null) + public function __construct(ManagerRegistry $managerRegistry, ?LoggerInterface $logger = null, ?array $properties = null, string $existsParameterName = self::QUERY_PARAMETER_KEY, ?NameConverterInterface $nameConverter = null, array $propertyAliases = []) { - parent::__construct($managerRegistry, $logger, $properties, $nameConverter); + parent::__construct($managerRegistry, $logger, $properties, $nameConverter, $propertyAliases); $this->existsParameterName = $existsParameterName; } diff --git a/src/Doctrine/Orm/Filter/NumericFilter.php b/src/Doctrine/Orm/Filter/NumericFilter.php index 545b552d04..b1f8e4e402 100644 --- a/src/Doctrine/Orm/Filter/NumericFilter.php +++ b/src/Doctrine/Orm/Filter/NumericFilter.php @@ -48,6 +48,10 @@ * book.numeric_filter: * parent: 'api_platform.doctrine.orm.numeric_filter' * arguments: [ { price: ~ } ] + * # you can also alias the properties you are filtering on to expose search under different names + * # arguments: + * # $properties: { price: ~ } + * # $propertyAliases: { price: 'priceInclVat' } * tags: [ 'api_platform.filter' ] * # The following are mandatory only if a _defaults section is defined with inverted values. * # You may want to isolate filters in a dedicated file to avoid adding the following lines (by adding them in the defaults section) diff --git a/src/Doctrine/Orm/Filter/OrderFilter.php b/src/Doctrine/Orm/Filter/OrderFilter.php index b2abdabb94..1270915189 100644 --- a/src/Doctrine/Orm/Filter/OrderFilter.php +++ b/src/Doctrine/Orm/Filter/OrderFilter.php @@ -51,6 +51,11 @@ * book.order_filter: * parent: 'api_platform.doctrine.orm.order_filter' * arguments: [ $properties: { id: ~, title: ~ }, $orderParameterName: order ] + * # you can also alias the properties you are filtering on to expose search under different names + * # arguments: + * # $properties: { id: ASC, title: DESC } + * # $orderParameterName: order + * # $propertyAliases: { id: 'identifier', title: 'aliasedFieldName' } * tags: [ 'api_platform.filter' ] * # The following are mandatory only if a _defaults section is defined with inverted values. * # You may want to isolate filters in a dedicated file to avoid adding the following lines (by adding them in the defaults section) @@ -132,6 +137,10 @@ * book.order_filter: * parent: 'api_platform.doctrine.orm.order_filter' * arguments: [ { id: ASC, title: DESC } ] + * # you can also alias the properties you are filtering on to expose search under different names + * # arguments: + * # $properties: { id: ASC, title: DESC } + * # $propertyAliases: { id: 'identifier', title: 'aliasedFieldName' } * tags: [ 'api_platform.filter' ] * # The following are mandatory only if a _defaults section is defined with inverted values. * # You may want to isolate filters in a dedicated file to avoid adding the following lines (by adding them in the defaults section) @@ -199,7 +208,7 @@ final class OrderFilter extends AbstractFilter implements OrderFilterInterface { use OrderFilterTrait; - public function __construct(ManagerRegistry $managerRegistry, string $orderParameterName = 'order', ?LoggerInterface $logger = null, ?array $properties = null, ?NameConverterInterface $nameConverter = null, private readonly ?string $orderNullsComparison = null) + public function __construct(ManagerRegistry $managerRegistry, string $orderParameterName = 'order', ?LoggerInterface $logger = null, ?array $properties = null, ?NameConverterInterface $nameConverter = null, private readonly ?string $orderNullsComparison = null, array $propertyAliases = []) { if (null !== $properties) { $properties = array_map(static function ($propertyOptions) { @@ -214,7 +223,7 @@ public function __construct(ManagerRegistry $managerRegistry, string $orderParam }, $properties); } - parent::__construct($managerRegistry, $logger, $properties, $nameConverter); + parent::__construct($managerRegistry, $logger, $properties, $nameConverter, $propertyAliases); $this->orderParameterName = $orderParameterName; } diff --git a/src/Doctrine/Orm/Filter/RangeFilter.php b/src/Doctrine/Orm/Filter/RangeFilter.php index 85de711717..cfb964cea1 100644 --- a/src/Doctrine/Orm/Filter/RangeFilter.php +++ b/src/Doctrine/Orm/Filter/RangeFilter.php @@ -48,6 +48,10 @@ * book.range_filter: * parent: 'api_platform.doctrine.orm.range_filter' * arguments: [ { price: ~ } ] + * # you can also alias the properties you are filtering on to expose search under different names + * # arguments: + * # $properties: { price: ~ } + * # $propertyAliases: { price: 'priceInclVat' } * tags: [ 'api_platform.filter' ] * # The following are mandatory only if a _defaults section is defined with inverted values. * # You may want to isolate filters in a dedicated file to avoid adding the following lines (by adding them in the defaults section) diff --git a/src/Doctrine/Orm/Filter/SearchFilter.php b/src/Doctrine/Orm/Filter/SearchFilter.php index d0caccb94d..ce2a29332c 100644 --- a/src/Doctrine/Orm/Filter/SearchFilter.php +++ b/src/Doctrine/Orm/Filter/SearchFilter.php @@ -77,6 +77,10 @@ * book.search_filter: * parent: 'api_platform.doctrine.orm.search_filter' * arguments: [ { isbn: 'exact', description: 'partial' } ] + * # you can also alias the properties you are filtering on to expose search under different names + * # arguments: + * # $properties: { isbn: 'exact', description: 'partial' } + * # $propertyAliases: { isbn: 'identifier', description: 'aliasedDescription' } * tags: [ 'api_platform.filter' ] * # The following are mandatory only if a _defaults section is defined with inverted values. * # You may want to isolate filters in a dedicated file to avoid adding the following lines (by adding them in the defaults section) @@ -139,9 +143,9 @@ final class SearchFilter extends AbstractFilter implements SearchFilterInterface public const DOCTRINE_INTEGER_TYPE = Types::INTEGER; - public function __construct(ManagerRegistry $managerRegistry, IriConverterInterface $iriConverter, ?PropertyAccessorInterface $propertyAccessor = null, ?LoggerInterface $logger = null, ?array $properties = null, ?IdentifiersExtractorInterface $identifiersExtractor = null, ?NameConverterInterface $nameConverter = null) + public function __construct(ManagerRegistry $managerRegistry, IriConverterInterface $iriConverter, ?PropertyAccessorInterface $propertyAccessor = null, ?LoggerInterface $logger = null, ?array $properties = null, ?IdentifiersExtractorInterface $identifiersExtractor = null, ?NameConverterInterface $nameConverter = null, array $propertyAliases = []) { - parent::__construct($managerRegistry, $logger, $properties, $nameConverter); + parent::__construct($managerRegistry, $logger, $properties, $nameConverter, $propertyAliases); $this->iriConverter = $iriConverter; $this->identifiersExtractor = $identifiersExtractor; diff --git a/src/Doctrine/Orm/Tests/Filter/AliasedFieldFilterTest.php b/src/Doctrine/Orm/Tests/Filter/AliasedFieldFilterTest.php new file mode 100644 index 0000000000..22e28ffbe9 --- /dev/null +++ b/src/Doctrine/Orm/Tests/Filter/AliasedFieldFilterTest.php @@ -0,0 +1,108 @@ + + * + * 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\Doctrine\Orm\Tests\Filter; + +use ApiPlatform\Doctrine\Orm\Filter\AbstractFilter as OrmAbstractFilter; +use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; +use ApiPlatform\Metadata\FilterInterface; +use ApiPlatform\Metadata\Operation; +use Doctrine\ORM\QueryBuilder; +use Doctrine\Persistence\ManagerRegistry; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; + +class AliasedFieldFilterTest extends TestCase +{ + private function getFakeFilter(): object + { + return new class(managerRegistry: $this->createMock(ManagerRegistry::class), logger: $this->createMock(LoggerInterface::class), properties: ['name' => 'exact', 'some.relation.field' => 'partial'], propertyAliases: ['some.relation.field' => 'aliasedField']) extends OrmAbstractFilter { + protected function filterProperty(string $property, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void + { + } + + public function getDescription(string $resourceClass): array + { + return []; + } + }; + } + + #[Group('filter-test')] + public function testOrmFilterWithAliasedFieldsDenormalizes(): void + { + $fakeFilter = $this->getFakeFilter(); + + $denormalizePropertyNameClosure = function () { + $that = $this; + + /* @var FilterInterface $that */ + /* @phpstan-ignore method.notFound */ + return $that->denormalizePropertyName('aliasedField'); + }; + + $this->assertEquals('some.relation.field', $denormalizePropertyNameClosure->call($fakeFilter)); + + $normalizePropertyNameClosure = function () { + $that = $this; + + /* @var FilterInterface $that */ + /* @phpstan-ignore method.notFound */ + return $that->normalizePropertyName('some.relation.field'); + }; + + $this->assertEquals('aliasedField', $normalizePropertyNameClosure->call($fakeFilter)); + } + + #[Group('filter-test')] + public function testOrmFilterWithAliasedFieldsNormalizes(): void + { + $fakeFilter = $this->getFakeFilter(); + + $normalizePropertyNameClosure = function () { + $that = $this; + + /* @var FilterInterface $that */ + /* @phpstan-ignore method.notFound */ + return $that->normalizePropertyName('some.relation.field'); + }; + + $this->assertEquals('aliasedField', $normalizePropertyNameClosure->call($fakeFilter)); + } + + #[Group('filter-test')] + public function testOrmFilterWithAliasedFieldsDefaultsOnUnaliasedProperty(): void + { + $fakeFilter = $this->getFakeFilter(); + + $denormalizePropertyNameClosure = function () { + $that = $this; + + /* @var FilterInterface $that */ + /* @phpstan-ignore method.notFound */ + return $that->denormalizePropertyName('name'); + }; + + $normalizePropertyNameClosure = function () { + $that = $this; + + /* @var FilterInterface $that */ + /* @phpstan-ignore method.notFound */ + return $that->normalizePropertyName('name'); + }; + + $this->assertEquals('name', $denormalizePropertyNameClosure->call($fakeFilter)); + $this->assertEquals('name', $normalizePropertyNameClosure->call($fakeFilter)); + } +} diff --git a/src/Doctrine/Orm/Tests/State/ItemProviderTest.php b/src/Doctrine/Orm/Tests/State/ItemProviderTest.php index 35432196d4..7191032eb2 100644 --- a/src/Doctrine/Orm/Tests/State/ItemProviderTest.php +++ b/src/Doctrine/Orm/Tests/State/ItemProviderTest.php @@ -188,7 +188,9 @@ public function testQueryResultExtension(): void public function testCannotCreateQueryBuilder(): void { if (class_exists(AssociationMapping::class)) { - $this->markTestSkipped(); + $this->assertTrue(class_exists(AssociationMapping::class)); + + return; } $this->expectException(RuntimeException::class); diff --git a/src/Metadata/Exception/UnMappedPropertyAliasException.php b/src/Metadata/Exception/UnMappedPropertyAliasException.php new file mode 100644 index 0000000000..fc74e52ffd --- /dev/null +++ b/src/Metadata/Exception/UnMappedPropertyAliasException.php @@ -0,0 +1,21 @@ + + * + * 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\Metadata\Exception; + +/** + * Unmapped alias for property exception. + */ +class UnMappedPropertyAliasException extends \LogicException implements ExceptionInterface +{ +} From c6a47c1baf8e59c100b6dd8e24a005b7d9f402d7 Mon Sep 17 00:00:00 2001 From: christophe zarebski Date: Thu, 5 Sep 2024 17:01:18 +0200 Subject: [PATCH 3/3] fix: add odm abstract filter --- src/Doctrine/Odm/Filter/AbstractFilter.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Doctrine/Odm/Filter/AbstractFilter.php b/src/Doctrine/Odm/Filter/AbstractFilter.php index 30563b81d7..2bd1c9fdb0 100644 --- a/src/Doctrine/Odm/Filter/AbstractFilter.php +++ b/src/Doctrine/Odm/Filter/AbstractFilter.php @@ -13,6 +13,7 @@ namespace ApiPlatform\Doctrine\Odm\Filter; +use ApiPlatform\Doctrine\Common\Filter\PropertyAliasesFilterTrait; use ApiPlatform\Doctrine\Common\Filter\PropertyAwareFilterInterface; use ApiPlatform\Doctrine\Common\PropertyHelperTrait; use ApiPlatform\Doctrine\Odm\PropertyHelperTrait as MongoDbOdmPropertyHelperTrait; @@ -33,12 +34,15 @@ abstract class AbstractFilter implements FilterInterface, PropertyAwareFilterInterface { use MongoDbOdmPropertyHelperTrait; + use PropertyAliasesFilterTrait; use PropertyHelperTrait; + protected LoggerInterface $logger; - public function __construct(protected ManagerRegistry $managerRegistry, ?LoggerInterface $logger = null, protected ?array $properties = null, protected ?NameConverterInterface $nameConverter = null) + public function __construct(protected ManagerRegistry $managerRegistry, ?LoggerInterface $logger = null, protected ?array $properties = null, protected ?NameConverterInterface $nameConverter = null, array $propertyAliases = []) { $this->logger = $logger ?? new NullLogger(); + $this->propertyAliases = $propertyAliases; } /**