Skip to content

Commit 6f85f2a

Browse files
fix(metadata): use entity class from stateOptions for filter property resolution (#7739)
Closes #7610
1 parent f30ea3c commit 6f85f2a

File tree

9 files changed

+185
-14
lines changed

9 files changed

+185
-14
lines changed

src/Doctrine/Odm/Metadata/Resource/DoctrineMongoDbOdmResourceCollectionMetadataFactory.php

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,9 @@ public function create(string $resourceClass): ResourceMetadataCollection
5050
continue;
5151
}
5252

53-
$operations->add($operationName, $this->addDefaults($operation));
53+
$operation = $this->addDefaults($operation);
54+
$operation = $this->setParametersFilterClass($operation, $documentClass);
55+
$operations->add($operationName, $operation);
5456
}
5557

5658
$resourceMetadata = $resourceMetadata->withOperations($operations);
@@ -60,11 +62,14 @@ public function create(string $resourceClass): ResourceMetadataCollection
6062

6163
if ($graphQlOperations) {
6264
foreach ($graphQlOperations as $operationName => $graphQlOperation) {
63-
if (!$this->managerRegistry->getManagerForClass($graphQlOperation->getClass()) instanceof DocumentManager) {
65+
$documentClass = $this->getStateOptionsClass($graphQlOperation, $graphQlOperation->getClass(), Options::class);
66+
if (!$this->managerRegistry->getManagerForClass($documentClass) instanceof DocumentManager) {
6467
continue;
6568
}
6669

67-
$graphQlOperations[$operationName] = $this->addDefaults($graphQlOperation);
70+
$graphQlOperation = $this->addDefaults($graphQlOperation);
71+
$graphQlOperation = $this->setParametersFilterClass($graphQlOperation, $documentClass);
72+
$graphQlOperations[$operationName] = $graphQlOperation;
6873
}
6974

7075
$resourceMetadata = $resourceMetadata->withGraphQlOperations($graphQlOperations);
@@ -112,4 +117,20 @@ private function getProcessor(Operation $operation): string
112117

113118
return 'api_platform.doctrine_mongodb.odm.state.persist_processor';
114119
}
120+
121+
private function setParametersFilterClass(Operation $operation, string $documentClass): Operation
122+
{
123+
$parameters = $operation->getParameters();
124+
if (!$parameters) {
125+
return $operation;
126+
}
127+
128+
foreach ($parameters as $key => $parameter) {
129+
if (null === $parameter->getFilterClass()) {
130+
$parameters->add($key, $parameter->withFilterClass($documentClass));
131+
}
132+
}
133+
134+
return $operation->withParameters($parameters);
135+
}
115136
}

src/Doctrine/Orm/Metadata/Resource/DoctrineOrmResourceCollectionMetadataFactory.php

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,9 @@ public function create(string $resourceClass): ResourceMetadataCollection
5151
continue;
5252
}
5353

54-
$operations->add($operationName, $this->addDefaults($operation));
54+
$operation = $this->addDefaults($operation);
55+
$operation = $this->setParametersFilterClass($operation, $entityClass);
56+
$operations->add($operationName, $operation);
5557
}
5658

5759
$resourceMetadata = $resourceMetadata->withOperations($operations);
@@ -67,7 +69,9 @@ public function create(string $resourceClass): ResourceMetadataCollection
6769
continue;
6870
}
6971

70-
$graphQlOperations[$operationName] = $this->addDefaults($graphQlOperation);
72+
$graphQlOperation = $this->addDefaults($graphQlOperation);
73+
$graphQlOperation = $this->setParametersFilterClass($graphQlOperation, $entityClass);
74+
$graphQlOperations[$operationName] = $graphQlOperation;
7175
}
7276

7377
$resourceMetadata = $resourceMetadata->withGraphQlOperations($graphQlOperations);
@@ -115,4 +119,20 @@ private function getProcessor(Operation $operation): string
115119

116120
return 'api_platform.doctrine.orm.state.persist_processor';
117121
}
122+
123+
private function setParametersFilterClass(Operation $operation, string $entityClass): Operation
124+
{
125+
$parameters = $operation->getParameters();
126+
if (!$parameters) {
127+
return $operation;
128+
}
129+
130+
foreach ($parameters as $key => $parameter) {
131+
if (null === $parameter->getFilterClass()) {
132+
$parameters->add($key, $parameter->withFilterClass($entityClass));
133+
}
134+
}
135+
136+
return $operation->withParameters($parameters);
137+
}
118138
}

src/Metadata/Parameter.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ abstract class Parameter
3030
* @param Type $nativeType the PHP native type, we cast values to an array if its a CollectionType, if not and it's an array with a single value we use it (eg: HTTP Header)
3131
* @param ?bool $castToNativeType whether API Platform should cast your parameter to the nativeType declared
3232
* @param ?callable(mixed): mixed $castFn the closure used to cast your parameter, this gets called only when $castToNativeType is set
33+
* @param ?string $filterClass the class to use when resolving filter properties (from stateOptions)
3334
*
3435
* @phpstan-param array<string, mixed>|null $schema
3536
*
@@ -56,6 +57,7 @@ public function __construct(
5657
protected ?bool $castToArray = null,
5758
protected ?bool $castToNativeType = null,
5859
protected mixed $castFn = null,
60+
protected ?string $filterClass = null,
5961
) {
6062
}
6163

