Skip to content

Commit b7f0f93

Browse files
authored
refactor(metadata): type parameters to list<string>|string (#7134)
1 parent b179302 commit b7f0f93

21 files changed

+246
-53
lines changed

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@
114114
"symfony/property-info": "^6.4 || ^7.1",
115115
"symfony/serializer": "^6.4 || ^7.0",
116116
"symfony/translation-contracts": "^3.3",
117+
"symfony/type-info": "^7.2",
117118
"symfony/web-link": "^6.4 || ^7.1",
118119
"willdurand/negotiation": "^3.1"
119120
},

src/Laravel/Eloquent/Extension/FilterQueryExtension.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ public function apply(Builder $builder, array $uriVariables, Operation $operatio
5353
continue;
5454
}
5555

56+
// most eloquent filters work with only a single value
57+
if (\is_array($values) && array_is_list($values) && 1 === \count($values)) {
58+
$values = current($values);
59+
}
60+
5661
$filter = $filterId instanceof FilterInterface ? $filterId : ($this->filterLocator->has($filterId) ? $this->filterLocator->get($filterId) : null);
5762
if ($filter instanceof FilterInterface) {
5863
$builder = $filter->apply($builder, $values, $parameter, $context + ($parameter->getFilterContext() ?? []));

src/Laravel/Eloquent/Filter/JsonApi/SortFilterParameterProvider.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ public function provide(Parameter $parameter, array $parameters = [], array $con
2828
$parameters = $operation->getParameters();
2929
$properties = $parameter->getExtraProperties()['_properties'] ?? [];
3030
$value = $parameter->getValue();
31+
32+
// most eloquent filters work with only a single value
33+
if (\is_array($value) && array_is_list($value) && 1 === \count($value)) {
34+
$value = current($value);
35+
}
36+
3137
if (!\is_string($value)) {
3238
return $operation;
3339
}

src/Metadata/Parameter.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter;
1717
use ApiPlatform\State\ParameterNotFound;
1818
use ApiPlatform\State\ParameterProviderInterface;
19+
use Symfony\Component\TypeInfo\Type;
1920

2021
/**
2122
* @experimental
@@ -29,6 +30,7 @@ abstract class Parameter
2930
* @param list<string> $properties a list of properties this parameter applies to (works with the :property placeholder)
3031
* @param FilterInterface|string|null $filter
3132
* @param mixed $constraints an array of Symfony constraints, or an array of Laravel rules
33+
* @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)
3234
*/
3335
public function __construct(
3436
protected ?string $key = null,
@@ -47,6 +49,7 @@ public function __construct(
4749
protected ?string $securityMessage = null,
4850
protected ?array $extraProperties = [],
4951
protected array|string|null $filterContext = null,
52+
protected ?Type $nativeType = null,
5053
) {
5154
}
5255

@@ -296,4 +299,17 @@ public function withProperties(?array $properties): self
296299

297300
return $self;
298301
}
302+
303+
public function getNativeType(): ?Type
304+
{
305+
return $this->nativeType;
306+
}
307+
308+
public function withNativeType(Type $nativeType): self
309+
{
310+
$self = clone $this;
311+
$self->nativeType = $nativeType;
312+
313+
return $self;
314+
}
299315
}

src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
use Psr\Container\ContainerInterface;
3333
use Psr\Log\LoggerInterface;
3434
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
35+
use Symfony\Component\TypeInfo\Type;
3536

3637
/**
3738
* Prepares Parameters documentation by reading its filter details and declaring an OpenApi parameter.
@@ -148,6 +149,16 @@ private function getDefaultParameters(Operation $operation, string $resourceClas
148149
}
149150

150151
$parameter = $this->setDefaults($key, $parameter, $resourceClass, $properties, $operation);
152+
// We don't do any type cast yet, a query parameter or an header is always a string or a list of strings
153+
if (null === $parameter->getNativeType()) {
154+
// this forces the type to be only a list
155+
if ('array' === ($parameter->getSchema()['type'] ?? null)) {
156+
$parameter = $parameter->withNativeType(Type::list(Type::string()));
157+
} else {
158+
$parameter = $parameter->withNativeType(Type::union(Type::string(), Type::list(Type::string())));
159+
}
160+
}
161+
151162
$priority = $parameter->getPriority() ?? $internalPriority--;
152163
$parameters->add($key, $parameter->withPriority($priority));
153164
}

src/Metadata/composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"psr/log": "^1.0 || ^2.0 || ^3.0",
3434
"symfony/property-info": "^6.4 || ^7.1",
3535
"symfony/string": "^6.4 || ^7.0",
36-
"symfony/type-info": "^7.1"
36+
"symfony/type-info": "^7.2"
3737
},
3838
"require-dev": {
3939
"api-platform/json-schema": "^4.1",

src/State/Util/ParameterParserTrait.php

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,13 @@
1313

1414
namespace ApiPlatform\State\Util;
1515

16+
use ApiPlatform\Metadata\HeaderParameter;
1617
use ApiPlatform\Metadata\HeaderParameterInterface;
1718
use ApiPlatform\Metadata\Parameter;
1819
use ApiPlatform\State\ParameterNotFound;
1920
use Symfony\Component\HttpFoundation\Request;
21+
use Symfony\Component\TypeInfo\Type\CollectionType;
22+
use Symfony\Component\TypeInfo\Type\UnionType;
2023

2124
/**
2225
* @internal
@@ -64,11 +67,7 @@ private function extractParameterValues(Parameter $parameter, array $values): st
6467
}
6568

6669
$value = $values[$key] ?? new ParameterNotFound();
67-
if (!$accessors) {
68-
return $value;
69-
}
70-
71-
foreach ($accessors as $accessor) {
70+
foreach ($accessors ?? [] as $accessor) {
7271
if (\is_array($value) && isset($value[$accessor])) {
7372
$value = $value[$accessor];
7473
} else {
@@ -77,6 +76,28 @@ private function extractParameterValues(Parameter $parameter, array $values): st
7776
}
7877
}
7978

79+
if ($value instanceof ParameterNotFound) {
80+
return $value;
81+
}
82+
83+
$isCollectionType = fn ($t) => $t instanceof CollectionType;
84+
$isCollection = $parameter->getNativeType()?->isSatisfiedBy($isCollectionType) ?? false;
85+
86+
// type-info 7.2
87+
if (!$isCollection && $parameter->getNativeType() instanceof UnionType) {
88+
foreach ($parameter->getNativeType()->getTypes() as $t) {
89+
if ($isCollection = $t->isSatisfiedBy($isCollectionType)) {
90+
break;
91+
}
92+
}
93+
}
94+
95+
if ($isCollection) {
96+
$value = \is_array($value) ? $value : [$value];
97+
} elseif ($parameter instanceof HeaderParameter && \is_array($value) && array_is_list($value) && 1 === \count($value)) {
98+
$value = $value[0];
99+
}
100+
80101
return $value;
81102
}
82103
}

src/Symfony/Validator/State/ParameterValidatorProvider.php

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -62,14 +62,7 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
6262
$value = null;
6363
}
6464

65-
$violations = [];
66-
if (\is_array($value) && $properties = $parameter->getExtraProperties()['_properties'] ?? []) {
67-
foreach ($properties as $property) {
68-
$violations = [...$violations, ...$this->validator->validate($value[$property] ?? null, $constraints)];
69-
}
70-
} else {
71-
$violations = $this->validator->validate($value, $constraints);
72-
}
65+
$violations = $this->validator->validate($value, $constraints);
7366

7467
foreach ($violations as $violation) {
7568
$constraintViolationList->add(new ConstraintViolation(
@@ -108,7 +101,7 @@ private function getProperty(Parameter $parameter, ConstraintViolationInterface
108101
}
109102

110103
if ($p = $violation->getPropertyPath()) {
111-
return $p;
104+
return $key.$p;
112105
}
113106

114107
return $key;

src/Validator/Metadata/Resource/Factory/ParameterValidationResourceMetadataCollectionFactory.php

Lines changed: 56 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@
2121
use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
2222
use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter;
2323
use Psr\Container\ContainerInterface;
24+
use Symfony\Component\TypeInfo\Type\CollectionType;
25+
use Symfony\Component\TypeInfo\Type\UnionType;
26+
use Symfony\Component\Validator\Constraints\All;
2427
use Symfony\Component\Validator\Constraints\Choice;
28+
use Symfony\Component\Validator\Constraints\Collection;
2529
use Symfony\Component\Validator\Constraints\Count;
2630
use Symfony\Component\Validator\Constraints\DivisibleBy;
2731
use Symfony\Component\Validator\Constraints\GreaterThan;
@@ -31,6 +35,7 @@
3135
use Symfony\Component\Validator\Constraints\LessThanOrEqual;
3236
use Symfony\Component\Validator\Constraints\NotBlank;
3337
use Symfony\Component\Validator\Constraints\NotNull;
38+
use Symfony\Component\Validator\Constraints\Range;
3439
use Symfony\Component\Validator\Constraints\Regex;
3540
use Symfony\Component\Validator\Constraints\Type;
3641
use Symfony\Component\Validator\Constraints\Unique;
@@ -108,55 +113,82 @@ private function addSchemaValidation(Parameter $parameter, ?array $schema = null
108113
}
109114

110115
$assertions = [];
116+
$allowEmptyValue = $openApi?->getAllowEmptyValue();
117+
if (false === ($allowEmptyValue ?? $openApi?->getAllowEmptyValue())) {
118+
$assertions[] = new NotBlank(allowNull: !$required);
119+
}
111120

112-
if ($required && false !== ($allowEmptyValue = $openApi?->getAllowEmptyValue())) {
113-
$assertions[] = new NotNull(message: \sprintf('The parameter "%s" is required.', $parameter->getKey()));
121+
$minimum = $schema['exclusiveMinimum'] ?? $schema['minimum'] ?? null;
122+
$exclusiveMinimum = isset($schema['exclusiveMinimum']);
123+
$maximum = $schema['exclusiveMaximum'] ?? $schema['maximum'] ?? null;
124+
$exclusiveMaximum = isset($schema['exclusiveMaximum']);
125+
126+
if ($minimum && $maximum) {
127+
if (!$exclusiveMinimum && !$exclusiveMaximum) {
128+
$assertions[] = new Range(min: $minimum, max: $maximum);
129+
} else {
130+
$assertions[] = $exclusiveMinimum ? new GreaterThan(value: $minimum) : new GreaterThanOrEqual(value: $minimum);
131+
$assertions[] = $exclusiveMaximum ? new LessThan(value: $maximum) : new LessThanOrEqual(value: $maximum);
132+
}
133+
} elseif ($minimum) {
134+
$assertions[] = $exclusiveMinimum ? new GreaterThan(value: $minimum) : new GreaterThanOrEqual(value: $minimum);
135+
} elseif ($maximum) {
136+
$assertions[] = $exclusiveMaximum ? new LessThan(value: $maximum) : new LessThanOrEqual(value: $maximum);
114137
}
115138

116-
if (false === ($allowEmptyValue ?? $openApi?->getAllowEmptyValue())) {
117-
$assertions[] = new NotBlank(allowNull: !$required);
139+
if (isset($schema['pattern'])) {
140+
$assertions[] = new Regex('#'.$schema['pattern'].'#');
118141
}
119142

120-
if (isset($schema['exclusiveMinimum'])) {
121-
$assertions[] = new GreaterThan(value: $schema['exclusiveMinimum']);
143+
if (isset($schema['maxLength']) || isset($schema['minLength'])) {
144+
$assertions[] = new Length(min: $schema['minLength'] ?? null, max: $schema['maxLength'] ?? null);
122145
}
123146

124-
if (isset($schema['exclusiveMaximum'])) {
125-
$assertions[] = new LessThan(value: $schema['exclusiveMaximum']);
147+
if (isset($schema['multipleOf'])) {
148+
$assertions[] = new DivisibleBy(value: $schema['multipleOf']);
149+
}
150+
151+
if (isset($schema['enum'])) {
152+
$assertions[] = new Choice(choices: $schema['enum']);
126153
}
127154

128-
if (isset($schema['minimum'])) {
129-
$assertions[] = new GreaterThanOrEqual(value: $schema['minimum']);
155+
if ($properties = $parameter->getExtraProperties()['_properties'] ?? []) {
156+
$fields = [];
157+
foreach ($properties as $propertyName) {
158+
$fields[$propertyName] = $assertions;
159+
}
160+
161+
return $parameter->withConstraints(new Collection(fields: $fields, allowMissingFields: true));
130162
}
131163

132-
if (isset($schema['maximum'])) {
133-
$assertions[] = new LessThanOrEqual(value: $schema['maximum']);
164+
$isCollectionType = fn ($t) => $t instanceof CollectionType;
165+
$isCollection = $parameter->getNativeType()?->isSatisfiedBy($isCollectionType) ?? false;
166+
167+
// type-info 7.2
168+
if (!$isCollection && $parameter->getNativeType() instanceof UnionType) {
169+
foreach ($parameter->getNativeType()->getTypes() as $t) {
170+
if ($isCollection = $t->isSatisfiedBy($isCollectionType)) {
171+
break;
172+
}
173+
}
134174
}
135175

136-
if (isset($schema['pattern'])) {
137-
$assertions[] = new Regex('#'.$schema['pattern'].'#');
176+
if ($isCollection) {
177+
$assertions = $assertions ? [new All($assertions)] : [];
138178
}
139179

140-
if (isset($schema['maxLength']) || isset($schema['minLength'])) {
141-
$assertions[] = new Length(min: $schema['minLength'] ?? null, max: $schema['maxLength'] ?? null);
180+
if ($required && false !== $allowEmptyValue) {
181+
$assertions[] = new NotNull(message: \sprintf('The parameter "%s" is required.', $parameter->getKey()));
142182
}
143183

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

148-
if (isset($schema['multipleOf'])) {
149-
$assertions[] = new DivisibleBy(value: $schema['multipleOf']);
150-
}
151-
152188
if ($schema['uniqueItems'] ?? false) {
153189
$assertions[] = new Unique();
154190
}
155191

156-
if (isset($schema['enum'])) {
157-
$assertions[] = new Choice(choices: $schema['enum']);
158-
}
159-
160192
if (isset($schema['type']) && 'array' === $schema['type']) {
161193
$assertions[] = new Type(type: 'array');
162194
}

src/Validator/composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"require": {
2525
"php": ">=8.2",
2626
"api-platform/metadata": "^4.1",
27+
"symfony/type-info": "^7.2",
2728
"symfony/web-link": "^6.4 || ^7.1"
2829
},
2930
"require-dev": {

tests/Fixtures/TestBundle/ApiResource/Issue6673/MutlipleParameterProvider.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
use ApiPlatform\Metadata\Operation;
1818
use ApiPlatform\Metadata\Parameter;
1919
use ApiPlatform\Metadata\QueryParameter;
20+
use Symfony\Component\TypeInfo\Type\BuiltinType;
21+
use Symfony\Component\TypeInfo\TypeIdentifier;
2022

2123
#[GetCollection(
2224
uriTemplate: 'issue6673_multiple_parameter_provider',
@@ -25,9 +27,11 @@
2527
parameters: [
2628
'a' => new QueryParameter(
2729
provider: [self::class, 'parameterOneProvider'],
30+
nativeType: new BuiltinType(TypeIdentifier::STRING),
2831
),
2932
'b' => new QueryParameter(
3033
provider: [self::class, 'parameterTwoProvider'],
34+
nativeType: new BuiltinType(TypeIdentifier::STRING),
3135
),
3236
],
3337
provider: [self::class, 'provide']

0 commit comments

Comments
 (0)