Skip to content

Commit a49bde1

Browse files
NathanPesneausoyukaNathan
authored
feat(laravel): filter validations rules
* refactor(metadata): move parameter validation to the validator component * feat(laravel): validations rules filters * cs fixes * fix(laravel): eloquent filters validation * fix(laravel): eloquent filters * fixes * fix --------- Co-authored-by: soyuka <[email protected]> Co-authored-by: Nathan <[email protected]>
1 parent 9d3a0b5 commit a49bde1

File tree

7 files changed

+412
-167
lines changed

7 files changed

+412
-167
lines changed

src/Laravel/ApiPlatformProvider.php

Lines changed: 32 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@
102102
use ApiPlatform\Laravel\Metadata\CachePropertyMetadataFactory;
103103
use ApiPlatform\Laravel\Metadata\CachePropertyNameCollectionMetadataFactory;
104104
use ApiPlatform\Laravel\Metadata\CacheResourceCollectionMetadataFactory;
105+
use ApiPlatform\Laravel\Metadata\ParameterValidationResourceMetadataCollectionFactory;
105106
use ApiPlatform\Laravel\Routing\IriConverter;
106107
use ApiPlatform\Laravel\Routing\Router as UrlGeneratorRouter;
107108
use ApiPlatform\Laravel\Routing\SkolemIriConverter;
@@ -331,46 +332,49 @@ public function register(): void
331332

