• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

api-platform / core / 20847864477

09 Jan 2026 09:47AM UTC coverage: 29.1% (+0.005%) from 29.095%
20847864477

Pull #7649

github

web-flow
Merge b342dd5db into d640d106b
Pull Request #7649: feat(validator): uuid/ulid parameter validation

0 of 4 new or added lines in 1 file covered. (0.0%)

15050 existing lines in 491 files now uncovered.

16996 of 58406 relevant lines covered (29.1%)

81.8 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.85
/src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php
1
<?php
2

3
/*
4
 * This file is part of the API Platform project.
5
 *
6
 * (c) Kévin Dunglas <dunglas@gmail.com>
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\Metadata\Resource\Factory;
15

16
use ApiPlatform\Doctrine\Common\Filter\PropertyAwareFilterInterface;
17
use ApiPlatform\Metadata\ApiProperty;
18
use ApiPlatform\Metadata\Exception\RuntimeException;
19
use ApiPlatform\Metadata\FilterInterface;
20
use ApiPlatform\Metadata\JsonSchemaFilterInterface;
21
use ApiPlatform\Metadata\OpenApiParameterFilterInterface;
22
use ApiPlatform\Metadata\Operation;
23
use ApiPlatform\Metadata\Parameter;
24
use ApiPlatform\Metadata\ParameterProviderFilterInterface;
25
use ApiPlatform\Metadata\Parameters;
26
use ApiPlatform\Metadata\PropertiesAwareInterface;
27
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
28
use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
29
use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
30
use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter;
31
use ApiPlatform\Serializer\Filter\FilterInterface as SerializerFilterInterface;
32
use ApiPlatform\State\Parameter\ValueCaster;
33
use ApiPlatform\State\Util\StateOptionsTrait;
34
use Psr\Container\ContainerInterface;
35
use Psr\Log\LoggerInterface;
36
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
37
use Symfony\Component\TypeInfo\Type;
38
use Symfony\Component\TypeInfo\TypeIdentifier;
39

40
/**
41
 * Prepares Parameters documentation by reading its filter details and declaring an OpenApi parameter.
42
 */
