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

api-platform / core / 17723449516

15 Sep 2025 05:52AM UTC coverage: 0.0% (-22.6%) from 22.578%
17723449516

Pull #7383

github

web-flow
Merge fa5b61e35 into 949c3c975
Pull Request #7383: fix(metadata): compute isWritable during updates

0 of 6 new or added lines in 4 files covered. (0.0%)

11356 existing lines in 371 files now uncovered.

0 of 48868 relevant lines covered (0.0%)

0.0 hits per line

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

0.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
 * @experimental
43
 */
44
final class ParameterResourceMetadataCollectionFactory implements ResourceMetadataCollectionFactoryInterface
45
{
46
    use StateOptionsTrait;
47

48
    private array $localPropertyCache;
49

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

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

UNCOV
64
        foreach ($resourceMetadataCollection as $i => $resource) {
×
UNCOV
65
            $operations = $resource->getOperations();
×
66

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

UNCOV
75
            $resourceMetadataCollection[$i] = $resource->withOperations($operations->sort());
×
76

UNCOV
77
            if (!$graphQlOperations = $resource->getGraphQlOperations()) {
×
UNCOV
78
                continue;
×
79
            }
80

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

UNCOV
89
            $resourceMetadataCollection[$i] = $resource->withGraphQlOperations($graphQlOperations);
×
90
        }
91

UNCOV
92
        return $resourceMetadataCollection;
×
93
    }
94

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

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

UNCOV
115
        $this->localPropertyCache[$k] = ['propertyNames' => $propertyNames, 'properties' => $properties];
×
116

UNCOV
117
        return $this->localPropertyCache[$k];
×
118
    }
119

120
    private function getDefaultParameters(Operation $operation, string $resourceClass, int &$internalPriority): Parameters
121
    {
UNCOV
122
        $propertyNames = $properties = [];
×
UNCOV
123
        $parameters = $operation->getParameters() ?? new Parameters();
×
UNCOV
124
        foreach ($parameters as $key => $parameter) {
×
UNCOV
125
            ['propertyNames' => $propertyNames, 'properties' => $properties] = $this->getProperties($resourceClass, $parameter);
×
UNCOV
126
            if (null === $parameter->getProvider() && (($f = $parameter->getFilter()) && $f instanceof ParameterProviderFilterInterface)) {
×
127
                $parameters->add($key, $parameter->withProvider($f->getParameterProvider()));
×
128
            }
129

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

UNCOV
138
                $parameters->remove($key, $parameter::class);
×
UNCOV
139
                continue;
×
140
            }
141

UNCOV
142
            $key = $parameter->getKey() ?? $key;
×
143

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

UNCOV
150
                $parameter = $parameter->withExtraProperties($parameter->getExtraProperties() + ['_properties' => $p]);
×
151
            }
152

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

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

UNCOV
184
            $priority = $parameter->getPriority() ?? $internalPriority--;
×
UNCOV
185
            $parameters->add($key, $parameter->withPriority($priority));
×
186
        }
187

UNCOV
188
        return $parameters;
×
189
    }
190

191
    private function addFilterMetadata(Parameter $parameter): Parameter
192
    {
UNCOV
193
        if (!($filterId = $parameter->getFilter())) {
×
UNCOV
194
            return $parameter;
×
195
        }
196

UNCOV
197
        if (!\is_object($filterId) && !$this->filterLocator->has($filterId)) {
×
198
            return $parameter;
×
199
        }
200

UNCOV
201
        $filter = \is_object($filterId) ? $filterId : $this->filterLocator->get($filterId);
×
202

UNCOV
203
        if ($filter instanceof ParameterProviderFilterInterface) {
×
204
            $parameter = $parameter->withProvider($filter::getParameterProvider());
×
205
        }
206

UNCOV
207
        if (!$filter) {
×
208
            return $parameter;
×
209
        }
210

UNCOV
211
        if (null === $parameter->getSchema() && $filter instanceof JsonSchemaFilterInterface && $schema = $filter->getSchema($parameter)) {
×
UNCOV
212
            $parameter = $parameter->withSchema($schema);
×
213
        }
214

UNCOV
215
        if (null === $parameter->getOpenApi() && $filter instanceof OpenApiParameterFilterInterface && ($openApiParameter = $filter->getOpenApiParameters($parameter))) {
×
UNCOV
216
            $parameter = $parameter->withOpenApi($openApiParameter);
×
217
        }
218

UNCOV
219
        return $parameter;
×
220
    }
221

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

UNCOV
231
        $filter = $parameter->getFilter();
×
UNCOV
232
        if (\is_string($filter) && $this->filterLocator->has($filter)) {
×
UNCOV
233
            $filter = $this->filterLocator->get($filter);
×
234
        }
235

UNCOV
236
        if ($filter instanceof SerializerFilterInterface && null === $parameter->getProvider()) {
×
UNCOV
237
            $parameter = $parameter->withProvider('api_platform.serializer.filter_parameter_provider');
×
238
        }
UNCOV
239
        $currentKey = $key;
×
UNCOV
240
        if (null === $parameter->getProperty() && isset($properties[$key])) {
×
UNCOV
241
            $parameter = $parameter->withProperty($key);
×
242
        }
243

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

UNCOV
249
        if ($this->nameConverter && $property = $parameter->getProperty()) {
×
UNCOV
250
            $parameter = $parameter->withProperty($this->nameConverter->normalize($property));
×
251
        }
252

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

UNCOV
257
        $parameter = $this->addFilterMetadata($parameter);
×
258

UNCOV
259
        if ($filter instanceof FilterInterface) {
×
260
            try {
UNCOV
261
                return $this->getLegacyFilterMetadata($parameter, $operation, $filter);
×
UNCOV
262
            } catch (RuntimeException $exception) {
×
UNCOV
263
                $this->logger?->alert($exception->getMessage(), ['exception' => $exception]);
×
264

UNCOV
265
                return $parameter;
×
266
            }
267
        }
268

UNCOV
269
        return $parameter;
×
270
    }
271

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

UNCOV
280
        if (null === $parameter->getProperty() && ($property = $description[$key]['property'] ?? null)) {
×
281
            $parameter = $parameter->withProperty($property);
×
282
        }
283

UNCOV
284
        if (null === $parameter->getRequired() && ($required = $description[$key]['required'] ?? null)) {
×
285
            $parameter = $parameter->withRequired($required);
×
286
        }
287

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

UNCOV
292
        return $parameter;
×
293
    }
294
}
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

© 2025 Coveralls, Inc