Skip to content

Commit 9814f27

Browse files
authored
fix(metadata): property placeholder on multiple parameters (#7598)
1 parent 0ef0ba6 commit 9814f27

File tree

4 files changed

+269
-7
lines changed

4 files changed

+269
-7
lines changed

src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,10 @@ private function getDefaultParameters(Operation $operation, string $resourceClas
143143
continue;
144144
}
145145

146+
if (!$parameter->getKey()) {
147+
$parameter = $parameter->withKey($key);
148+
}
149+
146150
['propertyNames' => $propertyNames, 'properties' => $properties] = $this->getProperties($resourceClass, $parameter);
147151
$parameter = $parameter->withProperties($propertyNames);
148152

@@ -170,7 +174,7 @@ private function getDefaultParameters(Operation $operation, string $resourceClas
170174
$parameter = $parameter->withProvider($f->getParameterProvider());
171175
}
172176

173-
$key = $parameter->getKey() ?? $key;
177+
$key = $parameter->getKey();
174178

175179
['propertyNames' => $propertyNames, 'properties' => $properties] = $this->getProperties($resourceClass, $parameter);
176180

src/Metadata/Tests/Resource/Factory/ParameterResourceMetadataCollectionFactoryTest.php

Lines changed: 89 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
namespace ApiPlatform\Metadata\Tests\Resource\Factory;
1515

1616
use ApiPlatform\Metadata\ApiProperty;
17+
use ApiPlatform\Metadata\ApiResource;
1718
use ApiPlatform\Metadata\FilterInterface;
19+
use ApiPlatform\Metadata\GetCollection;
1820
use ApiPlatform\Metadata\Parameters;
1921
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
2022
use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
@@ -36,8 +38,12 @@ public function testParameterFactory(): void
3638
$nameCollection->method('create')->willReturn(new PropertyNameCollection(['id', 'hydra', 'everywhere']));
3739
$propertyMetadata = $this->createStub(PropertyMetadataFactoryInterface::class);
3840
$propertyMetadata->method('create')->willReturnOnConsecutiveCalls(
39-
new ApiProperty(identifier: true), new ApiProperty(readable: true), new ApiProperty(readable: true),
40-
new ApiProperty(identifier: true), new ApiProperty(readable: true), new ApiProperty(readable: true)
41+
new ApiProperty(identifier: true),
42+
new ApiProperty(readable: true),
43+
new ApiProperty(readable: true),
44+
new ApiProperty(identifier: true),
45+
new ApiProperty(readable: true),
46+
new ApiProperty(readable: true)
4147
);
4248
$filterLocator = $this->createStub(ContainerInterface::class);
4349
$filterLocator->method('has')->willReturn(true);
@@ -77,14 +83,71 @@ public function getDescription(string $resourceClass): array
7783
$this->assertNull($everywhere->getOpenApi());
7884
}
7985

