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

api-platform / core / 14400124412

11 Apr 2025 09:31AM UTC coverage: 8.488%. Remained the same
14400124412

push

github

web-flow
fix(metadata): parameter provider within filter (#7081)

2 of 5 new or added lines in 2 files covered. (40.0%)

126 existing lines in 12 files now uncovered.

13402 of 157890 relevant lines covered (8.49%)

22.86 hits per line

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

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

37
/**
38
 * Prepares Parameters documentation by reading its filter details and declaring an OpenApi parameter.
39
 *
40
 * @experimental
41
 */
42
final class ParameterResourceMetadataCollectionFactory implements ResourceMetadataCollectionFactoryInterface
43
{
44
    private array $localPropertyCache;
45

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

56
    public function create(string $resourceClass): ResourceMetadataCollection
57
    {
58
        $resourceMetadataCollection = $this->decorated?->create($resourceClass) ?? new ResourceMetadataCollection($resourceClass);
133✔
59

60
        foreach ($resourceMetadataCollection as $i => $resource) {
133✔
61
            $operations = $resource->getOperations();
126✔
62

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

71
            $resourceMetadataCollection[$i] = $resource->withOperations($operations->sort());
126✔
72

73
            if (!$graphQlOperations = $resource->getGraphQlOperations()) {
126✔
74
                continue;
54✔
75
            }
76

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

85
            $resourceMetadataCollection[$i] = $resource->withGraphQlOperations($graphQlOperations);
107✔
86
        }
87

88
        return $resourceMetadataCollection;
133✔
89
    }
90

91
    /**
92
     * @return array{propertyNames: string[], properties: array<string, ApiProperty>}
93
     */
94
    private function getProperties(string $resourceClass): array
95
    {
96
        if (isset($this->localPropertyCache[$resourceClass])) {
126✔
97
            return $this->localPropertyCache[$resourceClass];
109✔
98
        }
99

100
        $propertyNames = [];
126✔
101
        $properties = [];
126✔
102
        foreach ($this->propertyNameCollectionFactory->create($resourceClass) as $property) {
126✔
103
            $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $property);
116✔
104
            if ($propertyMetadata->isReadable()) {
116✔
105
                $propertyNames[] = $property;
116✔
106
                $properties[$property] = $propertyMetadata;
116✔
107
            }
108
        }
109

110
        $this->localPropertyCache = [$resourceClass => ['propertyNames' => $propertyNames, 'properties' => $properties]];
126✔
111

112
        return $this->localPropertyCache[$resourceClass];
126✔
113
    }
114

115
    private function getDefaultParameters(Operation $operation, string $resourceClass, int &$internalPriority): Parameters
116
    {
117
        ['propertyNames' => $propertyNames, 'properties' => $properties] = $this->getProperties($resourceClass);
126✔
118
        $parameters = $operation->getParameters() ?? new Parameters();
126✔
119
        foreach ($parameters as $key => $parameter) {
126✔
120
            if (null === $parameter->getProvider() && (($f = $parameter->getFilter()) && $f instanceof ParameterProviderFilterInterface)) {
29✔
NEW
121
                $parameters->add($key, $parameter->withProvider($f->getParameterProvider()));
×
122
            }
123

124
            if (':property' === $key) {
29✔
125
                foreach ($propertyNames as $property) {
4✔
126
                    $converted = $this->nameConverter?->denormalize($property) ?? $property;
4✔
127
                    $propertyParameter = $this->setDefaults($converted, $parameter, $resourceClass, $properties, $operation);
4✔
128
                    $priority = $propertyParameter->getPriority() ?? $internalPriority--;
4✔
129
                    $parameters->add($converted, $propertyParameter->withPriority($priority)->withKey($converted));
4✔
130
                }
131

132
                $parameters->remove($key, $parameter::class);
4✔
133
                continue;
4✔
134
            }
135

136
            $key = $parameter->getKey() ?? $key;
29✔
137

138
            if (str_contains($key, ':property') || ((($f = $parameter->getFilter()) && is_a($f, PropertiesAwareInterface::class, true)) || $parameter instanceof PropertiesAwareInterface)) {
29✔
139
                $p = [];
10✔
140
                foreach ($propertyNames as $prop) {
10✔
141
                    $p[$this->nameConverter?->denormalize($prop) ?? $prop] = $prop;
8✔
142
                }
143

144
                $parameter = $parameter->withExtraProperties($parameter->getExtraProperties() + ['_properties' => $p]);
10✔
145
            }
146

147
            $parameter = $this->setDefaults($key, $parameter, $resourceClass, $properties, $operation);
29✔
148
            $priority = $parameter->getPriority() ?? $internalPriority--;
29✔
149
            $parameters->add($key, $parameter->withPriority($priority));
29✔
150
        }
151

152
        return $parameters;
126✔
153
    }
154

155
    private function addFilterMetadata(Parameter $parameter): Parameter
156
    {
157
        if (!($filterId = $parameter->getFilter())) {
29✔
158
            return $parameter;
14✔
159
        }
160

161
        if (!\is_object($filterId) && !$this->filterLocator->has($filterId)) {
21✔
162
            return $parameter;
×
163
        }
164

165
        $filter = \is_object($filterId) ? $filterId : $this->filterLocator->get($filterId);
21✔
166

167
        if ($filter instanceof ParameterProviderFilterInterface) {
21✔
168
            $parameter = $parameter->withProvider($filter::getParameterProvider());
×
169
        }
170

171
        if (!$filter) {
21✔
172
            return $parameter;
×
173
        }
174

175
        if (null === $parameter->getSchema() && $filter instanceof JsonSchemaFilterInterface && $schema = $filter->getSchema($parameter)) {
21✔
176
            $parameter = $parameter->withSchema($schema);
19✔
177
        }
178

179
        if (null === $parameter->getOpenApi() && $filter instanceof OpenApiParameterFilterInterface && ($openApiParameter = $filter->getOpenApiParameters($parameter))) {
21✔
180
            $parameter = $parameter->withOpenApi($openApiParameter);
13✔
181
        }
182

183
        return $parameter;
21✔
184
    }
185

186
    /**
187
     * @param array<string, ApiProperty> $properties
188
     */
189
    private function setDefaults(string $key, Parameter $parameter, string $resourceClass, array $properties, Operation $operation): Parameter
190
    {
191
        if (null === $parameter->getKey()) {
29✔
192
            $parameter = $parameter->withKey($key);
29✔
193
        }
194

195
        $filter = $parameter->getFilter();
29✔
196
        if (\is_string($filter) && $this->filterLocator->has($filter)) {
29✔
197
            $filter = $this->filterLocator->get($filter);
9✔
198
        }
199

200
        if ($filter instanceof SerializerFilterInterface && null === $parameter->getProvider()) {
29✔
201
            $parameter = $parameter->withProvider('api_platform.serializer.filter_parameter_provider');
4✔
202
        }
203
        $currentKey = $key;
29✔
204
        if (null === $parameter->getProperty() && isset($properties[$key])) {
29✔
205
            $parameter = $parameter->withProperty($key);
19✔
206
        }
207

208
        if (null === $parameter->getProperty() && $this->nameConverter && ($nameConvertedKey = $this->nameConverter->normalize($key)) && isset($properties[$nameConvertedKey])) {
29✔
209
            $parameter = $parameter->withProperty($key)->withExtraProperties(['_query_property' => $nameConvertedKey] + $parameter->getExtraProperties());
×
210
            $currentKey = $nameConvertedKey;
×
211
        }
212

213
        if ($this->nameConverter && $property = $parameter->getProperty()) {
29✔
214
            $parameter = $parameter->withProperty($this->nameConverter->normalize($property));
21✔
215
        }
216

217
        if (isset($properties[$currentKey]) && ($eloquentRelation = ($properties[$currentKey]->getExtraProperties()['eloquent_relation'] ?? null)) && isset($eloquentRelation['foreign_key'])) {
29✔
218
            $parameter = $parameter->withExtraProperties(['_query_property' => $eloquentRelation['foreign_key']] + $parameter->getExtraProperties());
×
219
        }
220

221
        $parameter = $this->addFilterMetadata($parameter);
29✔
222

223
        if ($filter instanceof FilterInterface) {
29✔
224
            try {
225
                return $this->getLegacyFilterMetadata($parameter, $operation, $filter);
21✔
226
            } catch (RuntimeException $exception) {
14✔
227
                $this->logger?->alert($exception->getMessage(), ['exception' => $exception]);
14✔
228

229
                return $parameter;
14✔
230
            }
231
        }
232

233
        return $parameter;
14✔
234
    }
235

236
    private function getLegacyFilterMetadata(Parameter $parameter, Operation $operation, FilterInterface $filter): Parameter
237
    {
238
        $description = $filter->getDescription($this->getFilterClass($operation));
21✔
239
        $key = $parameter->getKey();
9✔
240
        if (($schema = $description[$key]['schema'] ?? null) && null === $parameter->getSchema()) {
9✔
241
            $parameter = $parameter->withSchema($schema);
×
242
        }
243

244
        if (null === $parameter->getProperty() && ($property = $description[$key]['property'] ?? null)) {
9✔
245
            $parameter = $parameter->withProperty($property);
×
246
        }
247

248
        if (null === $parameter->getRequired() && ($required = $description[$key]['required'] ?? null)) {
9✔
249
            $parameter = $parameter->withRequired($required);
×
250
        }
251

252
        if (null === $parameter->getOpenApi() && ($openApi = $description[$key]['openapi'] ?? null) && $openApi instanceof OpenApiParameter) {
9✔
253
            $parameter = $parameter->withOpenApi($openApi);
×
254
        }
255

256
        return $parameter;
9✔
257
    }
258

259
    private function getFilterClass(Operation $operation): ?string
260
    {
261
        $stateOptions = $operation->getStateOptions();
21✔
262
        if ($stateOptions instanceof DoctrineORMOptions) {
21✔
263
            return $stateOptions->getEntityClass();
6✔
264
        }
265
        if ($stateOptions instanceof DoctrineODMOptions) {
21✔
266
            return $stateOptions->getDocumentClass();
4✔
267
        }
268

269
        return $operation->getClass();
18✔
270
    }
271
}
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