Skip to content

Commit e867d07

Browse files
authored
feat(parametervalidator): parameter validation (#6296)
1 parent dfa1b13 commit e867d07

File tree

18 files changed

+358
-14
lines changed

18 files changed

+358
-14
lines changed

src/Metadata/Parameter.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
use ApiPlatform\OpenApi;
1717
use ApiPlatform\State\ProviderInterface;
18+
use Symfony\Component\Validator\Constraint;
1819

1920
/**
2021
* @experimental
@@ -26,6 +27,7 @@ abstract class Parameter
2627
* @param array<string, mixed> $extraProperties
2728
* @param ProviderInterface|callable|string|null $provider
2829
* @param FilterInterface|string|null $filter
30+
* @param Constraint|Constraint[]|null $constraints
2931
*/
3032
public function __construct(
3133
protected ?string $key = null,
@@ -37,6 +39,7 @@ public function __construct(
3739
protected ?string $description = null,
3840
protected ?bool $required = null,
3941
protected ?int $priority = null,
42+
protected Constraint|array|null $constraints = null,
4043
protected ?array $extraProperties = [],
4144
) {
4245
}
@@ -89,6 +92,14 @@ public function getPriority(): ?int
8992
return $this->priority;
9093
}
9194

95+
/**
96+
* @return Constraint|Constraint[]|null
97+
*/
98+
public function getConstraints(): Constraint|array|null
99+
{
100+
return $this->constraints;
101+
}
102+
92103
/**
93104
* @return array<string, mixed>
94105
*/
@@ -178,6 +189,14 @@ public function withRequired(bool $required): static
178189
return $self;
179190
}
180191

192+
public function withConstraints(array|Constraint $constraints): static
193+
{
194+
$self = clone $this;
195+
$self->constraints = $constraints;
196+
197+
return $self;
198+
}
199+
181200
/**
182201
* @param array<string, mixed> $extraProperties
183202
*/

src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php

Lines changed: 98 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,22 @@
1818
use ApiPlatform\Metadata\Parameter;
1919
use ApiPlatform\Metadata\Parameters;
2020
use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
21-
use ApiPlatform\OpenApi;
21+
use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter;
2222
use ApiPlatform\Serializer\Filter\FilterInterface as SerializerFilterInterface;
2323
use Psr\Container\ContainerInterface;
24+
use Symfony\Component\Validator\Constraints\Choice;
25+
use Symfony\Component\Validator\Constraints\Count;
26+
use Symfony\Component\Validator\Constraints\DivisibleBy;
27+
use Symfony\Component\Validator\Constraints\GreaterThan;
28+
use Symfony\Component\Validator\Constraints\GreaterThanOrEqual;
29+
use Symfony\Component\Validator\Constraints\Length;
30+
use Symfony\Component\Validator\Constraints\LessThan;
31+
use Symfony\Component\Validator\Constraints\LessThanOrEqual;
32+
use Symfony\Component\Validator\Constraints\NotBlank;
33+
use Symfony\Component\Validator\Constraints\NotNull;
34+
use Symfony\Component\Validator\Constraints\Regex;
35+
use Symfony\Component\Validator\Constraints\Unique;
36+
use Symfony\Component\Validator\Validator\ValidatorInterface;
2437

