Skip to content

Commit e9b9d5a

Browse files
committed
fix(laravel): parameter fixes and tests
1 parent 941bf4f commit e9b9d5a

File tree

9 files changed

+89
-20
lines changed

9 files changed

+89
-20
lines changed

src/Laravel/ApiPlatformMiddleware.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public function __construct(
3030
*/
3131
public function handle(Request $request, \Closure $next, ?string $operationName = null): Response
3232
{
33+
$operation = null;
3334
if ($operationName) {
3435
$request->attributes->set('_api_operation', $operation = $this->operationMetadataFactory->create($operationName));
3536
}

src/Laravel/Eloquent/Metadata/ModelMetadata.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ public function getRelations(Model $model): Collection
193193
'name' => $method->getName(),
194194
'type' => $relation::class,
195195
'related' => \get_class($relation->getRelated()),
196+
'foreign_key' => method_exists($relation, 'getForeignKeyName') ? $relation->getForeignKeyName() : null,
196197
];
197198
})
198199
->filter()

src/Laravel/State/ParameterValidatorProvider.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,12 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
6464
$value = null;
6565
}
6666

67-
foreach ($constraints as $c) {
68-
$allConstraints[] = $c;
67+
foreach ((array) $constraints as $k => $c) {
68+
if (!\is_string($k)) {
69+
$k = $key;
70+
}
71+
72+
$allConstraints[$k] = $c;
6973
}
7074
}
7175

src/Laravel/Tests/EloquentTest.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,18 @@ public function testSearchFilter(): void
3333
$this->assertSame($response->json()['member'][0], $book);
3434
}
3535

36+
public function testValidateSearchFilter(): void
37+
{
38+
$response = $this->get('/api/books?isbn=a', ['accept' => ['application/ld+json']]);
39+
$this->assertSame($response->json()['detail'], 'The isbn field must be at least 2 characters.');
40+
}
41+
42+
public function testSearchFilterRelation(): void
43+
{
44+
$response = $this->get('/api/books?author=1', ['accept' => ['application/ld+json']]);
45+
$this->assertSame($response->json()['member'][0]['author'], '/api/authors/1');
46+
}
47+
3648
public function testPropertyFilter(): void
3749
{
3850
$response = $this->get('/api/books', ['accept' => ['application/ld+json']]);

src/Laravel/workbench/app/Models/Book.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,9 @@
4545
new GetCollection(),
4646
]
4747
)]
48-
#[QueryParameter(key: 'isbn', filter: PartialSearchFilter::class)]
48+
#[QueryParameter(key: 'isbn', filter: PartialSearchFilter::class, constraints: 'min:2')]
4949
#[QueryParameter(key: 'name', filter: PartialSearchFilter::class)]
50+
#[QueryParameter(key: 'author', filter: EqualsFilter::class)]
5051
#[QueryParameter(key: 'publicationDate', filter: DateFilter::class)]
5152
#[QueryParameter(
5253
key: 'name2',

src/Metadata/Parameter.php

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ abstract class Parameter
2828
* @param array<string, mixed> $extraProperties
2929
* @param ParameterProviderInterface|callable|string|null $provider
3030
* @param FilterInterface|string|null $filter
31-
* @param Constraint|Constraint[]|null $constraints
31+
* @param Constraint|array<string, string>|string|Constraint[]|null $constraints
3232
*/
3333
public function __construct(
3434
protected ?string $key = null,
@@ -41,7 +41,7 @@ public function __construct(
4141
protected ?bool $required = null,
4242
protected ?int $priority = null,
4343
protected ?false $hydra = null,
44-
protected Constraint|array|null $constraints = null,
44+
protected Constraint|array|string|null $constraints = null,
4545
protected string|\Stringable|null $security = null,
4646
protected ?string $securityMessage = null,
4747
protected ?array $extraProperties = [],
@@ -103,9 +103,9 @@ public function getHydra(): ?bool
103103
}
104104

105105
/**
106-
* @return Constraint|Constraint[]|null
106+
* @return Constraint|string|array<string, string>|Constraint[]|null
107107
*/
108-
public function getConstraints(): Constraint|array|null
108+
public function getConstraints(): Constraint|string|array|null
109109
{
110110
return $this->constraints;
111111
}
@@ -232,7 +232,10 @@ public function withHydra(false $hydra): static
232232
return $self;
233233
}
234234

