Skip to content

Commit

Permalink
feat(parametervalidator): parameter validation
Browse files Browse the repository at this point in the history
  • Loading branch information
soyuka committed Apr 29, 2024
1 parent dfa1b13 commit 35bbb96
Show file tree
Hide file tree
Showing 18 changed files with 358 additions and 14 deletions.
19 changes: 19 additions & 0 deletions src/Metadata/Parameter.php
Expand Up @@ -15,6 +15,7 @@

use ApiPlatform\OpenApi;
use ApiPlatform\State\ProviderInterface;
use Symfony\Component\Validator\Constraint;

/**
* @experimental
Expand All @@ -26,6 +27,7 @@ abstract class Parameter
* @param array<string, mixed> $extraProperties
* @param ProviderInterface|callable|string|null $provider
* @param FilterInterface|string|null $filter
* @param Constraint|Constraint[]|null $constraints
*/
public function __construct(
protected ?string $key = null,
Expand All @@ -37,6 +39,7 @@ public function __construct(
protected ?string $description = null,
protected ?bool $required = null,
protected ?int $priority = null,
protected Constraint|array|null $constraints = null,
protected ?array $extraProperties = [],
) {
}
Expand Down Expand Up @@ -89,6 +92,14 @@ public function getPriority(): ?int
return $this->priority;
}

/**
* @return Constraint|Constraint[]|null
*/
public function getConstraints(): Constraint|array|null
{
return $this->constraints;
}

/**
* @return array<string, mixed>
*/
Expand Down Expand Up @@ -178,6 +189,14 @@ public function withRequired(bool $required): static
return $self;
}

public function withConstraints(array|Constraint $constraints): static
{
$self = clone $this;
$self->constraints = $constraints;

return $self;
}

/**
* @param array<string, mixed> $extraProperties
*/
Expand Down
Expand Up @@ -18,9 +18,22 @@
use ApiPlatform\Metadata\Parameter;
use ApiPlatform\Metadata\Parameters;
use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
use ApiPlatform\OpenApi;
use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter;
use ApiPlatform\Serializer\Filter\FilterInterface as SerializerFilterInterface;
use Psr\Container\ContainerInterface;
use Symfony\Component\Validator\Constraints\Choice;
use Symfony\Component\Validator\Constraints\Count;
use Symfony\Component\Validator\Constraints\DivisibleBy;
use Symfony\Component\Validator\Constraints\GreaterThan;
use Symfony\Component\Validator\Constraints\GreaterThanOrEqual;
use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\LessThan;
use Symfony\Component\Validator\Constraints\LessThanOrEqual;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\NotNull;
use Symfony\Component\Validator\Constraints\Regex;
use Symfony\Component\Validator\Constraints\Unique;
use Symfony\Component\Validator\Validator\ValidatorInterface;

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

if (null === $parameter->getProperty() && ($property = $description[$key]['property'] ?? null)) {
$parameter = $parameter->withProperty($property);
}

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