86+
public function testQueryParameterWithPropertyPlaceholder(): void
87+
{
88+
$nameCollection = $this->createStub(PropertyNameCollectionFactoryInterface::class);
89+
$nameCollection->method('create')->willReturn(new PropertyNameCollection(['id', 'name', 'description']));
90+
91+
$propertyMetadata = $this->createStub(PropertyMetadataFactoryInterface::class);
92+
$propertyMetadata->method('create')->willReturn(
93+
new ApiProperty(readable: true),
94+
);
95+
96+
$filterLocator = $this->createStub(ContainerInterface::class);
97+
$filterLocator->method('has')->willReturn(false); // No specific filter logic needed for this test
98+
99+
$parameterFactory = new ParameterResourceMetadataCollectionFactory(
100+
$nameCollection,
101+
$propertyMetadata,
102+
new AttributesResourceMetadataCollectionFactory(),
103+
$filterLocator
104+
);
105+
106+
$resourceMetadataCollection = $parameterFactory->create(HasParameterAttribute::class);
107+
$operation = $resourceMetadataCollection->getOperation(forceCollection: true);
108+
$parameters = $operation->getParameters();
109+
110+
$this->assertInstanceOf(Parameters::class, $parameters);
111+
112+
// Assert that the original parameter with ':property' is removed
113+
$this->assertFalse($parameters->has('search[:property]'));
114+
115+
// Assert that the new parameters are created and have the correct properties
116+
$this->assertTrue($parameters->has('search[name]'));
117+
$this->assertTrue($parameters->has('search[description]'));
118+
$this->assertTrue($parameters->has('static_param'));
119+
120+
$searchNameParam = $parameters->get('search[name]');
121+
$this->assertInstanceOf(QueryParameter::class, $searchNameParam);
122+
$this->assertSame('Search by property', $searchNameParam->getDescription());
123+
$this->assertSame('name', $searchNameParam->getProperty());
124+
$this->assertSame('search[name]', $searchNameParam->getKey());
125+
126+
$searchDescriptionParam = $parameters->get('search[description]');
127+
$this->assertInstanceOf(QueryParameter::class, $searchDescriptionParam);
128+
$this->assertSame('Search by property', $searchDescriptionParam->getDescription());
129+
$this->assertSame('description', $searchDescriptionParam->getProperty());
130+
$this->assertSame('search[description]', $searchDescriptionParam->getKey());
131+
132+
$staticParam = $parameters->get('static_param');
133+
$this->assertInstanceOf(QueryParameter::class, $staticParam);
134+
$this->assertSame('A static parameter', $staticParam->getDescription());
135+
$this->assertNull($staticParam->getProperty());
136+
$this->assertSame('static_param', $staticParam->getKey());
137+
}
138+
80139
public function testParameterFactoryNoFilter(): void
81140
{
82141
$nameCollection = $this->createStub(PropertyNameCollectionFactoryInterface::class);
83142
$nameCollection->method('create')->willReturn(new PropertyNameCollection(['id', 'hydra', 'everywhere']));
84143
$propertyMetadata = $this->createStub(PropertyMetadataFactoryInterface::class);
85144
$propertyMetadata->method('create')->willReturnOnConsecutiveCalls(
86-
new ApiProperty(identifier: true), new ApiProperty(readable: true), new ApiProperty(readable: true),
87-
new ApiProperty(identifier: true), new ApiProperty(readable: true), new ApiProperty(readable: true)
145+
new ApiProperty(identifier: true),
146+
new ApiProperty(readable: true),
147+
new ApiProperty(readable: true),
148+
new ApiProperty(identifier: true),
149+
new ApiProperty(readable: true),
150+
new ApiProperty(readable: true)
88151
);
89152
$filterLocator = $this->createStub(ContainerInterface::class);
90153
$filterLocator->method('has')->willReturn(false);
@@ -135,3 +198,25 @@ public function testParameterFactoryWithLimitedProperties(): void
135198
$this->assertSame(['name'], $param->getProperties());
136199
}
137200
}
201+
202+
#[ApiResource(
203+
operations: [
204+
new GetCollection(
205+
parameters: [
206+
'search[:property]' => new QueryParameter(
207+
description: 'Search by property',
208+
properties: ['name', 'description']
209+
),
210+
'static_param' => new QueryParameter(
211+
description: 'A static parameter'
212+
),
213+
]
214+
),
215+
]
216+
)]
217+
class HasParameterAttribute
218+
{
219+
public $id;
220+
public $name;
221+
public $description;
222+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
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 ApiPlatform\Doctrine\Orm\Filter\ExactFilter;
17+
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
18+
use ApiPlatform\Doctrine\Orm\Filter\PartialSearchFilter;
19+
use ApiPlatform\Metadata\ApiResource;
20+
use ApiPlatform\Metadata\GetCollection;
21+
use ApiPlatform\Metadata\QueryParameter;
22+
use Doctrine\DBAL\Types\Types;
23+
use Doctrine\ORM\Mapping as ORM;
24+
25+
#[ORM\Entity]
26+
#[ApiResource(
27+
operations: [
28+
new GetCollection(
29+
parameters: [
30+
'brand' => new QueryParameter(
31+
filter: new ExactFilter(),
32+
),
33+
'search[:property]' => new QueryParameter(
34+
filter: new PartialSearchFilter(),
35+
properties: ['title', 'description']
36+
),
37+
'filter[:property]' => new QueryParameter(
38+
filter: new ExactFilter(),
39+
properties: ['category', 'brand'],
40+
),
41+
'order[:property]' => new QueryParameter(
42+
filter: new OrderFilter(),
43+
properties: ['rating']
44+
),
45+
]
46+
),
47+
]
48+
)]
49+
class ProductWithQueryParameter
50+
{
51+
#[ORM\Id]
52+
#[ORM\Column()]
53+
#[ORM\GeneratedValue]
54+
private ?int $id = null;
55+
56+
#[ORM\Column(length: 255)]
57+
public ?string $sku = null;
58+
59+
#[ORM\Column(length: 255)]
60+
public ?string $title = null;
61+
62+
#[ORM\Column(nullable: true)]
63+
public ?string $description = null;
64+
65+
#[ORM\Column(nullable: true)]
66+
public ?string $category = null;
67+
68+
#[ORM\Column(nullable: true)]
69+
public ?string $brand = null;
70+
71+
#[ORM\Column(nullable: true)]
72+
public ?float $exactPrice = null;
73+
74+
#[ORM\Column()]
75+
public int $rating = 0;
76+
77+
#[ORM\Column()]
78+
public int $stock = 0;
79+
80+
#[ORM\Column(type: Types::JSON, nullable: true, options: ['jsonb' => true])]
81+
public array $tags = [];
82+
83+
public function getId(): ?int
84+
{
85+
return $this->id;
86+
}
87+
}

tests/Functional/Parameters/DoctrineTest.php

