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

api-platform / core / 15133993414

20 May 2025 09:30AM UTC coverage: 26.313% (-1.2%) from 27.493%
15133993414

Pull #7161

github

web-flow
Merge e2c03d45f into 5459ba375
Pull Request #7161: fix(metadata): infer parameter string type from schema

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

11019 existing lines in 363 files now uncovered.

12898 of 49018 relevant lines covered (26.31%)

34.33 hits per line

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

96.51
/src/Metadata/Resource/Factory/MetadataCollectionFactoryTrait.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\ApiResource;
17
use ApiPlatform\Metadata\Exception\RuntimeException;
18
use ApiPlatform\Metadata\GraphQl\Operation as GraphQlOperation;
19
use ApiPlatform\Metadata\HttpOperation;
20
use ApiPlatform\Metadata\Metadata;
21
use ApiPlatform\Metadata\Operations;
22
use ApiPlatform\Metadata\Parameter;
23
use ApiPlatform\Metadata\Parameters;
24
use ApiPlatform\Metadata\Util\CamelCaseToSnakeCaseNameConverter;
25
use Psr\Log\LoggerInterface;
26
use Psr\Log\NullLogger;
27

28
/**
29
 * @internal
30
 *
31
 * This trait shares the common logic between attributes and Laravel concerns factories
32
 *
33
 * @author Antoine Bluchet <soyuka@gmail.com>
34
 * @author Kévin Dunglas <kevin@dunglas.dev>
35
 */