235-
public function withConstraints(array|Constraint $constraints): static
235+
/**
236+
* @param string|array<string, string>|Constraint[]|Constraint
237+
*/
238+
public function withConstraints(string|array|Constraint $constraints): static
236239
{
237240
$self = clone $this;
238241
$self->constraints = $constraints;

src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
namespace ApiPlatform\Metadata\Resource\Factory;
1515

16+
use ApiPlatform\Metadata\ApiProperty;
1617
use ApiPlatform\Metadata\FilterInterface;
1718
use ApiPlatform\Metadata\HasOpenApiParameterFilterInterface;
1819
use ApiPlatform\Metadata\HasSchemaFilterInterface;
@@ -63,14 +64,17 @@ public function create(string $resourceClass): ResourceMetadataCollection
6364
$resourceMetadataCollection = $this->decorated?->create($resourceClass) ?? new ResourceMetadataCollection($resourceClass);
6465

6566
$propertyNames = [];
67+
$properties = [];
6668
foreach ($this->propertyNameCollectionFactory->create($resourceClass) as $i => $property) {
6769
$propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $property);
70+
if ('author' === $property) {
71+
}
6872
if ($propertyMetadata->isReadable()) {
6973
$propertyNames[] = $property;
74+
$properties[$property] = $propertyMetadata;
7075
}
7176
}
7277

73-
$properties = array_flip($propertyNames);
7478
foreach ($resourceMetadataCollection as $i => $resource) {
7579
$operations = $resource->getOperations();
7680

@@ -169,7 +173,7 @@ private function addFilterMetadata(Parameter $parameter): Parameter
169173
}
170174