if (null === $parameter->getOpenApi() && $openApi = $description[$key]['openapi'] ?? null) {
if ($openApi instanceof OpenApi\Model\Parameter) {
if ($openApi instanceof OpenApiParameter) {
$parameter = $parameter->withOpenApi($openApi);
}

if (\is_array($openApi)) {
$parameter = $parameter->withOpenApi(new OpenApi\Model\Parameter(
} elseif (\is_array($openApi)) {
// @phpstan-ignore-next-line
$schema = $schema ?? $openapi['schema'] ?? [];
$parameter = $parameter->withOpenApi(new OpenApiParameter(
$key,
$parameter instanceof HeaderParameterInterface ? 'header' : 'query',
$description[$key]['description'] ?? '',
$description[$key]['required'] ?? $openApi['required'] ?? false,
$openApi['deprecated'] ?? false,
$openApi['allowEmptyValue'] ?? true,
$schema ?? $openApi['schema'] ?? [],
$schema,
$openApi['style'] ?? null,
$openApi['explode'] ?? ('array' === ($schema['type'] ?? null)),
$openApi['allowReserved'] ?? false,
Expand All @@ -121,6 +142,76 @@ private function setDefaults(string $key, Parameter $parameter, string $resource
}
}

$schema = $parameter->getSchema() ?? $parameter->getOpenApi()?->getSchema();

// Only add validation if the Symfony Validator is installed
if (interface_exists(ValidatorInterface::class) && !$parameter->getConstraints()) {
$parameter = $this->addSchemaValidation($parameter, $schema, $parameter->getRequired() ?? $description['required'] ?? false, $parameter->getOpenApi());
}

return $parameter;
}

private function addSchemaValidation(Parameter $parameter, ?array $schema = null, bool $required = false, ?OpenApiParameter $openApi = null): Parameter
{
$assertions = [];

if ($required) {
$assertions[] = new NotNull(message: sprintf('The parameter "%s" is required.', $parameter->getKey()));
}

if (isset($schema['exclusiveMinimum'])) {
$assertions[] = new GreaterThan(value: $schema['exclusiveMinimum']);
}

if (isset($schema['exclusiveMaximum'])) {
$assertions[] = new LessThan(value: $schema['exclusiveMaximum']);
}

if (isset($schema['minimum'])) {
$assertions[] = new GreaterThanOrEqual(value: $schema['minimum']);
}

if (isset($schema['maximum'])) {
$assertions[] = new LessThanOrEqual(value: $schema['maximum']);
}

if (isset($schema['pattern'])) {
$assertions[] = new Regex($schema['pattern']);
}

if (isset($schema['maxLength']) || isset($schema['minLength'])) {
$assertions[] = new Length(min: $schema['minLength'] ?? null, max: $schema['maxLength'] ?? null);
}

if (isset($schema['minItems']) || isset($schema['maxItems'])) {
$assertions[] = new Count(min: $schema['minItems'] ?? null, max: $schema['maxItems'] ?? null);
}

if (isset($schema['multipleOf'])) {
$assertions[] = new DivisibleBy(value: $schema['multipleOf']);
}

if ($schema['uniqueItems'] ?? false) {
$assertions[] = new Unique();
}

if (isset($schema['enum'])) {
$assertions[] = new Choice(choices: $schema['enum']);
}

if (false === $openApi?->getAllowEmptyValue()) {
$assertions[] = new NotBlank(allowNull: !$required);
}

if (!$assertions) {
return $parameter;
}

if (1 === \count($assertions)) {
return $parameter->withConstraints($assertions[0]);
}

return $parameter->withConstraints($assertions);
}
}
3 changes: 3 additions & 0 deletions src/ParameterValidator/Validator/ArrayItems.php
Expand Up @@ -13,6 +13,9 @@

namespace ApiPlatform\ParameterValidator\Validator;

/**
* @deprecated use Parameter constraint instead
*/
final class ArrayItems implements ValidatorInterface
{
use CheckFilterDeprecationsTrait;
Expand Down
3 changes: 3 additions & 0 deletions src/ParameterValidator/Validator/Bounds.php
Expand Up @@ -13,6 +13,9 @@

namespace ApiPlatform\ParameterValidator\Validator;

/**
* @deprecated use Parameter constraint instead
*/
final class Bounds implements ValidatorInterface
{
use CheckFilterDeprecationsTrait;
Expand Down
3 changes: 3 additions & 0 deletions src/ParameterValidator/Validator/Enum.php
Expand Up @@ -13,6 +13,9 @@

namespace ApiPlatform\ParameterValidator\Validator;

/**
* @deprecated use Parameter constraint instead
*/
final class Enum implements ValidatorInterface
{
use CheckFilterDeprecationsTrait;
Expand Down
3 changes: 3 additions & 0 deletions src/ParameterValidator/Validator/Length.php
Expand Up @@ -13,6 +13,9 @@

namespace ApiPlatform\ParameterValidator\Validator;

/**
* @deprecated use Parameter constraint instead
*/
final class Length implements ValidatorInterface
{
use CheckFilterDeprecationsTrait;
Expand Down
3 changes: 3 additions & 0 deletions src/ParameterValidator/Validator/MultipleOf.php
Expand Up @@ -13,6 +13,9 @@

namespace ApiPlatform\ParameterValidator\Validator;

/**
* @deprecated use Parameter constraint instead
*/
final class MultipleOf implements ValidatorInterface
{
use CheckFilterDeprecationsTrait;
Expand Down
3 changes: 3 additions & 0 deletions src/ParameterValidator/Validator/Pattern.php
Expand Up @@ -13,6 +13,9 @@

namespace ApiPlatform\ParameterValidator\Validator;

/**
* @deprecated use Parameter constraint instead
*/
final class Pattern implements ValidatorInterface
{
use CheckFilterDeprecationsTrait;
Expand Down
3 changes: 3 additions & 0 deletions src/ParameterValidator/Validator/Required.php
Expand Up @@ -15,6 +15,9 @@

use ApiPlatform\State\Util\RequestParser;

/**
* @deprecated use Parameter constraint instead
*/
final class Required implements ValidatorInterface
{
use CheckFilterDeprecationsTrait;
Expand Down
3 changes: 3 additions & 0 deletions src/ParameterValidator/Validator/ValidatorInterface.php
Expand Up @@ -13,6 +13,9 @@

namespace ApiPlatform\ParameterValidator\Validator;

/**
* @deprecated use Parameter constraint instead
*/
interface ValidatorInterface
{
/**
Expand Down
2 changes: 1 addition & 1 deletion src/State/Provider/ParameterProvider.php
Expand Up @@ -40,7 +40,6 @@ public function __construct(private readonly ?ProviderInterface $decorated = nul
public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
{
$request = $context['request'] ?? null;

if ($request && null === $request->attributes->get('_api_query_parameters')) {
$queryString = RequestParser::getQueryString($request);
$request->attributes->set('_api_query_parameters', $queryString ? RequestParser::parseRequestParams($queryString) : []);
Expand All @@ -57,6 +56,7 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
$key = $parameter->getKey();
$parameters = $this->extractParameterValues($parameter, $request, $context);
$parsedKey = explode('[:property]', $key);

if (isset($parsedKey[0]) && isset($parameters[$parsedKey[0]])) {
$key = $parsedKey[0];
}
Expand Down
Expand Up @@ -819,6 +819,7 @@ private function registerValidatorConfiguration(ContainerBuilder $container, arr
$container->setParameter('api_platform.validator.legacy_validation_exception', $config['validator']['legacy_validation_exception'] ?? true);
$loader->load('metadata/validator.xml');
$loader->load('validator/validator.xml');
$loader->load('symfony/parameter_validator.xml');

if ($this->isConfigEnabled($container, $config['graphql'])) {
$loader->load('graphql/validator.xml');
Expand Down Expand Up @@ -846,6 +847,7 @@ private function registerValidatorConfiguration(ContainerBuilder $container, arr
if (!$config['validator']['query_parameter_validation']) {
$container->removeDefinition('api_platform.listener.view.validate_query_parameters');
$container->removeDefinition('api_platform.validator.query_parameter_validator');
$container->removeDefinition('api_platform.symfony.parameter_validator');
}
}

Expand Down
5 changes: 0 additions & 5 deletions src/Symfony/Bundle/Resources/config/state/provider.xml
Expand Up @@ -25,11 +25,6 @@
<argument type="service" id="translator" on-invalid="null" />
</service>

<service id="api_platform.state_provider.parameter" class="ApiPlatform\State\Provider\ParameterProvider" decorates="api_platform.state_provider.main" decoration-priority="300">
<argument type="service" id="api_platform.state_provider.parameter.inner" />
<argument type="tagged_locator" tag="api_platform.parameter_provider" index-by="key" />
</service>

<service id="api_platform.error_listener" class="ApiPlatform\Symfony\EventListener\ErrorListener">
<argument key="$controller">api_platform.symfony.main_controller</argument>
<argument key="$logger" type="service" id="logger" on-invalid="null" />
Expand Down
5 changes: 5 additions & 0 deletions src/Symfony/Bundle/Resources/config/state/state.xml
Expand Up @@ -54,5 +54,10 @@
<tag name="api_platform.state_provider" key="api_platform.state_provider.object" />
</service>
<service id="ApiPlatform\State\ObjectProvider" alias="api_platform.state_provider.object" />

<service id="api_platform.state_provider.parameter" class="ApiPlatform\State\Provider\ParameterProvider" decorates="api_platform.state_provider.read" decoration-priority="300">
<argument type="service" id="api_platform.state_provider.parameter.inner" />
<argument type="tagged_locator" tag="api_platform.parameter_provider" index-by="key" />
</service>
</services>
</container>
@@ -0,0 +1,11 @@
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="api_platform.symfony.parameter_validator" class="ApiPlatform\Symfony\Validator\State\ParameterValidatorProvider" public="true" decorates="api_platform.state_provider.parameter">
<argument type="service" id="api_platform.symfony.parameter_validator.inner" />
<argument type="service" id="validator" />
</service>
</services>
</container>

0 comments on commit 35bbb96

Please sign in to comment.