2538
/**
2639
* Prepares Parameters documentation by reading its filter details and declaring an OpenApi parameter.
@@ -96,20 +109,28 @@ private function setDefaults(string $key, Parameter $parameter, string $resource
96109
$parameter = $parameter->withSchema($schema);
97110
}
98111

112+
if (null === $parameter->getProperty() && ($property = $description[$key]['property'] ?? null)) {
113+
$parameter = $parameter->withProperty($property);
114+
}
115+
116+
if (null === $parameter->getRequired() && ($required = $description[$key]['required'] ?? null)) {
117+
$parameter = $parameter->withRequired($required);
118+
}
119+
99120
if (null === $parameter->getOpenApi() && $openApi = $description[$key]['openapi'] ?? null) {
100-
if ($openApi instanceof OpenApi\Model\Parameter) {
121+
if ($openApi instanceof OpenApiParameter) {
101122
$parameter = $parameter->withOpenApi($openApi);
102-
}
103-
104-
if (\is_array($openApi)) {
105-
$parameter = $parameter->withOpenApi(new OpenApi\Model\Parameter(
123+
} elseif (\is_array($openApi)) {
124+
// @phpstan-ignore-next-line
125+
$schema = $schema ?? $openapi['schema'] ?? [];
126+
$parameter = $parameter->withOpenApi(new OpenApiParameter(
106127
$key,
107128
$parameter instanceof HeaderParameterInterface ? 'header' : 'query',
108129
$description[$key]['description'] ?? '',
109130
$description[$key]['required'] ?? $openApi['required'] ?? false,
110131
$openApi['deprecated'] ?? false,
111132
$openApi['allowEmptyValue'] ?? true,
112-
$schema ?? $openApi['schema'] ?? [],
133+
$schema,
113134
$openApi['style'] ?? null,
114135
$openApi['explode'] ?? ('array' === ($schema['type'] ?? null)),
115136
$openApi['allowReserved'] ?? false,
@@ -121,6 +142,76 @@ private function setDefaults(string $key, Parameter $parameter, string $resource
121142
}
122143
}
123144

145+
$schema = $parameter->getSchema() ?? $parameter->getOpenApi()?->getSchema();
146+
147+
// Only add validation if the Symfony Validator is installed
148+
if (interface_exists(ValidatorInterface::class) && !$parameter->getConstraints()) {
149+
$parameter = $this->addSchemaValidation($parameter, $schema, $parameter->getRequired() ?? $description['required'] ?? false, $parameter->getOpenApi());
150+
}
151+
124152
return $parameter;
125153
}
154+
155+
private function addSchemaValidation(Parameter $parameter, ?array $schema = null, bool $required = false, ?OpenApiParameter $openApi = null): Parameter
156+
{
157+
$assertions = [];
158+
159+
if ($required) {
160+
$assertions[] = new NotNull(message: sprintf('The parameter "%s" is required.', $parameter->getKey()));
161+
}
162+
163+
if (isset($schema['exclusiveMinimum'])) {
164+
$assertions[] = new GreaterThan(value: $schema['exclusiveMinimum']);
165+
}
166+
167+
if (isset($schema['exclusiveMaximum'])) {
168+
$assertions[] = new LessThan(value: $schema['exclusiveMaximum']);
169+
}
170+
171+
if (isset($schema['minimum'])) {
172+
$assertions[] = new GreaterThanOrEqual(value: $schema['minimum']);
173+
}
174+
175+
if (isset($schema['maximum'])) {
176+
$assertions[] = new LessThanOrEqual(value: $schema['maximum']);
177+
}
178+
179+
if (isset($schema['pattern'])) {
180+
$assertions[] = new Regex($schema['pattern']);
181+
}
182+
183+
if (isset($schema['maxLength']) || isset($schema['minLength'])) {
184+
$assertions[] = new Length(min: $schema['minLength'] ?? null, max: $schema['maxLength'] ?? null);
185+
}
186+
187+
if (isset($schema['minItems']) || isset($schema['maxItems'])) {
188+
$assertions[] = new Count(min: $schema['minItems'] ?? null, max: $schema['maxItems'] ?? null);
189+
}
190+
191+
if (isset($schema['multipleOf'])) {
192+
$assertions[] = new DivisibleBy(value: $schema['multipleOf']);
193+
}
194+
195+
if ($schema['uniqueItems'] ?? false) {
196+
$assertions[] = new Unique();
197+
}
198+
199+
if (isset($schema['enum'])) {
200+
$assertions[] = new Choice(choices: $schema['enum']);
201+
}
202+
203+
if (false === $openApi?->getAllowEmptyValue()) {
204+
$assertions[] = new NotBlank(allowNull: !$required);
205+
}
206+
207+
if (!$assertions) {
208+
return $parameter;
209+
}
210+
211+
if (1 === \count($assertions)) {
212+
return $parameter->withConstraints($assertions[0]);
213+
}
214+
215+
return $parameter->withConstraints($assertions);
216+
}
126217
}

src/ParameterValidator/Validator/ArrayItems.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313

1414
namespace ApiPlatform\ParameterValidator\Validator;
1515

16+
/**
17+
* @deprecated use Parameter constraint instead
18+
*/
1619
final class ArrayItems implements ValidatorInterface
1720
{
1821
use CheckFilterDeprecationsTrait;

src/ParameterValidator/Validator/Bounds.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313

1414
namespace ApiPlatform\ParameterValidator\Validator;
1515

16+
/**
17+
* @deprecated use Parameter constraint instead
18+
*/
1619
final class Bounds implements ValidatorInterface
1720
{
1821
use CheckFilterDeprecationsTrait;

src/ParameterValidator/Validator/Enum.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313

1414
namespace ApiPlatform\ParameterValidator\Validator;
1515

16+
/**
17+
* @deprecated use Parameter constraint instead
18+
*/
1619
final class Enum implements ValidatorInterface
1720
{
1821
use CheckFilterDeprecationsTrait;

src/ParameterValidator/Validator/Length.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313

1414
namespace ApiPlatform\ParameterValidator\Validator;
1515

16+
/**
17+
* @deprecated use Parameter constraint instead
18+
*/
1619
final class Length implements ValidatorInterface
1720
{
1821
use CheckFilterDeprecationsTrait;

src/ParameterValidator/Validator/MultipleOf.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313

1414
namespace ApiPlatform\ParameterValidator\Validator;
1515

16+
/**
17+
* @deprecated use Parameter constraint instead
18+
*/
1619
final class MultipleOf implements ValidatorInterface
1720
{
1821
use CheckFilterDeprecationsTrait;

src/ParameterValidator/Validator/Pattern.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313

1414
namespace ApiPlatform\ParameterValidator\Validator;
1515

16+
/**
17+
* @deprecated use Parameter constraint instead
18+
*/
1619
final class Pattern implements ValidatorInterface
1720
{
1821
use CheckFilterDeprecationsTrait;

src/ParameterValidator/Validator/Required.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515

1616
use ApiPlatform\State\Util\RequestParser;
1717

18+
/**
19+
* @deprecated use Parameter constraint instead
20+
*/
1821
final class Required implements ValidatorInterface
1922
{
2023
use CheckFilterDeprecationsTrait;

src/ParameterValidator/Validator/ValidatorInterface.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313

1414
namespace ApiPlatform\ParameterValidator\Validator;
1515

16+
/**
17+
* @deprecated use Parameter constraint instead
18+
*/
1619
interface ValidatorInterface
1720
{
1821
/**

src/State/Provider/ParameterProvider.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ public function __construct(private readonly ?ProviderInterface $decorated = nul
4040
public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
4141
{
4242
$request = $context['request'] ?? null;
43-
4443
if ($request && null === $request->attributes->get('_api_query_parameters')) {
4544
$queryString = RequestParser::getQueryString($request);
4645
$request->attributes->set('_api_query_parameters', $queryString ? RequestParser::parseRequestParams($queryString) : []);
@@ -57,6 +56,7 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
5756
$key = $parameter->getKey();
5857
$parameters = $this->extractParameterValues($parameter, $request, $context);
5958
$parsedKey = explode('[:property]', $key);
59+
6060
if (isset($parsedKey[0]) && isset($parameters[$parsedKey[0]])) {
6161
$key = $parsedKey[0];
6262
}

src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -819,6 +819,7 @@ private function registerValidatorConfiguration(ContainerBuilder $container, arr
819819
$container->setParameter('api_platform.validator.legacy_validation_exception', $config['validator']['legacy_validation_exception'] ?? true);
820820
$loader->load('metadata/validator.xml');
821821
$loader->load('validator/validator.xml');
822+
$loader->load('symfony/parameter_validator.xml');
822823

823824
if ($this->isConfigEnabled($container, $config['graphql'])) {
824825
$loader->load('graphql/validator.xml');
@@ -846,6 +847,7 @@ private function registerValidatorConfiguration(ContainerBuilder $container, arr
846847
if (!$config['validator']['query_parameter_validation']) {
847848
$container->removeDefinition('api_platform.listener.view.validate_query_parameters');
848849
$container->removeDefinition('api_platform.validator.query_parameter_validator');
850+
$container->removeDefinition('api_platform.symfony.parameter_validator');
849851
}
850852
}
851853

src/Symfony/Bundle/Resources/config/state/provider.xml

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,6 @@
2525
<argument type="service" id="translator" on-invalid="null" />
2626
</service>
2727

28-
<service id="api_platform.state_provider.parameter" class="ApiPlatform\State\Provider\ParameterProvider" decorates="api_platform.state_provider.main" decoration-priority="300">
29-
<argument type="service" id="api_platform.state_provider.parameter.inner" />
30-
<argument type="tagged_locator" tag="api_platform.parameter_provider" index-by="key" />
31-
</service>
32-
3328
<service id="api_platform.error_listener" class="ApiPlatform\Symfony\EventListener\ErrorListener">
3429
<argument key="$controller">api_platform.symfony.main_controller</argument>
3530
<argument key="$logger" type="service" id="logger" on-invalid="null" />

src/Symfony/Bundle/Resources/config/state/state.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,5 +54,10 @@
5454
<tag name="api_platform.state_provider" key="api_platform.state_provider.object" />
5555
</service>
5656
<service id="ApiPlatform\State\ObjectProvider" alias="api_platform.state_provider.object" />
57+
58+
<service id="api_platform.state_provider.parameter" class="ApiPlatform\State\Provider\ParameterProvider" decorates="api_platform.state_provider.read" decoration-priority="300">
59+
<argument type="service" id="api_platform.state_provider.parameter.inner" />
60+
<argument type="tagged_locator" tag="api_platform.parameter_provider" index-by="key" />
61+
</service>
5762
</services>
5863
</container>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?xml version="1.0" ?>
2+
<container xmlns="http://symfony.com/schema/dic/services"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
5+
<services>
6+
<service id="api_platform.symfony.parameter_validator" class="ApiPlatform\Symfony\Validator\State\ParameterValidatorProvider" public="true" decorates="api_platform.state_provider.parameter">
7+
<argument type="service" id="api_platform.symfony.parameter_validator.inner" />
8+
<argument type="service" id="validator" />
9+
</service>
10+
</services>
11+
</container>

0 commit comments

Comments
 (0)