171175
/**
172-
* @param array<string, int> $properties
176+
* @param array<string, ApiProperty> $properties
173177
*/
174178
private function setDefaults(string $key, Parameter $parameter, string $resourceClass, array $properties): Parameter
175179
{
@@ -196,12 +200,18 @@ private function setDefaults(string $key, Parameter $parameter, string $resource
196200
$parameter = $parameter->withProperty($property);
197201
}
198202

203+
$currentKey = $key;
199204
if (null === $parameter->getProperty() && isset($properties[$key])) {
200205
$parameter = $parameter->withProperty($key);
201206
}
202207

203208
if (null === $parameter->getProperty() && $this->nameConverter && ($nameConvertedKey = $this->nameConverter->normalize($key)) && isset($properties[$nameConvertedKey])) {
204209
$parameter = $parameter->withProperty($key)->withExtraProperties(['_query_property' => $nameConvertedKey] + $parameter->getExtraProperties());
210+
$currentKey = $nameConvertedKey;
211+
}
212+
213+
if (isset($properties[$currentKey]) && ($eloquentRelation = ($properties[$currentKey]->getExtraProperties()['eloquent_relation'] ?? null)) && isset($eloquentRelation['foreign_key'])) {
214+
$parameter = $parameter->withExtraProperties(['_query_property' => $eloquentRelation['foreign_key']] + $parameter->getExtraProperties());
205215
}
206216

207217
if (null === $parameter->getRequired() && ($required = $description[$key]['required'] ?? null)) {

src/Serializer/Filter/PropertyFilter.php

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
namespace ApiPlatform\Serializer\Filter;
1515

1616
use ApiPlatform\Metadata\HasOpenApiParameterFilterInterface;
17+
use ApiPlatform\Metadata\HasSchemaFilterInterface;
1718
use ApiPlatform\Metadata\Parameter as MetadataParameter;
1819
use ApiPlatform\Metadata\QueryParameter;
1920
use ApiPlatform\OpenApi\Model\Parameter;
@@ -117,7 +118,7 @@
117118
*
118119
* @author Baptiste Meyer <[email protected]>
119120
*/
120-
final class PropertyFilter implements FilterInterface, HasOpenApiParameterFilterInterface
121+
final class PropertyFilter implements FilterInterface, HasOpenApiParameterFilterInterface, HasSchemaFilterInterface
121122
{
122123
private ?array $whitelist;
123124

@@ -131,25 +132,40 @@ public function __construct(private readonly string $parameterName = 'properties
131132
*/
132133
public function apply(Request $request, bool $normalization, array $attributes, array &$context): void
133134
{
135+
// TODO: ideally we should return the new context, not mutate the context given in our arguments which is the serializer context
136+
// this would allow to use `Parameter::filterContext` properly, for now let's retrieve it like this:
137+
/** @var MetadataParameter */
138+
$parameter = $request->attributes->get('_api_parameter', null);
139+
$parameterName = $this->parameterName;
140+
$whitelist = $this->whitelist;
141+
$overrideDefaultProperties = $this->overrideDefaultProperties;
142+
143+
if ($parameter) {
144+
$parameterName = $parameter->getKey();
145+
$whitelist = $parameter->getFilterContext()['whitelist'] ?? $this->whitelist;
146+
$overrideDefaultProperties = $parameter->getFilterContext()['override_default_properties'] ?? $this->overrideDefaultProperties;
147+
}
148+
134149
if (null !== $propertyAttribute = $request->attributes->get('_api_filter_property')) {
135150
$properties = $propertyAttribute;
136-
} elseif (\array_key_exists($this->parameterName, $commonAttribute = $request->attributes->get('_api_filters', []))) {
137-
$properties = $commonAttribute[$this->parameterName];
151+
} elseif (\array_key_exists($parameterName, $commonAttribute = $request->attributes->get('_api_filters', []))) {
152+
$properties = $commonAttribute[$parameterName];
138153
} else {
139-
$properties = $request->query->all()[$this->parameterName] ?? null;
154+
$properties = $request->query->all()[$parameterName] ?? null;
140155
}
141156

142157
if (!\is_array($properties)) {
143158
return;
144159
}
145160

161+
// TODO: when refactoring this eventually, note that the ParameterResourceMetadataCollectionFactory already does that and caches this behavior in our Parameter metadata
146162
$properties = $this->denormalizeProperties($properties);
147163

148-
if (null !== $this->whitelist) {
149-
$properties = $this->getProperties($properties, $this->whitelist);
164+
if (null !== $whitelist) {
165+
$properties = $this->getProperties($properties, $whitelist);
150166
}
151167

152-
if (!$this->overrideDefaultProperties && isset($context[AbstractNormalizer::ATTRIBUTES])) {
168+
if (!$overrideDefaultProperties && isset($context[AbstractNormalizer::ATTRIBUTES])) {
153169
$properties = array_merge_recursive((array) $context[AbstractNormalizer::ATTRIBUTES], $properties);
154170
}
155171

@@ -161,7 +177,8 @@ public function apply(Request $request, bool $normalization, array $attributes,
161177
*/
162178
public function getDescription(string $resourceClass): array
163179
{
164-
$example = \sprintf('%1$s[]={propertyName}&%1$s[]={anotherPropertyName}&%1$s[{nestedPropertyParent}][]={nestedProperty}',
180+
$example = \sprintf(
181+
'%1$s[]={propertyName}&%1$s[]={anotherPropertyName}&%1$s[{nestedPropertyParent}][]={nestedProperty}',
165182
$this->parameterName
166183
);
167184

@@ -250,8 +267,27 @@ private function denormalizePropertyName($property): string
250267
return null !== $this->nameConverter ? $this->nameConverter->denormalize($property) : $property;
251268
}
252269

270+
public function getSchema(MetadataParameter $parameter): array
271+
{
272+
return [
273+
'type' => 'array',
274+
'items' => [
275+
'type' => 'string',
276+
],
277+
];
278+
}
279+
253280
public function getOpenApiParameter(MetadataParameter $parameter): Parameter
254281
{
255-
return new Parameter(name: $parameter->getKey().'[]', in: $parameter instanceof QueryParameter ? 'query' : 'header');
282+
$example = \sprintf(
283+
'%1$s[]={propertyName}&%1$s[]={anotherPropertyName}',
284+
$parameter->getKey()
285+
);
286+
287+
return new Parameter(
288+
name: $parameter->getKey().'[]',
289+
in: $parameter instanceof QueryParameter ? 'query' : 'header',
290+
description: 'Allows you to reduce the response to contain only the properties you need. If your desired property is nested, you can address it using nested arrays. Example: '.$example
291+
);
256292
}
257293
}

src/State/SerializerContextBuilderInterface.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ interface SerializerContextBuilderInterface
4848
* deep_object_to_populate?: bool,
4949
* collect_denormalization_errors?: bool,
5050
* exclude_from_cache_key?: string[],
51-
* api_included?: bool
51+
* api_included?: bool,
52+
* attributes?: string[],
5253
* }
5354
*/
5455
public function createFromRequest(Request $request, bool $normalization, ?array $extractedAttributes = null): array;

0 commit comments

Comments
 (0)