From a13be92a0d76294151fca871969cf2d524176460 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Tue, 16 Sep 2025 15:24:05 +0200 Subject: [PATCH 1/2] feat(mongodb): make ParameterExtension context more generic --- phpstan.neon.dist | 7 + .../Odm/Extension/ParameterExtension.php | 15 +- .../Extension/ParameterExtensionTest.php | 182 ++++++++++++++++++ 3 files changed, 195 insertions(+), 9 deletions(-) create mode 100644 src/Doctrine/Odm/Tests/Extension/ParameterExtensionTest.php diff --git a/phpstan.neon.dist b/phpstan.neon.dist index dbdeeb7c511..ef1bbd402a6 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -113,6 +113,13 @@ parameters: # Allow extra assertions in tests: https://github.com/phpstan/phpstan-strict-rules/issues/130 - '#^Call to (static )?method PHPUnit\\Framework\\Assert::.* will always evaluate to true\.$#' + # Unsealed array shapes not supported + - + message: '#^Parameter &\$context by\-ref type of method ApiPlatform\\Doctrine\\Odm\\Extension\\ParameterExtension\:\:applyFilter\(\) expects array\, array(.*) given\.$#' + identifier: parameterByRef.type + count: 5 + path: src/Doctrine/Odm/Extension/ParameterExtension.php + # Level 6 - identifier: missingType.iterableValue diff --git a/src/Doctrine/Odm/Extension/ParameterExtension.php b/src/Doctrine/Odm/Extension/ParameterExtension.php index 585ea9ec059..5ed9d08fa7b 100644 --- a/src/Doctrine/Odm/Extension/ParameterExtension.php +++ b/src/Doctrine/Odm/Extension/ParameterExtension.php @@ -89,15 +89,12 @@ private function applyFilter(Builder $aggregationBuilder, ?string $resourceClass $filter->setProperties($properties ?? []); } - $filterContext = ['filters' => $values, 'parameter' => $parameter, 'match' => $context['match'] ?? null]; - $filter->apply($aggregationBuilder, $resourceClass, $operation, $filterContext); - // update by reference - if (isset($filterContext['mongodb_odm_sort_fields'])) { - $context['mongodb_odm_sort_fields'] = $filterContext['mongodb_odm_sort_fields']; - } - if (isset($filterContext['match'])) { - $context['match'] = $filterContext['match']; - } + $context['filters'] = $values; + $context['parameter'] = $parameter; + + $filter->apply($aggregationBuilder, $resourceClass, $operation, $context); + + unset($context['filters'], $context['parameter']); } if (isset($context['match'])) { diff --git a/src/Doctrine/Odm/Tests/Extension/ParameterExtensionTest.php b/src/Doctrine/Odm/Tests/Extension/ParameterExtensionTest.php new file mode 100644 index 00000000000..0188c1c0206 --- /dev/null +++ b/src/Doctrine/Odm/Tests/Extension/ParameterExtensionTest.php @@ -0,0 +1,182 @@ + + * + * 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\Extension; + +use ApiPlatform\Doctrine\Common\Filter\LoggerAwareInterface; +use ApiPlatform\Doctrine\Common\Filter\LoggerAwareTrait; +use ApiPlatform\Doctrine\Common\Filter\ManagerRegistryAwareInterface; +use ApiPlatform\Doctrine\Common\Filter\ManagerRegistryAwareTrait; +use ApiPlatform\Doctrine\Odm\Extension\ParameterExtension; +use ApiPlatform\Doctrine\Odm\Filter\FilterInterface; +use ApiPlatform\Metadata\BackwardCompatibleFilterDescriptionTrait; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\Parameter; +use ApiPlatform\Metadata\QueryParameter; +use Doctrine\Bundle\MongoDBBundle\ManagerRegistry; +use Doctrine\ODM\MongoDB\Aggregation\Builder; +use PHPUnit\Framework\Assert; +use PHPUnit\Framework\TestCase; +use Psr\Container\ContainerInterface; +use Psr\Log\LoggerInterface; + +class ParameterExtensionTest extends TestCase +{ + public function testApplyToCollectionWithNoParameters(): void + { + $aggregationBuilder = $this->createMock(Builder::class); + $operation = new GetCollection(); + $extension = new ParameterExtension($this->createNonCalledFilterLocator()); + + $context = []; + $extension->applyToCollection($aggregationBuilder, 'SomeClass', $operation, $context); + + $this->assertSame([], $context); + } + + public function testApplyToCollectionWithParameterAndFilter(): void + { + $filterLocator = $this->createMock(ContainerInterface::class); + $filterLocator->expects($this->once())->method('has') + ->with('filter_service_id') + ->willReturn(true); + $filterLocator->expects($this->once())->method('get') + ->with('filter_service_id') + ->willReturn($this->createFilterMock()); + + $aggregationBuilder = $this->createMock(Builder::class); + $operation = (new GetCollection()) + ->withParameters([ + (new QueryParameter( + key: 'param1', + filter: $this->createFilterMock(), + ))->setValue(1), + (new QueryParameter( + key: 'param2', + filter: 'filter_service_id' // From the container + ))->setValue(2), + new QueryParameter( + key: 'param3', + // Filer not called because no value + filter: $this->createFilterMock(false) + ), + new QueryParameter( + key: 'param4', + // Filer not called because no value + filter: 'filter_service_id_not_called' + ), + ]); + $extension = new ParameterExtension($filterLocator); + + $context = []; + $extension->applyToCollection($aggregationBuilder, 'SomeClass', $operation, $context); + + $this->assertSame([], $context); + } + + public function testApplyToCollectionWithLoggerAndManagerRegistry(): void + { + $aggregationBuilder = $this->createMock(Builder::class); + + $filter = new class implements FilterInterface, LoggerAwareInterface, ManagerRegistryAwareInterface { + use BackwardCompatibleFilterDescriptionTrait; + use LoggerAwareTrait; + use ManagerRegistryAwareTrait; + + public function apply(Builder $aggregationBuilder, string $resourceClass, ?Operation $operation = null, array &$context = []): void + { + Assert::assertNotNull($this->logger); + Assert::assertNotNull($this->managerRegistry); + Assert::assertSame('SomeClass', $resourceClass); + } + }; + + $operation = (new GetCollection()) + ->withParameters([ + (new QueryParameter( + key: 'param1', + filter: $filter, + ))->setValue(1), + ]); + + $extension = new ParameterExtension( + $this->createNonCalledFilterLocator(), + $this->createMock(ManagerRegistry::class), + $this->createMock(LoggerInterface::class), + ); + $context = []; + $extension->applyToCollection($aggregationBuilder, 'SomeClass', $operation, $context); + + $this->assertSame([], $context); + $this->assertNotNull($filter->getLogger()); + $this->assertNotNull($filter->getManagerRegistry()); + } + + public function testApplyToCollectionPassesContext(): void + { + $aggregationBuilder = $this->createMock(Builder::class); + + $filter = new class implements FilterInterface { + use BackwardCompatibleFilterDescriptionTrait; + + public function apply(Builder $aggregationBuilder, string $resourceClass, ?Operation $operation = null, array &$context = []): void + { + Assert::assertIsArray($context['filters']); + Assert::assertInstanceOf(Parameter::class, $context['parameter']); + $context['check_the_filters'][] = $context['filters']; + } + }; + + $operation = (new GetCollection()) + ->withParameters([ + (new QueryParameter( + key: 'param1', + filter: $filter, + ))->setValue(1), + (new QueryParameter( + key: 'param2', + filter: $filter, + ))->setValue(2), + ]); + + $extension = new ParameterExtension($this->createNonCalledFilterLocator()); + $context = []; + $extension->applyToCollection($aggregationBuilder, 'SomeClass', $operation, $context); + + $this->assertSame([ + 'check_the_filters' => [ + ['param1' => 1], + ['param2' => 2], + ], + ], $context); + } + + private function createFilterMock(bool $expectCall = true): FilterInterface + { + $filter = $this->createMock(FilterInterface::class); + $filter->expects($expectCall ? $this->once() : $this->never()) + ->method('apply'); + + return $filter; + } + + private function createNonCalledFilterLocator(): ContainerInterface + { + $filterLocator = $this->createMock(ContainerInterface::class); + $filterLocator->expects($this->never())->method('has'); + $filterLocator->expects($this->never())->method('get'); + + return $filterLocator; + } +} From 83f8f8cdb8b65b75e0d45104ccdf1f65a441fb97 Mon Sep 17 00:00:00 2001 From: soyuka Date: Thu, 18 Sep 2025 10:08:34 +0200 Subject: [PATCH 2/2] fix tests --- src/Doctrine/Odm/Extension/ParameterExtension.php | 2 +- src/Doctrine/Odm/composer.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Doctrine/Odm/Extension/ParameterExtension.php b/src/Doctrine/Odm/Extension/ParameterExtension.php index 5ed9d08fa7b..d68e6e9ed3b 100644 --- a/src/Doctrine/Odm/Extension/ParameterExtension.php +++ b/src/Doctrine/Odm/Extension/ParameterExtension.php @@ -89,7 +89,7 @@ private function applyFilter(Builder $aggregationBuilder, ?string $resourceClass $filter->setProperties($properties ?? []); } - $context['filters'] = $values; + $context['filters'] = $values; $context['parameter'] = $parameter; $filter->apply($aggregationBuilder, $resourceClass, $operation, $context); diff --git a/src/Doctrine/Odm/composer.json b/src/Doctrine/Odm/composer.json index 12e82c863db..ba419bb64e5 100644 --- a/src/Doctrine/Odm/composer.json +++ b/src/Doctrine/Odm/composer.json @@ -25,8 +25,8 @@ ], "require": { "php": ">=8.2", - "api-platform/doctrine-common": "^4.1.11", - "api-platform/metadata": "^4.1.11", + "api-platform/doctrine-common": "^4.2.0@beta", + "api-platform/metadata": "^4.2.0@beta", "api-platform/state": "^4.1.11", "doctrine/mongodb-odm": "^2.10", "symfony/property-info": "^6.4 || ^7.1",