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

api-platform / core / 19799301771

30 Nov 2025 01:04PM UTC coverage: 25.229% (-0.03%) from 25.257%
19799301771

push

github

web-flow
fix(metadata): repeatable attribute mutators (#7542)

14557 of 57700 relevant lines covered (25.23%)

28.11 hits per line

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

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

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

46
    private array $localPropertyCache;
47

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

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

62
        foreach ($resourceMetadataCollection as $i => $resource) {
124✔
63
            $operations = $resource->getOperations();
122✔
64

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

73
            $resourceMetadataCollection[$i] = $resource->withOperations($operations->sort());
122✔
74

75
            if (!$graphQlOperations = $resource->getGraphQlOperations()) {
122✔
76
                continue;
62✔
77
            }
78

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

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

90
        return $resourceMetadataCollection;
124✔
91
    }
92

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

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

113
        $this->localPropertyCache[$k] = ['propertyNames' => $propertyNames, 'properties' => $properties];
36✔
114

115
        return $this->localPropertyCache[$k];
36✔
116
    }
117

118
    private function getDefaultParameters(Operation $operation, string $resourceClass, int &$internalPriority): Parameters
119
    {
120
        $propertyNames = $properties = [];
122✔
121
        $parameters = $operation->getParameters() ?? new Parameters();
122✔
122
        foreach ($parameters as $key => $parameter) {
122✔
123
            if (!$parameter->getKey()) {
36✔
124
                $parameter = $parameter->withKey($key);
36✔
125
            }
126

127
            ['propertyNames' => $propertyNames, 'properties' => $properties] = $this->getProperties($resourceClass, $parameter);
36✔
128
            if (null === $parameter->getProvider() && (($f = $parameter->getFilter()) && $f instanceof ParameterProviderFilterInterface)) {
36✔
129
                $parameters->add($key, $parameter->withProvider($f->getParameterProvider()));
2✔
130
            }
131

132
            if (':property' === $key) {
36✔
133
                foreach ($propertyNames as $property) {
2✔
134
                    $converted = $this->nameConverter?->denormalize($property) ?? $property;
2✔
135
                    $propertyParameter = $this->setDefaults($converted, $parameter, $resourceClass, $properties, $operation);
2✔
136
                    $priority = $propertyParameter->getPriority() ?? $internalPriority--;
2✔
137
                    $parameters->add($converted, $propertyParameter->withPriority($priority)->withKey($converted));
2✔
138
                }
139

140
                $parameters->remove($key, $parameter::class);
2✔
141
                continue;
2✔
142
            }
143

144
            $key = $parameter->getKey() ?? $key;
36✔
145

146
            if (str_contains($key, ':property') || ((($f = $parameter->getFilter()) && is_a($f, PropertiesAwareInterface::class, true)) || $parameter instanceof PropertiesAwareInterface)) {
36✔
147
                $p = [];
10✔
148
                foreach ($propertyNames as $prop) {
10✔
149
                    $p[$this->nameConverter?->denormalize($prop) ?? $prop] = $prop;
8✔
150
                }
151

152
                $parameter = $parameter->withExtraProperties($parameter->getExtraProperties() + ['_properties' => $p]);
10✔
153
            }
154

155
            $parameter = $this->setDefaults($key, $parameter, $resourceClass, $properties, $operation);
36✔
156
            // We don't do any type cast yet, a query parameter or an header is always a string or a list of strings
157
            if (null === $parameter->getNativeType()) {
36✔
158
                // this forces the type to be only a list
159
                if ('array' === ($parameter->getSchema()['type'] ?? null)) {
32✔
160
                    $parameter = $parameter->withNativeType(Type::list(Type::string()));
2✔
161
                } elseif ('string' === ($parameter->getSchema()['type'] ?? null)) {
32✔
162
                    $parameter = $parameter->withNativeType(Type::string());
12✔
163
                } elseif ('boolean' === ($parameter->getSchema()['type'] ?? null)) {
24✔
164
                    $parameter = $parameter->withNativeType(Type::bool());
6✔
165
                } elseif ('integer' === ($parameter->getSchema()['type'] ?? null)) {
20✔
166
                    $parameter = $parameter->withNativeType(Type::int());
2✔
167
                } elseif ('number' === ($parameter->getSchema()['type'] ?? null)) {
20✔
168
                    $parameter = $parameter->withNativeType(Type::float());
4✔
169
                } else {
170
                    $parameter = $parameter->withNativeType(Type::union(Type::string(), Type::list(Type::string())));
18✔
171
                }
172
            }
173

174
            if ($parameter->getCastToNativeType() && null === $parameter->getCastFn() && ($nativeType = $parameter->getNativeType())) {
36✔
175
                if ($nativeType->isIdentifiedBy(TypeIdentifier::BOOL)) {
4✔
176
                    $parameter = $parameter->withCastFn([ValueCaster::class, 'toBool']);
4✔
177
                }
178
                if ($nativeType->isIdentifiedBy(TypeIdentifier::INT)) {
4✔
179
                    $parameter = $parameter->withCastFn([ValueCaster::class, 'toInt']);
2✔
180
                }
181
                if ($nativeType->isIdentifiedBy(TypeIdentifier::FLOAT)) {
4✔
182
                    $parameter = $parameter->withCastFn([ValueCaster::class, 'toFloat']);
2✔
183
                }
184
            }
185

186
            $priority = $parameter->getPriority() ?? $internalPriority--;
36✔
187
            $parameters->add($key, $parameter->withPriority($priority));
36✔
188
        }
189

190
        return $parameters;
122✔
191
    }