43
final class ParameterResourceMetadataCollectionFactory implements ResourceMetadataCollectionFactoryInterface
44
{
45
    use StateOptionsTrait;
46

47
    private array $localPropertyCache = [];
48

49
    public function __construct(
50
        private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory,
51
        private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory,
52
        private readonly ?ResourceMetadataCollectionFactoryInterface $decorated = null,
53
        private readonly ?ContainerInterface $filterLocator = null,
54
        private readonly ?NameConverterInterface $nameConverter = null,
55
        private readonly ?LoggerInterface $logger = null,
56
    ) {
UNCOV
57
    }
2,474✔
58

59
    public function create(string $resourceClass): ResourceMetadataCollection
60
    {
UNCOV
61
        $resourceMetadataCollection = $this->decorated?->create($resourceClass) ?? new ResourceMetadataCollection($resourceClass);
211✔
62

UNCOV
63
        foreach ($resourceMetadataCollection as $i => $resource) {
211✔
UNCOV
64
            $operations = $resource->getOperations();
203✔
65

UNCOV
66
            $internalPriority = -1;
203✔
UNCOV
67
            foreach ($operations as $operationName => $operation) {
203✔
UNCOV
68
                $parameters = $this->getDefaultParameters($operation, $resourceClass, $internalPriority);
203✔
UNCOV
69
                if (\count($parameters) > 0) {
203✔
UNCOV
70
                    $operations->add($operationName, $operation->withParameters($parameters));
49✔
71
                }
72
            }
73

UNCOV
74
            $resourceMetadataCollection[$i] = $resource->withOperations($operations->sort());
203✔
75

UNCOV
76
            if (!$graphQlOperations = $resource->getGraphQlOperations()) {
203✔
UNCOV
77
                continue;
86✔
78
            }
79

UNCOV
80
            $internalPriority = -1;
161✔
UNCOV
81
            foreach ($graphQlOperations as $operationName => $operation) {
161✔
UNCOV
82
                $parameters = $this->getDefaultParameters($operation, $resourceClass, $internalPriority);
161✔
UNCOV
83
                if (\count($parameters) > 0) {
161✔
UNCOV
84
                    $graphQlOperations[$operationName] = $operation->withParameters($parameters);
6✔
85
                }
86
            }
87

UNCOV
88
            $resourceMetadataCollection[$i] = $resource->withGraphQlOperations($graphQlOperations);
161✔
89
        }
90

UNCOV
91
        return $resourceMetadataCollection;
211✔
92
    }
93

94
    /**
95
     * @return array{propertyNames: string[], properties: array<string, ApiProperty>}
96
     */
97
    private function getProperties(string $resourceClass, ?Parameter $parameter = null): array
98
    {
UNCOV
99
        $k = $resourceClass.($parameter?->getProperties() ? ($parameter->getKey() ?? '') : '').(\is_string($parameter->getFilter()) ? $parameter->getFilter() : '');
49✔
UNCOV
100
        if (isset($this->localPropertyCache[$k])) {
49✔
UNCOV
101
            return $this->localPropertyCache[$k];
42✔
102
        }
103

UNCOV
104
        $propertyNames = [];
49✔
UNCOV
105
        $properties = [];
49✔
UNCOV
106
        foreach ($parameter?->getProperties() ?? $this->propertyNameCollectionFactory->create($resourceClass) as $property) {
49✔
UNCOV
107
            $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $property);
43✔
UNCOV
108
            if ($propertyMetadata->isReadable()) {
43✔
UNCOV
109
                $propertyNames[] = $property;
43✔
UNCOV
110
                $properties[$property] = $propertyMetadata;
43✔
111
            }
112
        }
113

UNCOV
114
        if (($filter = $this->getFilterInstance($parameter->getFilter())) && $filter instanceof PropertyAwareFilterInterface) {
49✔
UNCOV
115
            if (!method_exists($filter, 'getProperties')) { // @phpstan-ignore-line todo 5.x remove this check
19✔
116
                trigger_deprecation('api-platform/core', 'In API Platform 5.0 "%s" will implement a method named "getProperties"', PropertyAwareFilterInterface::class);
×
117
                $refl = new \ReflectionClass($filter);
×
118
                $filterProperties = $refl->hasProperty('properties') ? $refl->getProperty('properties')->getValue($filter) : [];
×
119
            } else {
UNCOV
120
                $filterProperties = array_keys($filter->getProperties() ?? []);
19✔
121
            }
122

UNCOV
123
            foreach ($filterProperties as $prop) {
19✔
UNCOV
124
                if (!\in_array($prop, $propertyNames, true)) {
7✔
125
                    $propertyNames[] = $this->nameConverter?->denormalize($prop) ?? $prop;
×
126
                }
127
            }
128
        }
129

UNCOV
130
        $this->localPropertyCache[$k] = ['propertyNames' => $propertyNames, 'properties' => $properties];
49✔
131

UNCOV
132
        return $this->localPropertyCache[$k];
49✔
133
    }
134

135
    private function getDefaultParameters(Operation $operation, string $resourceClass, int &$internalPriority): Parameters