Lines changed: 88 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\FilterWithStateOptions;
1818
use ApiPlatform\Tests\Fixtures\TestBundle\Document\SearchFilterParameter as SearchFilterParameterDocument;
1919
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\FilterWithStateOptionsEntity;
20+
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\ProductWithQueryParameter;
2021
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\SearchFilterParameter;
2122
use ApiPlatform\Tests\RecreateSchemaTrait;
2223
use ApiPlatform\Tests\SetupClassResourcesTrait;
@@ -34,7 +35,7 @@ final class DoctrineTest extends ApiTestCase
3435
*/
3536
public static function getResources(): array
3637
{
37-
return [SearchFilterParameter::class, FilterWithStateOptions::class];
38+
return [SearchFilterParameter::class, FilterWithStateOptions::class, ProductWithQueryParameter::class];
3839
}
3940

4041
public function testDoctrineEntitySearchFilter(): void
@@ -195,7 +196,50 @@ public static function partialFilterParameterProviderForSearchFilterParameter():
195196
];
196197
}
197198

198-
public function loadFixtures(string $resourceClass): void
199+
public function testQueryParameterWithPropertyArgument(): void
200+
{
201+
if ($this->isMongoDB()) {
202+
$this->markTestSkipped('Not tested with mongodb.');
203+
}
204+
205+
$resource = ProductWithQueryParameter::class;
206+
$this->recreateSchema([$resource]);
207+
$this->loadProductFixtures($resource);
208+
209+
// Test search[:property] with 'title'
210+
$response = self::createClient()->request('GET', '/product_with_query_parameters?search[title]=Awesome');
211+
$this->assertResponseIsSuccessful();
212+
$this->assertCount(1, $response->toArray()['hydra:member']);
213+
$this->assertEquals('Awesome Widget', $response->toArray()['hydra:member'][0]['title']);
214+
215+
// Test search[:property] with 'description'
216+
$response = self::createClient()->request('GET', '/product_with_query_parameters?search[description]=super');
217+
$this->assertResponseIsSuccessful();
218+
$this->assertCount(1, $response->toArray()['hydra:member']);
219+
$this->assertEquals('Super Gadget', $response->toArray()['hydra:member'][0]['title']);
220+
221+
// Test filter[:property] with 'category'
222+
$response = self::createClient()->request('GET', '/product_with_query_parameters?filter[category]=Electronics');
223+
$this->assertResponseIsSuccessful();
224+
$this->assertCount(2, $response->toArray()['hydra:member']);
225+
226+
// Test filter[:property] with 'brand'
227+
$response = self::createClient()->request('GET', '/product_with_query_parameters?filter[brand]=BrandY');
228+
$this->assertResponseIsSuccessful();
229+
$this->assertCount(1, $response->toArray()['hydra:member']);
230+
$this->assertEquals('Super Gadget', $response->toArray()['hydra:member'][0]['title']);
231+
232+
// Test order[:property] with 'rating'
233+
$response = self::createClient()->request('GET', '/product_with_query_parameters?order[rating]=desc');
234+
$this->assertResponseIsSuccessful();
235+
$members = $response->toArray()['hydra:member'];
236+
$this->assertCount(3, $members);
237+
$this->assertEquals('Awesome Widget', $members[0]['title']);
238+
$this->assertEquals('Super Gadget', $members[1]['title']);
239+
$this->assertEquals('Mega Device', $members[2]['title']);
240+
}
241+
242+
private function loadFixtures(string $resourceClass): void
199243
{
200244
$container = static::$kernel->getContainer();
201245
$registry = $this->isMongoDB() ? $container->get('doctrine_mongodb') : $container->get('doctrine');
@@ -214,4 +258,46 @@ public function loadFixtures(string $resourceClass): void
214258

215259
$manager->flush();
216260
}
261+
262+
private function loadProductFixtures(string $resourceClass): void
263+
{
264+
$container = static::$kernel->getContainer();
265+
$registry = $this->isMongoDB() ? $container->get('doctrine_mongodb') : $container->get('doctrine');
266+
$manager = $registry->getManager();
267+
268+
$product1 = new $resourceClass();
269+
$product1->sku = 'SKU001';
270+
$product1->title = 'Awesome Widget';
271+
$product1->description = 'A really awesome widget.';
272+
$product1->category = 'Electronics';
273+
$product1->brand = 'BrandX';
274+
$product1->rating = 5;
275+
$product1->stock = 100;
276+
$product1->tags = ['new', 'sale'];
277+
$manager->persist($product1);
278+
279+
$product2 = new $resourceClass();
280+
$product2->sku = 'SKU002';
281+
$product2->title = 'Super Gadget';
282+
$product2->description = 'A super cool gadget.';
283+
$product2->category = 'Electronics';
284+
$product2->brand = 'BrandY';
285+
$product2->rating = 4;
286+
$product2->stock = 50;
287+
$product2->tags = ['popular'];
288+
$manager->persist($product2);
289+
290+
$product3 = new $resourceClass();
291+
$product3->sku = 'SKU003';
292+
$product3->title = 'Mega Device';
293+
$product3->description = 'A mega useful device.';
294+
$product3->category = 'Home';
295+
$product3->brand = 'BrandX';
296+
$product3->rating = 3;
297+
$product3->stock = 200;
298+
$product3->tags = ['clearance'];
299+
$manager->persist($product3);
300+
301+
$manager->flush();
302+
}
217303
}

0 commit comments

Comments
 (0)