36
trait MetadataCollectionFactoryTrait
37
{
38
    use OperationDefaultsTrait;
39

40
    public function __construct(private readonly ?ResourceMetadataCollectionFactoryInterface $decorated = null, ?LoggerInterface $logger = null, array $defaults = [], private readonly bool $graphQlEnabled = false)
41
    {
UNCOV
42
        $this->logger = $logger ?? new NullLogger();
982✔
UNCOV
43
        $this->defaults = $defaults;
982✔
UNCOV
44
        $this->camelCaseToSnakeCaseNameConverter = new CamelCaseToSnakeCaseNameConverter();
982✔
45
    }
46

47
    private function isResourceMetadata(string $name): bool
48
    {
UNCOV
49
        return is_a($name, ApiResource::class, true) || is_subclass_of($name, HttpOperation::class) || is_subclass_of($name, GraphQlOperation::class) || is_a($name, Parameter::class, true);
72✔
50
    }
51

52
    /**
53
     * Builds resource operations to support:
54
     * Resource
55
     * Get
56
     * Post
57
     * Resource
58
     * Get
59
     * In the future, we will be able to use nested attributes (https://wiki.php.net/rfc/new_in_initializers).
60
     *
61
     * @param array<Metadata|Parameter> $metadataCollection
62
     *
63
     * @return ApiResource[]
64
     */
65
    private function buildResourceOperations(array $metadataCollection, string $resourceClass, array $resources = []): array
66
    {
UNCOV
67
        $shortName = (false !== $pos = strrpos($resourceClass, '\\')) ? substr($resourceClass, $pos + 1) : $resourceClass;
72✔
UNCOV
68
        $index = -1;
72✔
UNCOV
69
        $operationPriority = 0;
72✔
UNCOV
70
        $hasApiResource = false;
72✔
UNCOV
71
        $globalParameters = new Parameters();
72✔
72

UNCOV
73
        foreach ($metadataCollection as $metadata) {
72✔
UNCOV
74
            if ($metadata instanceof Parameter) {
68✔
UNCOV
75
                if (!$k = $metadata->getKey()) {
3✔
76
                    throw new RuntimeException('Parameter "key" is mandatory when used on a class.');
×
77
                }
UNCOV
78
                $globalParameters->add($k, $metadata);
3✔
UNCOV
79
                continue;
3✔
80
            }
81

UNCOV
82
            if ($metadata instanceof ApiResource) {
68✔
UNCOV
83
                $hasApiResource = true;
54✔
UNCOV
84
                $resource = $this->getResourceWithDefaults($resourceClass, $shortName, $metadata);
54✔
UNCOV
85
                $operations = [];
54✔
UNCOV
86
                foreach ($resource->getOperations() ?? new Operations() as $operation) {
54✔
UNCOV
87
                    [$key, $operation] = $this->getOperationWithDefaults($resource, $operation);
29✔
UNCOV
88
                    $operations[$key] = $operation;
29✔
89
                }
UNCOV
90
                if ($operations) {
54✔
UNCOV
91
                    $resource = $resource->withOperations(new Operations($operations));
29✔
92
                }
UNCOV
93
                $resources[++$index] = $resource;
54✔
UNCOV
94
                continue;
54✔
95
            }
96

UNCOV
97
            if (!is_subclass_of($metadata, HttpOperation::class) && !is_subclass_of($metadata, GraphQlOperation::class)) {
32✔
98
                continue;
×
99
            }
100

UNCOV
101
            if ($metadata instanceof GraphQlOperation) {
32✔
UNCOV
102
                [$key, $operation] = $this->getOperationWithDefaults($resources[$index], $metadata);
2✔
UNCOV
103
                $graphQlOperations = $resources[$index]->getGraphQlOperations();
2✔
UNCOV
104
                $graphQlOperations[$key] = $operation;
2✔
UNCOV
105
                $resources[$index] = $resources[$index]->withGraphQlOperations($graphQlOperations);
2✔
UNCOV
106
                continue;
2✔
107
            }
108

UNCOV
109
            if (-1 === $index || $this->hasSameOperation($resources[$index], $metadata::class, $metadata)) {
32✔
UNCOV
110
                $resources[++$index] = $this->getResourceWithDefaults($resourceClass, $shortName, new ApiResource());
19✔
111
            }
112

UNCOV
113
            [$key, $operation] = $this->getOperationWithDefaults($resources[$index], $metadata);
32✔
UNCOV
114
            if (null === $operation->getPriority()) {
32✔
UNCOV
115
                $operation = $operation->withPriority(++$operationPriority);
32✔
116
            }
UNCOV
117
            $operations = $resources[$index]->getOperations() ?? new Operations();
32✔
UNCOV
118
            $resources[$index] = $resources[$index]->withOperations($operations->add($key, $operation));
32✔
119
        }
120

121
        // Loop again and set default operations if none where found
UNCOV
122
        foreach ($resources as $index => $resource) {
72✔
UNCOV
123
            if (\count($globalParameters) > 0) {
68✔
UNCOV
124
                $resources[$index] = $resource = $this->mergeOperationParameters($resource, $globalParameters);
3✔
125
            }
126

UNCOV
127
            if (null === $resource->getOperations()) {
68✔
UNCOV
128
                $operations = [];
28✔
UNCOV
129
                foreach ($this->getDefaultHttpOperations($resource) as $operation) {
28✔
UNCOV
130
                    [$key, $operation] = $this->getOperationWithDefaults($resource, $operation, true);
28✔
UNCOV
131
                    $operations[$key] = $operation;
28✔
132
                }
UNCOV
133
                $resources[$index] = $resource = $resource->withOperations(new Operations($operations));
28✔
134
            }
135

UNCOV
136
            if ($parameters = $resource->getParameters()) {
68✔
UNCOV
137
                $operations = [];
3✔
UNCOV
138
                foreach ($resource->getOperations() ?? [] as $i => $operation) {
3✔
UNCOV
139
                    $operations[$i] = $this->mergeOperationParameters($operation, $parameters);
3✔
140
                }
UNCOV
141
                $resources[$index] = $resource = $resource->withOperations(new Operations($operations)); // @phpstan-ignore-line
3✔
142
            }
143

UNCOV
144
            if (!$this->graphQlEnabled) {
68✔
UNCOV
145
                continue;
3✔
146
            }
147

UNCOV
148
            $graphQlOperations = $resource->getGraphQlOperations();
65✔
UNCOV
149
            if (null === $graphQlOperations) {
65✔
UNCOV
150
                if (!$hasApiResource) {
62✔
UNCOV
151
                    $resources[$index] = $resources[$index]->withGraphQlOperations([]);
18✔
UNCOV
152
                    continue;
18✔
153
                }
154

155
                // Add default GraphQL operations on the first resource
UNCOV
156
                if (0 === $index) {
48✔
UNCOV
157
                    $resources[$index] = $this->addDefaultGraphQlOperations($resources[$index]);
44✔
158
                }
UNCOV
159
                continue;
48✔
160
            }
161

UNCOV
162
            $resources[$index] = $this->completeGraphQlOperations($resources[$index]);
11✔
UNCOV
163
            $graphQlOperations = $resources[$index]->getGraphQlOperations();
11✔
164

UNCOV
165
            $graphQlOperationsWithDefaults = [];
11✔
UNCOV
166
            foreach ($graphQlOperations as $operation) {
11✔
UNCOV
167
                [$key, $operation] = $this->getOperationWithDefaults($resource, $operation);
11✔
UNCOV
168
                if ($parameters) {
11✔
UNCOV
169
                    $operation = $this->mergeOperationParameters($operation, $parameters);
2✔
170
                }
171

UNCOV
172
                $graphQlOperationsWithDefaults[$key] = $operation;
11✔
173
            }
174

UNCOV
175
            $resources[$index] = $resources[$index]->withGraphQlOperations($graphQlOperationsWithDefaults);
11✔
176
        }
177

UNCOV
178
        return $resources;
72✔
179
    }
180

181
    /**
182
     * Does the resource already have an operation of the $operationClass type?
183
     * Useful to determine if we need to create a new ApiResource when the class has only operation attributes, for example:.
184
     *
185
     * #[Get]
186
     * #[Get(uriTemplate: '/alternate')]
187
     * class Example {}
188
     */
189
    private function hasSameOperation(ApiResource $resource, string $operationClass, HttpOperation $operation): bool
190
    {
UNCOV
191
        foreach ($resource->getOperations() ?? [] as $o) {
17✔
UNCOV
192
            if ($o instanceof $operationClass && $operation->getUriTemplate() === $o->getUriTemplate() && $operation->getName() === $o->getName() && $operation->getRouteName() === $o->getRouteName()) {
9✔
193
                return true;
×
194
            }
195
        }
196

UNCOV
197
        return false;
17✔
198
    }
199

200
    /**
201
     * @template T of Metadata
202
     *
203
     * @param T $resource
204
     *
205
     * @return T
206
     */
207
    private function mergeOperationParameters(Metadata $resource, Parameters $globalParameters): Metadata
208
    {
UNCOV
209
        $parameters = $resource->getParameters() ?? new Parameters();
3✔
UNCOV
210
        foreach ($globalParameters as $parameterName => $parameter) {
3✔
UNCOV
211
            if ($key = $parameter->getKey()) {
3✔
UNCOV
212
                $parameterName = $key;
3✔
213
            }
214

UNCOV
215
            if (!$parameters->has($parameterName, $parameter::class)) {
3✔
UNCOV
216
                $parameters->add($parameterName, $parameter);
3✔
217
            }
218
        }
219

UNCOV
220
        return $resource->withParameters($parameters);
3✔
221
    }
222
}
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