332333
return new CacheResourceCollectionMetadataFactory(
333334
new EloquentResourceCollectionMetadataFactory(
334-
new ParameterResourceMetadataCollectionFactory(
335-
$this->app->make(PropertyNameCollectionFactoryInterface::class),
336-
$this->app->make(PropertyMetadataFactoryInterface::class),
337-
new AlternateUriResourceMetadataCollectionFactory(
338-
new FiltersResourceMetadataCollectionFactory(
339-
new FormatsResourceMetadataCollectionFactory(
340-
new InputOutputResourceMetadataCollectionFactory(
341-
new PhpDocResourceMetadataCollectionFactory(
342-
new OperationNameResourceMetadataCollectionFactory(
343-
new LinkResourceMetadataCollectionFactory(
344-
$app->make(LinkFactoryInterface::class),
345-
new UriTemplateResourceMetadataCollectionFactory(
335+
new ParameterValidationResourceMetadataCollectionFactory(
336+
new ParameterResourceMetadataCollectionFactory(
337+
$this->app->make(PropertyNameCollectionFactoryInterface::class),
338+
$this->app->make(PropertyMetadataFactoryInterface::class),
339+
new AlternateUriResourceMetadataCollectionFactory(
340+
new FiltersResourceMetadataCollectionFactory(
341+
new FormatsResourceMetadataCollectionFactory(
342+
new InputOutputResourceMetadataCollectionFactory(
343+
new PhpDocResourceMetadataCollectionFactory(
344+
new OperationNameResourceMetadataCollectionFactory(
345+
new LinkResourceMetadataCollectionFactory(
346346
$app->make(LinkFactoryInterface::class),
347-
$app->make(PathSegmentNameGeneratorInterface::class),
348-
new NotExposedOperationResourceMetadataCollectionFactory(
347+
new UriTemplateResourceMetadataCollectionFactory(
349348
$app->make(LinkFactoryInterface::class),
350-
new AttributesResourceMetadataCollectionFactory(
351-
new ConcernsResourceMetadataCollectionFactory(
352-
null,
349+
$app->make(PathSegmentNameGeneratorInterface::class),
350+
new NotExposedOperationResourceMetadataCollectionFactory(
351+
$app->make(LinkFactoryInterface::class),
352+
new AttributesResourceMetadataCollectionFactory(
353+
new ConcernsResourceMetadataCollectionFactory(
354+
null,
355+
$app->make(LoggerInterface::class),
356+
$config->get('api-platform.defaults', []),
357+
$config->get('api-platform.graphql.enabled'),
358+
),
353359
$app->make(LoggerInterface::class),
354360
$config->get('api-platform.defaults', []),
355361
$config->get('api-platform.graphql.enabled'),
356362
),
357-
$app->make(LoggerInterface::class),
358-
$config->get('api-platform.defaults', []),
359-
$config->get('api-platform.graphql.enabled'),
360-
),
363+
)
361364
)
362365
)
363366
)
364367
)
365-
)
366-
),
367-
$formats,
368-
$config->get('api-platform.patch_formats'),
368+
),
369+
$formats,
370+
$config->get('api-platform.patch_formats'),
371+
)
369372
)
370-
)
373+
),
374+
$app->make('filters'),
375+
$app->make(CamelCaseToSnakeCaseNameConverter::class)
371376
),
372-
$app->make('filters'),
373-
$app->make(CamelCaseToSnakeCaseNameConverter::class)
377+
$app->make('filters')
374378
)
375379
),
376380
true === $config->get('app.debug') ? 'array' : 'file'
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
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\Laravel\Metadata;
15+
16+
use ApiPlatform\Metadata\Parameter;
17+
use ApiPlatform\Metadata\Parameters;
18+
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
19+
use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
20+
use Illuminate\Validation\Rule;
21+
use Psr\Container\ContainerInterface;
22+
23+
final class ParameterValidationResourceMetadataCollectionFactory implements ResourceMetadataCollectionFactoryInterface
24+
{
25+
public function __construct(
26+
private readonly ?ResourceMetadataCollectionFactoryInterface $decorated = null,
27+
private readonly ?ContainerInterface $filterLocator = null,
28+
) {
29+
}
30+
31+
public function create(string $resourceClass): ResourceMetadataCollection
32+
{
33+
$resourceMetadataCollection = $this->decorated?->create($resourceClass) ?? new ResourceMetadataCollection($resourceClass);
34+
35+
foreach ($resourceMetadataCollection as $i => $resource) {
36+
$operations = $resource->getOperations();
37+
38+
foreach ($operations as $operationName => $operation) {
39+
$parameters = $operation->getParameters() ?? new Parameters();
40+
foreach ($parameters as $key => $parameter) {
41+
$parameters->add($key, $this->addSchemaValidation($parameter));
42+
}
43+
44+
if (\count($parameters) > 0) {
45+
$operations->add($operationName, $operation->withParameters($parameters));
46+
}
47+
}
48+
49+
$resourceMetadataCollection[$i] = $resource->withOperations($operations->sort());
50+
51+
if (!$graphQlOperations = $resource->getGraphQlOperations()) {
52+
continue;
53+
}
54+
55+
foreach ($graphQlOperations as $operationName => $operation) {
56+
$parameters = $operation->getParameters() ?? new Parameters();
57+
foreach ($operation->getParameters() ?? [] as $key => $parameter) {
58+
$parameters->add($key, $this->addSchemaValidation($parameter));
59+
}
60+
61+
if (\count($parameters) > 0) {
62+
$graphQlOperations[$operationName] = $operation->withParameters($parameters);
63+
}
64+
}
65+
66+
$resourceMetadataCollection[$i] = $resource->withGraphQlOperations($graphQlOperations);
67+
}
68+
69+
return $resourceMetadataCollection;
70+
}
71+
72+
private function addSchemaValidation(Parameter $parameter): Parameter
73+
{
74+
$schema = $parameter->getSchema();
75+
$required = $parameter->getRequired();
76+
$openApi = $parameter->getOpenApi();
77+
78+
// When it's an array of openapi parameters take the first one as it's probably just a variant of the query parameter,
79+
// only getAllowEmptyValue is used here anyways
80+
if (\is_array($openApi)) {
81+
$openApi = $openApi[0];
82+
}
83+
$assertions = [];
84+
$allowEmptyValue = $openApi?->getAllowEmptyValue();
85+
if ($required || (false === $required && false === $allowEmptyValue)) {
86+
$assertions[] = 'required';
87+
}
88+
89+
if (true === $allowEmptyValue) {
90+
$assertions[] = 'nullable';
91+
}
92+
93+
if (isset($schema['exclusiveMinimum'])) {
94+
$assertions[] = 'gt:'.$schema['exclusiveMinimum'];
95+
}
96+
97+
if (isset($schema['exclusiveMaximum'])) {
98+
$assertions[] = 'lt:'.$schema['exclusiveMaximum'];
99+
}
100+
101+
if (isset($schema['minimum'])) {
102+
$assertions[] = 'gte:'.$schema['minimum'];
103+
}
104+
105+
if (isset($schema['maximum'])) {
106+
$assertions[] = 'lte:'.$schema['maximum'];
107+
}
108+
109+
if (isset($schema['pattern'])) {
110+
$assertions[] = 'regex:'.$schema['pattern'];
111+
}
112+
113+
$minLength = isset($schema['minLength']);
114+
$maxLength = isset($schema['maxLength']);
115+
116+
if ($minLength && $maxLength) {
117+
$assertions[] = \sprintf('between:%s,%s', $schema['minLength'], $schema['maxLength']);
118+
} elseif ($minLength) {
119+
$assertions[] = 'min:'.$schema['minLength'];
120+
} elseif ($maxLength) {
121+
$assertions[] = 'max:'.$schema['maxLength'];
122+
}
123+
124+
$minItems = isset($schema['minItems']);
125+
$maxItems = isset($schema['maxItems']);
126+
127+
if ($minItems && $maxItems) {
128+
$assertions[] = \sprintf('between:%s,%s', $schema['minItems'], $schema['maxItems']);
129+
} elseif ($minItems) {
130+
$assertions[] = 'min:'.$schema['minItems'];
131+
} elseif ($maxItems) {
132+
$assertions[] = 'max:'.$schema['maxItems'];
133+
}
134+
135+
if (isset($schema['multipleOf'])) {
136+
$assertions[] = 'multiple_of:'.$schema['multipleOf'];
137+
}
138+
139+
// if (isset($schema['enum'])) {
140+
// $assertions[] = [Rule::enum($schema['enum'])];
141+
// }
142+
143+
if (isset($schema['type']) && 'array' === $schema['type']) {
144+
$assertions[] = 'array';
145+
}
146+
147+
if (!$assertions) {
148+
return $parameter;
149+
}
150+
151+
if (1 === \count($assertions)) {
152+
return $parameter->withConstraints($assertions[0]);
153+
}
154+
155+
return $parameter->withConstraints($assertions);
156+
}
157+
}

0 commit comments

Comments
 (0)