192

193
    private function addFilterMetadata(Parameter $parameter): Parameter
194
    {
195
        if (!($filterId = $parameter->getFilter())) {
36✔
196
            return $parameter;
18✔
197
        }
198

199
        if (!\is_object($filterId) && !$this->filterLocator->has($filterId)) {
22✔
200
            return $parameter;
×
201
        }
202

203
        $filter = \is_object($filterId) ? $filterId : $this->filterLocator->get($filterId);
22✔
204

205
        if ($filter instanceof ParameterProviderFilterInterface) {
22✔
206
            $parameter = $parameter->withProvider($filter::getParameterProvider());
2✔
207
        }
208

209
        if (!$filter) {
22✔
210
            return $parameter;
×
211
        }
212

213
        if (null === $parameter->getSchema() && $filter instanceof JsonSchemaFilterInterface && $schema = $filter->getSchema($parameter)) {
22✔
214
            $parameter = $parameter->withSchema($schema);
18✔
215
        }
216

217
        if (null === $parameter->getOpenApi() && $filter instanceof OpenApiParameterFilterInterface && ($openApiParameter = $filter->getOpenApiParameters($parameter))) {
22✔
218
            $parameter = $parameter->withOpenApi($openApiParameter);
12✔
219
        }
220

221
        return $parameter;
22✔
222
    }
223

224
    /**
225
     * @param array<string, ApiProperty> $properties
226
     */
227
    private function setDefaults(string $key, Parameter $parameter, string $resourceClass, array $properties, Operation $operation): Parameter
228
    {
229
        if (null === $parameter->getKey()) {
36✔
230
            $parameter = $parameter->withKey($key);
×
231
        }
232

233
        $filter = $parameter->getFilter();
36✔
234
        if (\is_string($filter) && $this->filterLocator->has($filter)) {
36✔
235
            $filter = $this->filterLocator->get($filter);
6✔
236
        }
237

238
        if ($filter instanceof SerializerFilterInterface && null === $parameter->getProvider()) {
36✔
239
            $parameter = $parameter->withProvider('api_platform.serializer.filter_parameter_provider');
2✔
240
        }
241
        $currentKey = $key;
36✔
242
        if (null === $parameter->getProperty() && isset($properties[$key])) {
36✔
243
            $parameter = $parameter->withProperty($key);
20✔
244
        }
245

246
        if (null === $parameter->getProperty() && $this->nameConverter && ($nameConvertedKey = $this->nameConverter->normalize($key)) && isset($properties[$nameConvertedKey])) {
36✔
247
            $parameter = $parameter->withProperty($key)->withExtraProperties(['_query_property' => $nameConvertedKey] + $parameter->getExtraProperties());
×
248
            $currentKey = $nameConvertedKey;
×
249
        }
250

251
        if ($this->nameConverter && $property = $parameter->getProperty()) {
36✔
252
            $parameter = $parameter->withProperty($this->nameConverter->normalize($property));
26✔
253
        }
254

255
        if (isset($properties[$currentKey]) && ($eloquentRelation = ($properties[$currentKey]->getExtraProperties()['eloquent_relation'] ?? null)) && isset($eloquentRelation['foreign_key'])) {
36✔
256
            $parameter = $parameter->withExtraProperties(['_query_property' => $eloquentRelation['foreign_key']] + $parameter->getExtraProperties());
×
257
        }
258

259
        $parameter = $this->addFilterMetadata($parameter);
36✔
260

261
        if ($filter instanceof FilterInterface) {
36✔
262
            try {
263
                return $this->getLegacyFilterMetadata($parameter, $operation, $filter);
22✔
264
            } catch (RuntimeException $exception) {
12✔
265
                $this->logger?->alert($exception->getMessage(), ['exception' => $exception]);
12✔
266

267
                return $parameter;
12✔
268
            }
269
        }
270

271
        return $parameter;
18✔
272
    }
273

274
    private function getLegacyFilterMetadata(Parameter $parameter, Operation $operation, FilterInterface $filter): Parameter
275
    {
276
        $description = $filter->getDescription($this->getStateOptionsClass($operation, $operation->getClass()));
22✔
277
        $key = $parameter->getKey();
10✔
278
        if (($schema = $description[$key]['schema'] ?? null) && null === $parameter->getSchema()) {
10✔
279
            $parameter = $parameter->withSchema($schema);
×
280
        }
281

282
        if (null === $parameter->getProperty() && ($property = $description[$key]['property'] ?? null)) {
10✔
283
            $parameter = $parameter->withProperty($property);
×
284
        }
285

286
        if (null === $parameter->getRequired() && ($required = $description[$key]['required'] ?? null)) {
10✔
287
            $parameter = $parameter->withRequired($required);
×
288
        }
289

290
        if (null === $parameter->getOpenApi() && ($openApi = $description[$key]['openapi'] ?? null) && $openApi instanceof OpenApiParameter) {
10✔
291
            $parameter = $parameter->withOpenApi($openApi);
×
292
        }
293

294
        return $parameter;
10✔
295
    }
296
}
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