@@ -370,4 +372,17 @@ public function withCastFn(mixed $castFn): self
370372

371373
return $self;
372374
}
375+
376+
public function getFilterClass(): ?string
377+
{
378+
return $this->filterClass;
379+
}
380+
381+
public function withFilterClass(?string $filterClass): self
382+
{
383+
$self = clone $this;
384+
$self->filterClass = $filterClass;
385+
386+
return $self;
387+
}
373388
}

src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -94,17 +94,23 @@ public function create(string $resourceClass): ResourceMetadataCollection
9494
/**
9595
* @return array{propertyNames: string[], properties: array<string, ApiProperty>}
9696
*/
97-
private function getProperties(string $resourceClass, ?Parameter $parameter = null): array
97+
private function getProperties(string $resourceClass, ?Parameter $parameter = null, ?Operation $operation = null): array
9898
{
99-
$k = $resourceClass.($parameter?->getProperties() ? ($parameter->getKey() ?? '') : '').(\is_string($parameter->getFilter()) ? $parameter->getFilter() : '');
99+
$filterClass = $parameter?->getFilterClass();
100+
if (null === $filterClass && null !== $operation) {
101+
$filterClass = $this->getStateOptionsClass($operation, $resourceClass);
102+
}
103+
$filterClass ??= $resourceClass;
104+
105+
$k = $resourceClass.($parameter?->getProperties() ? ($parameter->getKey() ?? '') : '').(\is_string($parameter->getFilter()) ? $parameter->getFilter() : '').$filterClass;
100106
if (isset($this->localPropertyCache[$k])) {
101107
return $this->localPropertyCache[$k];
102108
}
103109

104110
$propertyNames = [];
105111
$properties = [];
106-
foreach ($parameter?->getProperties() ?? $this->propertyNameCollectionFactory->create($resourceClass) as $property) {
107-
$propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $property);
112+
foreach ($parameter?->getProperties() ?? $this->propertyNameCollectionFactory->create($filterClass) as $property) {
113+
$propertyMetadata = $this->propertyMetadataFactory->create($filterClass, $property);
108114
if ($propertyMetadata->isReadable()) {
109115
$propertyNames[] = $property;
110116
$properties[$property] = $propertyMetadata;
@@ -147,7 +153,7 @@ private function getDefaultParameters(Operation $operation, string $resourceClas
147153
$parameter = $parameter->withKey($key);
148154
}
149155

150-
['propertyNames' => $propertyNames, 'properties' => $properties] = $this->getProperties($resourceClass, $parameter);
156+
['propertyNames' => $propertyNames, 'properties' => $properties] = $this->getProperties($resourceClass, $parameter, $operation);
151157
$parameter = $parameter->withProperties($propertyNames);
152158

153159
foreach ($propertyNames as $property) {
@@ -176,7 +182,7 @@ private function getDefaultParameters(Operation $operation, string $resourceClas
176182

177183
$key = $parameter->getKey();
178184

179-
['propertyNames' => $propertyNames, 'properties' => $properties] = $this->getProperties($resourceClass, $parameter);
185+
['propertyNames' => $propertyNames, 'properties' => $properties] = $this->getProperties($resourceClass, $parameter, $operation);
180186

181187
if ($filter instanceof PropertiesAwareInterface) {
182188
$parameter = $parameter->withProperties($propertyNames);

src/Symfony/Bundle/Resources/config/doctrine_mongodb_odm.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@
217217
$services->alias('api_platform.state.item_provider', 'ApiPlatform\Doctrine\Odm\State\ItemProvider');
218218

219219
$services->set('api_platform.doctrine.odm.metadata.resource.metadata_collection_factory', DoctrineMongoDbOdmResourceCollectionMetadataFactory::class)
220-
->decorate('api_platform.metadata.resource.metadata_collection_factory', null, 40)
220+
->decorate('api_platform.metadata.resource.metadata_collection_factory', null, -50)
221221
->args([
222222
service('doctrine_mongodb'),
223223
service('api_platform.doctrine.odm.metadata.resource.metadata_collection_factory.inner'),

src/Symfony/Bundle/Resources/config/doctrine_orm.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@
247247
->args([[]]);
248248

249249
$services->set('api_platform.doctrine.orm.metadata.resource.metadata_collection_factory', DoctrineOrmResourceCollectionMetadataFactory::class)
250-
->decorate('api_platform.metadata.resource.metadata_collection_factory', null, 40)
250+
->decorate('api_platform.metadata.resource.metadata_collection_factory', null, -50)
251251
->args([
252252
service('doctrine'),
253253
service('api_platform.doctrine.orm.metadata.resource.metadata_collection_factory.inner'),
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource;
15+
16+
use ApiPlatform\Doctrine\Orm\Filter\PartialSearchFilter;
17+
use ApiPlatform\Doctrine\Orm\State\Options;
18+
use ApiPlatform\Metadata\ApiResource;
19+
use ApiPlatform\Metadata\GetCollection;
20+
use ApiPlatform\Metadata\QueryParameter;
21+
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\FilterWithStateOptionsAndNoApiFilterEntity;
22+
23+
#[ApiResource(
24+
stateOptions: new Options(entityClass: FilterWithStateOptionsAndNoApiFilterEntity::class),
25+
operations: [
26+
new GetCollection(
27+
uriTemplate: '/filter_with_state_options_and_no_api_filters_api_resource',
28+
parameters: [
29+
'search[:property]' => new QueryParameter(
30+
properties: ['name'],
31+
filter: new PartialSearchFilter(),
32+
),
33+
],
34+
),
35+
]
36+
)]
37+
final class FilterWithStateOptionsAndNoApiFilter
38+
{
39+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Tests\Fixtures\TestBundle\Entity;
15+
16+
use Doctrine\ORM\Mapping as ORM;
17+
18+
#[ORM\Entity]
19+
class FilterWithStateOptionsAndNoApiFilterEntity
20+
{
21+
public function __construct(
22+
#[ORM\Column(type: 'integer')]
23+
#[ORM\Id]
24+
#[ORM\GeneratedValue(strategy: 'AUTO')]
25+
public ?int $id = null,
26+
#[ORM\Column(type: 'string', nullable: true)]
27+
public ?string $name = null,
28+
) {
29+
}
30+
}

tests/Functional/Parameters/DoctrineTest.php

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515

1616
use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
1717
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\FilterWithStateOptions;
18+
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\FilterWithStateOptionsAndNoApiFilter;
1819
use ApiPlatform\Tests\Fixtures\TestBundle\Document\SearchFilterParameter as SearchFilterParameterDocument;
20+
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\FilterWithStateOptionsAndNoApiFilterEntity;
1921
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\FilterWithStateOptionsEntity;
2022
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\ProductWithQueryParameter;
2123
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\SearchFilterParameter;
@@ -35,7 +37,12 @@ final class DoctrineTest extends ApiTestCase
3537
*/
3638
public static function getResources(): array
3739
{
38-
return [SearchFilterParameter::class, FilterWithStateOptions::class, ProductWithQueryParameter::class];
40+
return [
41+
SearchFilterParameter::class,
42+
FilterWithStateOptions::class,
43+
FilterWithStateOptionsAndNoApiFilter::class,
44+
ProductWithQueryParameter::class,
45+
];
3946
}
4047

4148
public function testDoctrineEntitySearchFilter(): void
@@ -147,6 +154,39 @@ public function testStateOptions(): void
147154
$this->assertEquals('after', $a['hydra:member'][0]['name']);
148155
}
149156

157+
public function testStateOptionsAndNoApiFilter(): void
158+
{
159+
if ($this->isMongoDB()) {
160+
$this->markTestSkipped('Not tested with mongodb.');
161+
}
162+
163+
static::bootKernel();
164+
$container = static::$kernel->getContainer();
165+
$this->recreateSchema([FilterWithStateOptionsAndNoApiFilterEntity::class]);
166+
167+
$manager = $container->get('doctrine')->getManager();
168+
$manager->persist(new FilterWithStateOptionsAndNoApiFilterEntity(name: 'current'));
169+
$manager->persist(new FilterWithStateOptionsAndNoApiFilterEntity(name: 'null'));
170+
$manager->persist(new FilterWithStateOptionsAndNoApiFilterEntity(name: 'after'));
171+
$manager->flush();
172+
173+
$uri = '/filter_with_state_options_and_no_api_filters_api_resource';
174+
175+
$response = self::createClient()->request('GET', $uri);
176+
$this->assertResponseIsSuccessful();
177+
$a = $response->toArray();
178+
$this->assertSame('hydra:Collection', $a['@type']);
179+
$this->assertSame(3, $a['hydra:totalItems']);
180+
$this->assertCount(3, $a['hydra:member']);
181+
182+
$response = self::createClient()->request('GET', $uri.'?search[name]=aft');
183+
$this->assertResponseIsSuccessful();
184+
$a = $response->toArray();
185+
$this->assertSame('hydra:Collection', $a['@type']);
186+
$this->assertSame(1, $a['hydra:totalItems']);
187+
$this->assertCount(1, $a['hydra:member']);
188+
}
189+
150190
#[DataProvider('partialFilterParameterProviderForSearchFilterParameter')]
151191
public function testPartialSearchFilterWithSearchFilterParameter(string $url, int $expectedCount, array $expectedFoos): void
152192
{

0 commit comments

Comments
 (0)