136
    {
UNCOV
137
        $propertyNames = $properties = [];
203✔
UNCOV
138
        $parameters = $operation->getParameters() ?? new Parameters();
203✔
139

140
        // First loop we look for the :property placeholder and replace its key
UNCOV
141
        foreach ($parameters as $key => $parameter) {
203✔
UNCOV
142
            if (!str_contains($key, ':property')) {
49✔
UNCOV
143
                continue;
47✔
144
            }
145

UNCOV
146
            if (!$parameter->getKey()) {
10✔
UNCOV
147
                $parameter = $parameter->withKey($key);
10✔
148
            }
149

UNCOV
150
            ['propertyNames' => $propertyNames, 'properties' => $properties] = $this->getProperties($resourceClass, $parameter);
10✔
UNCOV
151
            $parameter = $parameter->withProperties($propertyNames);
10✔
152

UNCOV
153
            foreach ($propertyNames as $property) {
10✔
UNCOV
154
                $converted = $this->nameConverter?->denormalize($property) ?? $property;
10✔
UNCOV
155
                $finalKey = str_replace(':property', $converted, $key);
10✔
UNCOV
156
                $parameters->add(
10✔
UNCOV
157
                    $finalKey,
10✔
UNCOV
158
                    $parameter->withProperty($converted)->withKey($finalKey)
10✔
UNCOV
159
                );
10✔
160
            }
161

UNCOV
162
            $parameters->remove($key, $parameter::class);
10✔
163
        }
164

UNCOV
165
        foreach ($parameters as $key => $parameter) {
203✔
UNCOV
166
            if (!$parameter->getKey()) {
49✔
UNCOV
167
                $parameter = $parameter->withKey($key);
47✔
168
            }
169

UNCOV
170
            $filter = $this->getFilterInstance($parameter->getFilter());
49✔
171

172
            // The filter has a parameter provider
UNCOV
173
            if (null === $parameter->getProvider() && (($f = $parameter->getFilter()) && $f instanceof ParameterProviderFilterInterface)) {
49✔
UNCOV
174
                $parameter = $parameter->withProvider($f->getParameterProvider());
4✔
175
            }
176

UNCOV
177
            $key = $parameter->getKey();
49✔
178

UNCOV
179
            ['propertyNames' => $propertyNames, 'properties' => $properties] = $this->getProperties($resourceClass, $parameter);
49✔
180

UNCOV
181
            if ($filter instanceof PropertiesAwareInterface) {
49✔
182
                $parameter = $parameter->withProperties($propertyNames);
×
183
            }
184

UNCOV
185
            $parameter = $this->setDefaults($key, $parameter, $filter, $properties, $operation);
49✔
186
            // We don't do any type cast yet, a query parameter or an header is always a string or a list of strings
UNCOV
187
            if (null === $parameter->getNativeType()) {
49✔
188
                // this forces the type to be only a list
UNCOV
189
                if ('array' === ($parameter->getSchema()['type'] ?? null)) {
45✔
UNCOV
190
                    $parameter = $parameter->withNativeType(Type::list(Type::string()));
6✔
UNCOV
191
                } elseif ('string' === ($parameter->getSchema()['type'] ?? null)) {
45✔
UNCOV
192
                    $parameter = $parameter->withNativeType(Type::string());
17✔
UNCOV
193
                } elseif ('boolean' === ($parameter->getSchema()['type'] ?? null)) {
36✔
UNCOV
194
                    $parameter = $parameter->withNativeType(Type::bool());
8✔
UNCOV
195
                } elseif ('integer' === ($parameter->getSchema()['type'] ?? null)) {
34✔
UNCOV
196
                    $parameter = $parameter->withNativeType(Type::int());
4✔
UNCOV
197
                } elseif ('number' === ($parameter->getSchema()['type'] ?? null)) {
34✔
UNCOV
198
                    $parameter = $parameter->withNativeType(Type::float());
6✔
199
                } else {
UNCOV
200
                    $parameter = $parameter->withNativeType(Type::union(Type::string(), Type::list(Type::string())));
32✔
201
                }
202
            }
203

UNCOV
204
            if ($parameter->getCastToNativeType() && null === $parameter->getCastFn() && ($nativeType = $parameter->getNativeType())) {
49✔
UNCOV
205
                if ($nativeType->isIdentifiedBy(TypeIdentifier::BOOL)) {
6✔
UNCOV
206
                    $parameter = $parameter->withCastFn([ValueCaster::class, 'toBool']);
6✔
207
                }
UNCOV
208
                if ($nativeType->isIdentifiedBy(TypeIdentifier::INT)) {
6✔
UNCOV
209
                    $parameter = $parameter->withCastFn([ValueCaster::class, 'toInt']);
4✔
210
                }
UNCOV
211
                if ($nativeType->isIdentifiedBy(TypeIdentifier::FLOAT)) {
6✔
UNCOV
212
                    $parameter = $parameter->withCastFn([ValueCaster::class, 'toFloat']);
4✔
213
                }
214
            }
215

UNCOV
216
            $priority = $parameter->getPriority() ?? $internalPriority--;
49✔
UNCOV
217
            $parameters->add($key, $parameter->withPriority($priority));
49✔
218
        }
219

UNCOV
220
        return $parameters;
203✔
221
    }
222

223
    private function addFilterMetadata(Parameter $parameter): Parameter
224
    {
UNCOV
225
        if (!$filter = $this->getFilterInstance($parameter->getFilter())) {
49✔
UNCOV
226
            return $parameter;
20✔
227
        }
228

UNCOV
229
        if ($filter instanceof ParameterProviderFilterInterface) {
35✔
UNCOV
230
            $parameter = $parameter->withProvider($filter::getParameterProvider());
4✔
231
        }
232

UNCOV
233
        if (null === $parameter->getSchema() && $filter instanceof JsonSchemaFilterInterface && $schema = $filter->getSchema($parameter)) {
35✔
UNCOV
234
            $parameter = $parameter->withSchema($schema);
31✔
235
        }
236

UNCOV
237
        if (null === $parameter->getOpenApi() && $filter instanceof OpenApiParameterFilterInterface && ($openApiParameter = $filter->getOpenApiParameters($parameter))) {
35✔
UNCOV
238
            $parameter = $parameter->withOpenApi($openApiParameter);
25✔
239
        }
240

UNCOV
241
        return $parameter;
35✔
242
    }
