From 0ebb1f04735ddc94b2654c1359df275e3029f3ab Mon Sep 17 00:00:00 2001 From: soyuka Date: Wed, 7 May 2025 22:38:03 +0200 Subject: [PATCH] refactor(metadata): type parameters to list|string --- composer.json | 1 + .../Extension/FilterQueryExtension.php | 5 ++ .../JsonApi/SortFilterParameterProvider.php | 6 ++ src/Metadata/Parameter.php | 16 ++++ ...meterResourceMetadataCollectionFactory.php | 11 +++ src/Metadata/composer.json | 2 +- src/State/Util/ParameterParserTrait.php | 31 +++++-- .../State/ParameterValidatorProvider.php | 11 +-- ...ationResourceMetadataCollectionFactory.php | 80 +++++++++++++------ src/Validator/composer.json | 1 + .../Issue6673/MutlipleParameterProvider.php | 4 + .../TestBundle/ApiResource/WithParameter.php | 70 ++++++++++++++-- .../ApiResource/WithSecurityParameter.php | 6 +- .../Document/FilteredBooleanParameter.php | 4 + .../Document/FilteredExistsParameter.php | 4 + .../Document/FilteredOrderParameter.php | 6 ++ .../Entity/FilteredBooleanParameter.php | 4 + .../Entity/FilteredExistsParameter.php | 4 + .../Entity/FilteredOrderParameter.php | 6 ++ tests/Functional/Parameters/ParameterTest.php | 18 ++++- .../Functional/Parameters/ValidationTest.php | 9 ++- 21 files changed, 246 insertions(+), 53 deletions(-) diff --git a/composer.json b/composer.json index 88f98e46044..b85f01b0023 100644 --- a/composer.json +++ b/composer.json @@ -114,6 +114,7 @@ "symfony/property-info": "^6.4 || ^7.1", "symfony/serializer": "^6.4 || ^7.0", "symfony/translation-contracts": "^3.3", + "symfony/type-info": "^7.2", "symfony/web-link": "^6.4 || ^7.1", "willdurand/negotiation": "^3.1" }, diff --git a/src/Laravel/Eloquent/Extension/FilterQueryExtension.php b/src/Laravel/Eloquent/Extension/FilterQueryExtension.php index dc91528cbe5..c28ceaaaf4c 100644 --- a/src/Laravel/Eloquent/Extension/FilterQueryExtension.php +++ b/src/Laravel/Eloquent/Extension/FilterQueryExtension.php @@ -53,6 +53,11 @@ public function apply(Builder $builder, array $uriVariables, Operation $operatio continue; } + // most eloquent filters work with only a single value + if (\is_array($values) && array_is_list($values) && 1 === \count($values)) { + $values = current($values); + } + $filter = $filterId instanceof FilterInterface ? $filterId : ($this->filterLocator->has($filterId) ? $this->filterLocator->get($filterId) : null); if ($filter instanceof FilterInterface) { $builder = $filter->apply($builder, $values, $parameter, $context + ($parameter->getFilterContext() ?? [])); diff --git a/src/Laravel/Eloquent/Filter/JsonApi/SortFilterParameterProvider.php b/src/Laravel/Eloquent/Filter/JsonApi/SortFilterParameterProvider.php index c6ffd33f922..afa2e10674f 100644 --- a/src/Laravel/Eloquent/Filter/JsonApi/SortFilterParameterProvider.php +++ b/src/Laravel/Eloquent/Filter/JsonApi/SortFilterParameterProvider.php @@ -28,6 +28,12 @@ public function provide(Parameter $parameter, array $parameters = [], array $con $parameters = $operation->getParameters(); $properties = $parameter->getExtraProperties()['_properties'] ?? []; $value = $parameter->getValue(); + + // most eloquent filters work with only a single value + if (\is_array($value) && array_is_list($value) && 1 === \count($value)) { + $value = current($value); + } + if (!\is_string($value)) { return $operation; } diff --git a/src/Metadata/Parameter.php b/src/Metadata/Parameter.php index 1acef3d8bce..6fdd9ff98f3 100644 --- a/src/Metadata/Parameter.php +++ b/src/Metadata/Parameter.php @@ -16,6 +16,7 @@ use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter; use ApiPlatform\State\ParameterNotFound; use ApiPlatform\State\ParameterProviderInterface; +use Symfony\Component\TypeInfo\Type; /** * @experimental @@ -29,6 +30,7 @@ abstract class Parameter * @param list $properties a list of properties this parameter applies to (works with the :property placeholder) * @param FilterInterface|string|null $filter * @param mixed $constraints an array of Symfony constraints, or an array of Laravel rules + * @param Type $nativeType the PHP native type, we cast values to an array if its a CollectionType, if not and it's an array with a single value we use it (eg: HTTP Header) */ public function __construct( protected ?string $key = null, @@ -47,6 +49,7 @@ public function __construct( protected ?string $securityMessage = null, protected ?array $extraProperties = [], protected array|string|null $filterContext = null, + protected ?Type $nativeType = null, ) { } @@ -296,4 +299,17 @@ public function withProperties(?array $properties): self return $self; } + + public function getNativeType(): ?Type + { + return $this->nativeType; + } + + public function withNativeType(Type $nativeType): self + { + $self = clone $this; + $self->nativeType = $nativeType; + + return $self; + } } diff --git a/src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php index d4a6416f79d..7675487762f 100644 --- a/src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php +++ b/src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php @@ -32,6 +32,7 @@ use Psr\Container\ContainerInterface; use Psr\Log\LoggerInterface; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; +use Symfony\Component\TypeInfo\Type; /** * Prepares Parameters documentation by reading its filter details and declaring an OpenApi parameter. @@ -148,6 +149,16 @@ private function getDefaultParameters(Operation $operation, string $resourceClas } $parameter = $this->setDefaults($key, $parameter, $resourceClass, $properties, $operation); + // We don't do any type cast yet, a query parameter or an header is always a string or a list of strings + if (null === $parameter->getNativeType()) { + // this forces the type to be only a list + if ('array' === ($parameter->getSchema()['type'] ?? null)) { + $parameter = $parameter->withNativeType(Type::list(Type::string())); + } else { + $parameter = $parameter->withNativeType(Type::union(Type::string(), Type::list(Type::string()))); + } + } + $priority = $parameter->getPriority() ?? $internalPriority--; $parameters->add($key, $parameter->withPriority($priority)); } diff --git a/src/Metadata/composer.json b/src/Metadata/composer.json index 63ae4d7a419..5366e43c1af 100644 --- a/src/Metadata/composer.json +++ b/src/Metadata/composer.json @@ -33,7 +33,7 @@ "psr/log": "^1.0 || ^2.0 || ^3.0", "symfony/property-info": "^6.4 || ^7.1", "symfony/string": "^6.4 || ^7.0", - "symfony/type-info": "^7.1" + "symfony/type-info": "^7.2" }, "require-dev": { "api-platform/json-schema": "^4.1", diff --git a/src/State/Util/ParameterParserTrait.php b/src/State/Util/ParameterParserTrait.php index 4d67397c39e..05282313498 100644 --- a/src/State/Util/ParameterParserTrait.php +++ b/src/State/Util/ParameterParserTrait.php @@ -13,10 +13,13 @@ namespace ApiPlatform\State\Util; +use ApiPlatform\Metadata\HeaderParameter; use ApiPlatform\Metadata\HeaderParameterInterface; use ApiPlatform\Metadata\Parameter; use ApiPlatform\State\ParameterNotFound; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\TypeInfo\Type\CollectionType; +use Symfony\Component\TypeInfo\Type\UnionType; /** * @internal @@ -64,11 +67,7 @@ private function extractParameterValues(Parameter $parameter, array $values): st } $value = $values[$key] ?? new ParameterNotFound(); - if (!$accessors) { - return $value; - } - - foreach ($accessors as $accessor) { + foreach ($accessors ?? [] as $accessor) { if (\is_array($value) && isset($value[$accessor])) { $value = $value[$accessor]; } else { @@ -77,6 +76,28 @@ private function extractParameterValues(Parameter $parameter, array $values): st } } + if ($value instanceof ParameterNotFound) { + return $value; + } + + $isCollectionType = fn ($t) => $t instanceof CollectionType; + $isCollection = $parameter->getNativeType()?->isSatisfiedBy($isCollectionType) ?? false; + + // type-info 7.2 + if (!$isCollection && $parameter->getNativeType() instanceof UnionType) { + foreach ($parameter->getNativeType()->getTypes() as $t) { + if ($isCollection = $t->isSatisfiedBy($isCollectionType)) { + break; + } + } + } + + if ($isCollection) { + $value = \is_array($value) ? $value : [$value]; + } elseif ($parameter instanceof HeaderParameter && \is_array($value) && array_is_list($value) && 1 === \count($value)) { + $value = $value[0]; + } + return $value; } } diff --git a/src/Symfony/Validator/State/ParameterValidatorProvider.php b/src/Symfony/Validator/State/ParameterValidatorProvider.php index 85e702380fb..741c4292edb 100644 --- a/src/Symfony/Validator/State/ParameterValidatorProvider.php +++ b/src/Symfony/Validator/State/ParameterValidatorProvider.php @@ -62,14 +62,7 @@ public function provide(Operation $operation, array $uriVariables = [], array $c $value = null; } - $violations = []; - if (\is_array($value) && $properties = $parameter->getExtraProperties()['_properties'] ?? []) { - foreach ($properties as $property) { - $violations = [...$violations, ...$this->validator->validate($value[$property] ?? null, $constraints)]; - } - } else { - $violations = $this->validator->validate($value, $constraints); - } + $violations = $this->validator->validate($value, $constraints); foreach ($violations as $violation) { $constraintViolationList->add(new ConstraintViolation( @@ -108,7 +101,7 @@ private function getProperty(Parameter $parameter, ConstraintViolationInterface } if ($p = $violation->getPropertyPath()) { - return $p; + return $key.$p; } return $key; diff --git a/src/Validator/Metadata/Resource/Factory/ParameterValidationResourceMetadataCollectionFactory.php b/src/Validator/Metadata/Resource/Factory/ParameterValidationResourceMetadataCollectionFactory.php index e34124f08ec..fd6191dfe52 100644 --- a/src/Validator/Metadata/Resource/Factory/ParameterValidationResourceMetadataCollectionFactory.php +++ b/src/Validator/Metadata/Resource/Factory/ParameterValidationResourceMetadataCollectionFactory.php @@ -21,7 +21,11 @@ use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter; use Psr\Container\ContainerInterface; +use Symfony\Component\TypeInfo\Type\CollectionType; +use Symfony\Component\TypeInfo\Type\UnionType; +use Symfony\Component\Validator\Constraints\All; use Symfony\Component\Validator\Constraints\Choice; +use Symfony\Component\Validator\Constraints\Collection; use Symfony\Component\Validator\Constraints\Count; use Symfony\Component\Validator\Constraints\DivisibleBy; use Symfony\Component\Validator\Constraints\GreaterThan; @@ -31,6 +35,7 @@ use Symfony\Component\Validator\Constraints\LessThanOrEqual; use Symfony\Component\Validator\Constraints\NotBlank; use Symfony\Component\Validator\Constraints\NotNull; +use Symfony\Component\Validator\Constraints\Range; use Symfony\Component\Validator\Constraints\Regex; use Symfony\Component\Validator\Constraints\Type; use Symfony\Component\Validator\Constraints\Unique; @@ -108,55 +113,82 @@ private function addSchemaValidation(Parameter $parameter, ?array $schema = null } $assertions = []; + $allowEmptyValue = $openApi?->getAllowEmptyValue(); + if (false === ($allowEmptyValue ?? $openApi?->getAllowEmptyValue())) { + $assertions[] = new NotBlank(allowNull: !$required); + } - if ($required && false !== ($allowEmptyValue = $openApi?->getAllowEmptyValue())) { - $assertions[] = new NotNull(message: \sprintf('The parameter "%s" is required.', $parameter->getKey())); + $minimum = $schema['exclusiveMinimum'] ?? $schema['minimum'] ?? null; + $exclusiveMinimum = isset($schema['exclusiveMinimum']); + $maximum = $schema['exclusiveMaximum'] ?? $schema['maximum'] ?? null; + $exclusiveMaximum = isset($schema['exclusiveMaximum']); + + if ($minimum && $maximum) { + if (!$exclusiveMinimum && !$exclusiveMaximum) { + $assertions[] = new Range(min: $minimum, max: $maximum); + } else { + $assertions[] = $exclusiveMinimum ? new GreaterThan(value: $minimum) : new GreaterThanOrEqual(value: $minimum); + $assertions[] = $exclusiveMaximum ? new LessThan(value: $maximum) : new LessThanOrEqual(value: $maximum); + } + } elseif ($minimum) { + $assertions[] = $exclusiveMinimum ? new GreaterThan(value: $minimum) : new GreaterThanOrEqual(value: $minimum); + } elseif ($maximum) { + $assertions[] = $exclusiveMaximum ? new LessThan(value: $maximum) : new LessThanOrEqual(value: $maximum); } - if (false === ($allowEmptyValue ?? $openApi?->getAllowEmptyValue())) { - $assertions[] = new NotBlank(allowNull: !$required); + if (isset($schema['pattern'])) { + $assertions[] = new Regex('#'.$schema['pattern'].'#'); } - if (isset($schema['exclusiveMinimum'])) { - $assertions[] = new GreaterThan(value: $schema['exclusiveMinimum']); + if (isset($schema['maxLength']) || isset($schema['minLength'])) { + $assertions[] = new Length(min: $schema['minLength'] ?? null, max: $schema['maxLength'] ?? null); } - if (isset($schema['exclusiveMaximum'])) { - $assertions[] = new LessThan(value: $schema['exclusiveMaximum']); + if (isset($schema['multipleOf'])) { + $assertions[] = new DivisibleBy(value: $schema['multipleOf']); + } + + if (isset($schema['enum'])) { + $assertions[] = new Choice(choices: $schema['enum']); } - if (isset($schema['minimum'])) { - $assertions[] = new GreaterThanOrEqual(value: $schema['minimum']); + if ($properties = $parameter->getExtraProperties()['_properties'] ?? []) { + $fields = []; + foreach ($properties as $propertyName) { + $fields[$propertyName] = $assertions; + } + + return $parameter->withConstraints(new Collection(fields: $fields, allowMissingFields: true)); } - if (isset($schema['maximum'])) { - $assertions[] = new LessThanOrEqual(value: $schema['maximum']); + $isCollectionType = fn ($t) => $t instanceof CollectionType; + $isCollection = $parameter->getNativeType()?->isSatisfiedBy($isCollectionType) ?? false; + + // type-info 7.2 + if (!$isCollection && $parameter->getNativeType() instanceof UnionType) { + foreach ($parameter->getNativeType()->getTypes() as $t) { + if ($isCollection = $t->isSatisfiedBy($isCollectionType)) { + break; + } + } } - if (isset($schema['pattern'])) { - $assertions[] = new Regex('#'.$schema['pattern'].'#'); + if ($isCollection) { + $assertions = $assertions ? [new All($assertions)] : []; } - if (isset($schema['maxLength']) || isset($schema['minLength'])) { - $assertions[] = new Length(min: $schema['minLength'] ?? null, max: $schema['maxLength'] ?? null); + if ($required && false !== $allowEmptyValue) { + $assertions[] = new NotNull(message: \sprintf('The parameter "%s" is required.', $parameter->getKey())); } 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 (isset($schema['type']) && 'array' === $schema['type']) { $assertions[] = new Type(type: 'array'); } diff --git a/src/Validator/composer.json b/src/Validator/composer.json index cded96d24e3..d473960fbe1 100644 --- a/src/Validator/composer.json +++ b/src/Validator/composer.json @@ -24,6 +24,7 @@ "require": { "php": ">=8.2", "api-platform/metadata": "^4.1", + "symfony/type-info": "^7.2", "symfony/web-link": "^6.4 || ^7.1" }, "require-dev": { diff --git a/tests/Fixtures/TestBundle/ApiResource/Issue6673/MutlipleParameterProvider.php b/tests/Fixtures/TestBundle/ApiResource/Issue6673/MutlipleParameterProvider.php index 83ef0829302..71a6792ba6b 100644 --- a/tests/Fixtures/TestBundle/ApiResource/Issue6673/MutlipleParameterProvider.php +++ b/tests/Fixtures/TestBundle/ApiResource/Issue6673/MutlipleParameterProvider.php @@ -17,6 +17,8 @@ use ApiPlatform\Metadata\Operation; use ApiPlatform\Metadata\Parameter; use ApiPlatform\Metadata\QueryParameter; +use Symfony\Component\TypeInfo\Type\BuiltinType; +use Symfony\Component\TypeInfo\TypeIdentifier; #[GetCollection( uriTemplate: 'issue6673_multiple_parameter_provider', @@ -25,9 +27,11 @@ parameters: [ 'a' => new QueryParameter( provider: [self::class, 'parameterOneProvider'], + nativeType: new BuiltinType(TypeIdentifier::STRING), ), 'b' => new QueryParameter( provider: [self::class, 'parameterTwoProvider'], + nativeType: new BuiltinType(TypeIdentifier::STRING), ), ], provider: [self::class, 'provide'] diff --git a/tests/Fixtures/TestBundle/ApiResource/WithParameter.php b/tests/Fixtures/TestBundle/ApiResource/WithParameter.php index aaf74caaec0..9c29ed33e5c 100644 --- a/tests/Fixtures/TestBundle/ApiResource/WithParameter.php +++ b/tests/Fixtures/TestBundle/ApiResource/WithParameter.php @@ -27,6 +27,8 @@ use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\Serializer\Attribute\Groups; +use Symfony\Component\TypeInfo\Type\BuiltinType; +use Symfony\Component\TypeInfo\TypeIdentifier; use Symfony\Component\Validator\Constraints as Assert; #[Get( @@ -58,14 +60,37 @@ uriTemplate: 'validate_parameters{._format}', parameters: [ 'enum' => new QueryParameter(schema: ['enum' => ['a', 'b'], 'uniqueItems' => true]), - 'num' => new QueryParameter(schema: ['minimum' => 1, 'maximum' => 3]), - 'exclusiveNum' => new QueryParameter(schema: ['exclusiveMinimum' => 1, 'exclusiveMaximum' => 3]), - 'blank' => new QueryParameter(openApi: new OpenApiParameter(name: 'blank', in: 'query', allowEmptyValue: false)), - 'length' => new QueryParameter(schema: ['maxLength' => 1, 'minLength' => 3]), + 'num' => new QueryParameter( + schema: ['minimum' => 1, 'maximum' => 3], + nativeType: new BuiltinType(TypeIdentifier::STRING), + ), + 'exclusiveNum' => new QueryParameter( + schema: ['exclusiveMinimum' => 1, 'exclusiveMaximum' => 3], + nativeType: new BuiltinType(TypeIdentifier::STRING), + ), + 'blank' => new QueryParameter( + openApi: new OpenApiParameter(name: 'blank', in: 'query', allowEmptyValue: false), + nativeType: new BuiltinType(TypeIdentifier::STRING), + ), + 'length' => new QueryParameter( + schema: ['maxLength' => 1, 'minLength' => 3], + nativeType: new BuiltinType(TypeIdentifier::STRING), + ), 'array' => new QueryParameter(schema: ['minItems' => 2, 'maxItems' => 3]), - 'multipleOf' => new QueryParameter(schema: ['multipleOf' => 2]), - 'int' => new QueryParameter(property: 'a', constraints: [new Assert\Type('integer')], provider: [self::class, 'toInt']), - 'pattern' => new QueryParameter(schema: ['pattern' => '\d']), + 'multipleOf' => new QueryParameter( + schema: ['multipleOf' => 2], + nativeType: new BuiltinType(TypeIdentifier::STRING), + ), + 'int' => new QueryParameter( + property: 'a', + constraints: [new Assert\Type('integer')], + provider: [self::class, 'toInt'], + nativeType: new BuiltinType(TypeIdentifier::STRING), + ), + 'pattern' => new QueryParameter( + schema: ['pattern' => '\d'], + nativeType: new BuiltinType(TypeIdentifier::STRING), + ), ], provider: [self::class, 'collectionProvider'] )] @@ -77,7 +102,16 @@ )] #[GetCollection( uriTemplate: 'with_parameters_header_and_query{._format}', - parameters: new Parameters([new QueryParameter(key: 'q'), new HeaderParameter(key: 'q')]), + parameters: new Parameters([ + new QueryParameter( + key: 'q', + nativeType: new BuiltinType(TypeIdentifier::STRING), + ), + new HeaderParameter( + key: 'q', + nativeType: new BuiltinType(TypeIdentifier::STRING), + ), + ]), provider: [self::class, 'headerAndQueryProvider'] )] #[GetCollection( @@ -87,6 +121,21 @@ ], provider: [self::class, 'headerProvider'] )] +#[GetCollection( + uriTemplate: 'header_integer', + parameters: [ + 'Foo' => new HeaderParameter( + schema: [ + 'type' => 'integer', + 'example' => 3, + 'minimum' => 1, + 'maximum' => 5, + ], + required: true, + ), + ], + provider: [self::class, 'noopProvider'] +)] #[QueryParameter(key: 'everywhere')] class WithParameter { @@ -170,4 +219,9 @@ public static function headerProvider(Operation $operation, array $uriVariables return new JsonResponse($values); } + + public static function noopProvider(Operation $operation, array $uriVariables = [], array $context = []): JsonResponse + { + return new JsonResponse([]); + } } diff --git a/tests/Fixtures/TestBundle/ApiResource/WithSecurityParameter.php b/tests/Fixtures/TestBundle/ApiResource/WithSecurityParameter.php index e1084e67384..4707c359fe9 100644 --- a/tests/Fixtures/TestBundle/ApiResource/WithSecurityParameter.php +++ b/tests/Fixtures/TestBundle/ApiResource/WithSecurityParameter.php @@ -16,13 +16,15 @@ use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\HeaderParameter; use ApiPlatform\Metadata\QueryParameter; +use Symfony\Component\TypeInfo\Type\BuiltinType; +use Symfony\Component\TypeInfo\TypeIdentifier; #[GetCollection( uriTemplate: 'with_security_parameters_collection{._format}', parameters: [ 'name' => new QueryParameter(security: 'is_granted("ROLE_ADMIN")'), - 'auth' => new HeaderParameter(security: '"secured" == auth[0]'), - 'secret' => new QueryParameter(security: '"secured" == secret'), + 'auth' => new HeaderParameter(security: '"secured" == auth', nativeType: new BuiltinType(TypeIdentifier::STRING)), + 'secret' => new QueryParameter(security: '"secured" == secret', nativeType: new BuiltinType(TypeIdentifier::STRING)), ], provider: [self::class, 'collectionProvider'], )] diff --git a/tests/Fixtures/TestBundle/Document/FilteredBooleanParameter.php b/tests/Fixtures/TestBundle/Document/FilteredBooleanParameter.php index efa2428f864..8964c3d49da 100644 --- a/tests/Fixtures/TestBundle/Document/FilteredBooleanParameter.php +++ b/tests/Fixtures/TestBundle/Document/FilteredBooleanParameter.php @@ -18,16 +18,20 @@ use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\QueryParameter; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; +use Symfony\Component\TypeInfo\Type\BuiltinType; +use Symfony\Component\TypeInfo\TypeIdentifier; #[ApiResource] #[GetCollection( parameters: [ 'active' => new QueryParameter( filter: new BooleanFilter(), + nativeType: new BuiltinType(TypeIdentifier::BOOL), ), 'enabled' => new QueryParameter( filter: new BooleanFilter(), property: 'active', + nativeType: new BuiltinType(TypeIdentifier::BOOL), ), ], )] diff --git a/tests/Fixtures/TestBundle/Document/FilteredExistsParameter.php b/tests/Fixtures/TestBundle/Document/FilteredExistsParameter.php index ee9812fe912..9ff9e628c70 100644 --- a/tests/Fixtures/TestBundle/Document/FilteredExistsParameter.php +++ b/tests/Fixtures/TestBundle/Document/FilteredExistsParameter.php @@ -18,6 +18,8 @@ use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\QueryParameter; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; +use Symfony\Component\TypeInfo\Type\BuiltinType; +use Symfony\Component\TypeInfo\TypeIdentifier; #[ApiResource] #[GetCollection( @@ -25,10 +27,12 @@ parameters: [ 'createdAt' => new QueryParameter( filter: new ExistsFilter(), + nativeType: new BuiltinType(TypeIdentifier::BOOL), ), 'hasCreationDate' => new QueryParameter( filter: new ExistsFilter(), property: 'createdAt', + nativeType: new BuiltinType(TypeIdentifier::BOOL), ), 'exists[:property]' => new QueryParameter( filter: new ExistsFilter(), diff --git a/tests/Fixtures/TestBundle/Document/FilteredOrderParameter.php b/tests/Fixtures/TestBundle/Document/FilteredOrderParameter.php index d3328acca64..ea6e5538dee 100644 --- a/tests/Fixtures/TestBundle/Document/FilteredOrderParameter.php +++ b/tests/Fixtures/TestBundle/Document/FilteredOrderParameter.php @@ -19,6 +19,8 @@ use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\QueryParameter; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; +use Symfony\Component\TypeInfo\Type\BuiltinType; +use Symfony\Component\TypeInfo\TypeIdentifier; #[ApiResource] #[GetCollection( @@ -26,19 +28,23 @@ parameters: [ 'createdAt' => new QueryParameter( filter: new OrderFilter(), + nativeType: new BuiltinType(TypeIdentifier::STRING) ), 'date' => new QueryParameter( filter: new OrderFilter(), property: 'createdAt', + nativeType: new BuiltinType(TypeIdentifier::STRING) ), 'date_null_always_first' => new QueryParameter( filter: new OrderFilter(), property: 'createdAt', filterContext: OrderFilterInterface::NULLS_ALWAYS_FIRST, + nativeType: new BuiltinType(TypeIdentifier::STRING) ), 'date_null_always_first_old_way' => new QueryParameter( filter: new OrderFilter(properties: ['createdAt' => OrderFilterInterface::NULLS_ALWAYS_FIRST]), property: 'createdAt', + nativeType: new BuiltinType(TypeIdentifier::STRING) ), 'order[:property]' => new QueryParameter( filter: new OrderFilter(), diff --git a/tests/Fixtures/TestBundle/Entity/FilteredBooleanParameter.php b/tests/Fixtures/TestBundle/Entity/FilteredBooleanParameter.php index 6ba24650a79..259c2aafa48 100644 --- a/tests/Fixtures/TestBundle/Entity/FilteredBooleanParameter.php +++ b/tests/Fixtures/TestBundle/Entity/FilteredBooleanParameter.php @@ -18,16 +18,20 @@ use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\QueryParameter; use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\TypeInfo\Type\BuiltinType; +use Symfony\Component\TypeInfo\TypeIdentifier; #[ApiResource] #[GetCollection( parameters: [ 'active' => new QueryParameter( filter: new BooleanFilter(), + nativeType: new BuiltinType(TypeIdentifier::BOOL), ), 'enabled' => new QueryParameter( filter: new BooleanFilter(), property: 'active', + nativeType: new BuiltinType(TypeIdentifier::BOOL), ), ], )] diff --git a/tests/Fixtures/TestBundle/Entity/FilteredExistsParameter.php b/tests/Fixtures/TestBundle/Entity/FilteredExistsParameter.php index c154f9798db..e04d55a1f65 100644 --- a/tests/Fixtures/TestBundle/Entity/FilteredExistsParameter.php +++ b/tests/Fixtures/TestBundle/Entity/FilteredExistsParameter.php @@ -18,6 +18,8 @@ use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\QueryParameter; use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\TypeInfo\Type\BuiltinType; +use Symfony\Component\TypeInfo\TypeIdentifier; #[ApiResource] #[GetCollection( @@ -25,10 +27,12 @@ parameters: [ 'createdAt' => new QueryParameter( filter: new ExistsFilter(), + nativeType: new BuiltinType(TypeIdentifier::BOOL), ), 'hasCreationDate' => new QueryParameter( filter: new ExistsFilter(), property: 'createdAt', + nativeType: new BuiltinType(TypeIdentifier::BOOL), ), 'exists[:property]' => new QueryParameter( filter: new ExistsFilter(), diff --git a/tests/Fixtures/TestBundle/Entity/FilteredOrderParameter.php b/tests/Fixtures/TestBundle/Entity/FilteredOrderParameter.php index 3da02eeb7a4..f431cb67b87 100644 --- a/tests/Fixtures/TestBundle/Entity/FilteredOrderParameter.php +++ b/tests/Fixtures/TestBundle/Entity/FilteredOrderParameter.php @@ -19,6 +19,8 @@ use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\QueryParameter; use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\TypeInfo\Type\BuiltinType; +use Symfony\Component\TypeInfo\TypeIdentifier; #[ApiResource] #[GetCollection( @@ -26,19 +28,23 @@ parameters: [ 'createdAt' => new QueryParameter( filter: new OrderFilter(), + nativeType: new BuiltinType(TypeIdentifier::STRING) ), 'date' => new QueryParameter( filter: new OrderFilter(), property: 'createdAt', + nativeType: new BuiltinType(TypeIdentifier::STRING) ), 'date_null_always_first' => new QueryParameter( filter: new OrderFilter(), property: 'createdAt', filterContext: OrderFilterInterface::NULLS_ALWAYS_FIRST, + nativeType: new BuiltinType(TypeIdentifier::STRING) ), 'date_null_always_first_old_way' => new QueryParameter( filter: new OrderFilter(properties: ['createdAt' => OrderFilterInterface::NULLS_ALWAYS_FIRST]), property: 'createdAt', + nativeType: new BuiltinType(TypeIdentifier::STRING) ), 'order[:property]' => new QueryParameter( filter: new OrderFilter(), diff --git a/tests/Functional/Parameters/ParameterTest.php b/tests/Functional/Parameters/ParameterTest.php index 6620975d9c9..5d08e514a40 100644 --- a/tests/Functional/Parameters/ParameterTest.php +++ b/tests/Functional/Parameters/ParameterTest.php @@ -16,6 +16,7 @@ use ApiPlatform\Symfony\Bundle\Test\ApiTestCase; use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\WithParameter; use ApiPlatform\Tests\SetupClassResourcesTrait; +use PHPUnit\Framework\Attributes\DataProvider; final class ParameterTest extends ApiTestCase { @@ -96,7 +97,7 @@ public function testHeaderAndQuery(): void { $response = self::createClient()->request('GET', 'with_parameters_header_and_query?q=blabla', ['headers' => ['q' => '(complex stuff)']]); $this->assertEquals($response->toArray(), [ - ['(complex stuff)'], + '(complex stuff)', 'blabla', ]); } @@ -109,4 +110,19 @@ public function testHeaderParameterRequired(): void self::createClient()->request('GET', 'header_required', ['headers' => []]); $this->assertResponseStatusCodeSame(422); } + + #[DataProvider('provideHeaderValues')] + public function testHeaderParameterInteger(string $value, int $expectedStatusCode): void + { + self::createClient()->request('GET', 'header_integer', ['headers' => ['Foo' => $value]]); + $this->assertResponseStatusCodeSame($expectedStatusCode); + } + + public static function provideHeaderValues(): iterable + { + yield 'valid integer' => ['3', 200]; + yield 'too high' => ['6', 422]; + yield 'too low' => ['0', 422]; + yield 'invalid integer' => ['string', 422]; + } } diff --git a/tests/Functional/Parameters/ValidationTest.php b/tests/Functional/Parameters/ValidationTest.php index f93d07982ab..73b2752df46 100644 --- a/tests/Functional/Parameters/ValidationTest.php +++ b/tests/Functional/Parameters/ValidationTest.php @@ -59,10 +59,13 @@ public static function provideQueryStrings(): array 'enum[]=c&enum[]=c', [ [ - 'message' => 'This collection should contain only unique elements.', + 'propertyPath' => 'enum[0]', 'message' => 'The value you selected is not a valid choice.', + ], + [ + 'propertyPath' => 'enum[1]', 'message' => 'The value you selected is not a valid choice.', ], [ - 'propertyPath' => 'enum', 'message' => 'The value you selected is not a valid choice.', + 'message' => 'This collection should contain only unique elements.', ], ], ], @@ -101,7 +104,7 @@ public static function provideQueryStrings(): array [ 'num=5', [ - ['propertyPath' => 'num', 'message' => 'This value should be less than or equal to 3.'], + ['propertyPath' => 'num', 'message' => 'This value should be between 1 and 3.'], ], ], [