243

244
    /**
245
     * @param array<string, ApiProperty> $properties
246
     */
247
    private function setDefaults(string $key, Parameter $parameter, ?object $filter, array $properties, Operation $operation): Parameter
248
    {
UNCOV
249
        if (null === $parameter->getKey()) {
49✔
250
            $parameter = $parameter->withKey($key);
×
251
        }
252

UNCOV
253
        if ($filter instanceof SerializerFilterInterface && null === $parameter->getProvider()) {
49✔
UNCOV
254
            $parameter = $parameter->withProvider('api_platform.serializer.filter_parameter_provider');
4✔
255
        }
256

UNCOV
257
        $currentKey = $key;
49✔
UNCOV
258
        if (null === $parameter->getProperty() && isset($properties[$key])) {
49✔
UNCOV
259
            $parameter = $parameter->withProperty($key);
33✔
260
        }
261

UNCOV
262
        if ($this->nameConverter && $property = $parameter->getProperty()) {
49✔
UNCOV
263
            $parameter = $parameter->withProperty($this->nameConverter->normalize($property));
39✔
264
        }
265

UNCOV
266
        if (isset($properties[$currentKey]) && ($eloquentRelation = ($properties[$currentKey]->getExtraProperties()['eloquent_relation'] ?? null)) && isset($eloquentRelation['foreign_key'])) {
49✔
267
            $parameter = $parameter->withProperty($eloquentRelation['foreign_key']);
×
268
        }
269

UNCOV
270
        $parameter = $this->addFilterMetadata($parameter);
49✔
271

UNCOV
272
        if ($filter instanceof FilterInterface) {
49✔
273
            try {
UNCOV
274
                return $this->getLegacyFilterMetadata($parameter, $operation, $filter);
35✔
UNCOV
275
            } catch (RuntimeException $exception) {
16✔
UNCOV
276
                $this->logger?->alert($exception->getMessage(), ['exception' => $exception]);
16✔
277

UNCOV
278
                return $parameter;
16✔
279
            }
280
        }
281

UNCOV
282
        return $parameter;
20✔
283
    }
284

285
    private function getLegacyFilterMetadata(Parameter $parameter, Operation $operation, FilterInterface $filter): Parameter
286
    {
UNCOV
287
        $description = $filter->getDescription($this->getStateOptionsClass($operation, $operation->getClass()));
35✔
UNCOV
288
        $key = $parameter->getKey();
23✔
UNCOV
289
        if (($schema = $description[$key]['schema'] ?? null) && null === $parameter->getSchema()) {
23✔
290
            $parameter = $parameter->withSchema($schema);
×
291
        }
292

UNCOV
293
        if (null === $parameter->getProperty() && ($property = $description[$key]['property'] ?? null)) {
23✔
294
            $parameter = $parameter->withProperty($property);
×
295
        }
296

UNCOV
297
        if (null === $parameter->getRequired() && ($required = $description[$key]['required'] ?? null)) {
23✔
298
            $parameter = $parameter->withRequired($required);
×
299
        }
300

UNCOV
301
        if (null === $parameter->getOpenApi() && ($openApi = $description[$key]['openapi'] ?? null) && $openApi instanceof OpenApiParameter) {
23✔
302
            $parameter = $parameter->withOpenApi($openApi);
×
303
        }
304

UNCOV
305
        return $parameter;
23✔
306
    }
307

308
    /**
309
     * TODO: 5.x use FilterInterface on Laravel eloquent filters.
310
     *
311
     * @return FilterInterface|object
312
     */
313
    private function getFilterInstance(object|string|null $filter): ?object
314
    {
UNCOV
315
        if (!$filter) {
49✔
UNCOV
316
            return null;
20✔
317
        }
318

UNCOV
319
        if (\is_object($filter)) {
35✔
UNCOV
320
            return $filter;
32✔
321
        }
322

UNCOV
323
        if (!$this->filterLocator->has($filter)) {
9✔
324
            return null;
1✔
325
        }
326

UNCOV
327
        return $this->filterLocator->get($filter);
9✔
328
    }
329
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc