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

api-platform / core / 10508505187

22 Aug 2024 12:55PM UTC coverage: 7.704%. Remained the same
10508505187

push

github

soyuka
Merge 3.4

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

8840 existing lines in 284 files now uncovered.

12477 of 161949 relevant lines covered (7.7%)

22.99 hits per line

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

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

30
/**
31
 * Creates a resource metadata from {@see ApiResource} annotations.
32
 *
33
 * @author Antoine Bluchet <soyuka@gmail.com>
34
 */
35
final class AttributesResourceMetadataCollectionFactory implements ResourceMetadataCollectionFactoryInterface
36
{
37
    use OperationDefaultsTrait;
38

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

46
    /**
47
     * {@inheritdoc}
48
     */
49
    public function create(string $resourceClass): ResourceMetadataCollection
50
    {
UNCOV
51
        $resourceMetadataCollection = new ResourceMetadataCollection($resourceClass);
42✔
UNCOV
52
        if ($this->decorated) {
42✔
53
            $resourceMetadataCollection = $this->decorated->create($resourceClass);
×
54
        }
55

56
        try {
UNCOV
57
            $reflectionClass = new \ReflectionClass($resourceClass);
42✔
58
        } catch (\ReflectionException) {
×
59
            throw new ResourceClassNotFoundException(\sprintf('Resource "%s" not found.', $resourceClass));
×
60
        }
61

UNCOV
62
        if ($this->hasResourceAttributes($reflectionClass)) {
42✔
UNCOV
63
            foreach ($this->buildResourceOperations($reflectionClass->getAttributes(), $resourceClass) as $resource) {
34✔
UNCOV
64
                $resourceMetadataCollection[] = $resource;
34✔
65
            }
66
        }
67

UNCOV
68
        return $resourceMetadataCollection;
42✔
69
    }
70

71
    /**
72
     * Builds resource operations to support:
73
     * Resource
74
     * Get
75
     * Post
76
     * Resource
77
     * Put
78
     * Get
79
     * In the future, we will be able to use nested attributes (https://wiki.php.net/rfc/new_in_initializers).
80
     *
81
     * @return ApiResource[]
82
     */
83
    private function buildResourceOperations(array $attributes, string $resourceClass): array
84
    {
UNCOV
85
        $shortName = (false !== $pos = strrpos($resourceClass, '\\')) ? substr($resourceClass, $pos + 1) : $resourceClass;
34✔
UNCOV
86
        $resources = [];
34✔
UNCOV
87
        $index = -1;
34✔
UNCOV
88
        $operationPriority = 0;
34✔
UNCOV
89
        $hasApiResource = false;
34✔
UNCOV
90
        $globalParameters = new Parameters();
34✔
91

UNCOV
92
        foreach ($attributes as $attribute) {
34✔
UNCOV
93
            if (is_a($attribute->getName(), Parameter::class, true)) {
34✔
UNCOV
94
                $parameter = $attribute->newInstance();
3✔
UNCOV
95
                if (!$k = $parameter->getKey()) {
3✔
96
                    throw new RuntimeException('Parameter "key" is mandatory when used on a class.');
×
97
                }
UNCOV
98
                $globalParameters->add($k, $parameter);
3✔
UNCOV
99
                continue;
3✔
100
            }
101

UNCOV
102
            if (is_a($attribute->getName(), ApiResource::class, true)) {
34✔
UNCOV
103
                $hasApiResource = true;
31✔
UNCOV
104
                $resource = $this->getResourceWithDefaults($resourceClass, $shortName, $attribute->newInstance());
31✔
UNCOV
105
                $operations = [];
31✔
UNCOV
106
                foreach ($resource->getOperations() ?? new Operations() as $operation) {
31✔
UNCOV
107
                    [$key, $operation] = $this->getOperationWithDefaults($resource, $operation);
20✔
UNCOV
108
                    $operations[$key] = $operation;
20✔
109
                }
UNCOV
110
                if ($operations) {
31✔
UNCOV
111
                    $resource = $resource->withOperations(new Operations($operations));
20✔
112
                }
UNCOV
113
                $resources[++$index] = $resource;
31✔
UNCOV
114
                continue;
31✔
115
            }
116

UNCOV
117
            if (!is_subclass_of($attribute->getName(), HttpOperation::class) && !is_subclass_of($attribute->getName(), GraphQlOperation::class)) {
32✔
UNCOV
118
                continue;
30✔
119
            }
120

UNCOV
121
            $operationAttribute = $attribute->newInstance();
7✔
122

UNCOV
123
            if ($operationAttribute instanceof GraphQlOperation) {
7✔
UNCOV
124
                [$key, $operation] = $this->getOperationWithDefaults($resources[$index], $operationAttribute);
3✔
UNCOV
125
                $graphQlOperations = $resources[$index]->getGraphQlOperations();
3✔
UNCOV
126
                $graphQlOperations[$key] = $operation;
3✔
UNCOV
127
                $resources[$index] = $resources[$index]->withGraphQlOperations($graphQlOperations);
3✔
UNCOV
128
                continue;
3✔
129
            }
130

UNCOV
131
            if (-1 === $index || $this->hasSameOperation($resources[$index], $attribute->getName(), $operationAttribute)) {
7✔
UNCOV
132
                $resources[++$index] = $this->getResourceWithDefaults($resourceClass, $shortName, new ApiResource());
7✔
133
            }
134

UNCOV
135
            [$key, $operation] = $this->getOperationWithDefaults($resources[$index], $operationAttribute);
7✔
UNCOV
136
            if (null === $operation->getPriority()) {
7✔
UNCOV
137
                $operation = $operation->withPriority(++$operationPriority);
7✔
138
            }
UNCOV
139
            $operations = $resources[$index]->getOperations() ?? new Operations();
7✔
UNCOV
140
            $resources[$index] = $resources[$index]->withOperations($operations->add($key, $operation));
7✔
141
        }
142

143
        // Loop again and set default operations if none where found
UNCOV
144
        foreach ($resources as $index => $resource) {
34✔
UNCOV
145
            if (\count($globalParameters) > 0) {
34✔
UNCOV
146
                $resources[$index] = $resource = $this->mergeOperationParameters($resource, $globalParameters);
3✔
147
            }
148

UNCOV
149
            if (null === $resource->getOperations()) {
34✔
UNCOV
150
                $operations = [];
23✔
UNCOV
151
                foreach ($this->getDefaultHttpOperations($resource) as $operation) {
23✔
UNCOV
152
                    [$key, $operation] = $this->getOperationWithDefaults($resource, $operation, true);
23✔
UNCOV
153
                    $operations[$key] = $operation;
23✔
154
                }
UNCOV
155
                $resources[$index] = $resource = $resource->withOperations(new Operations($operations));
23✔
156
            }
157

UNCOV
158
            if ($parameters = $resource->getParameters()) {
34✔
UNCOV
159
                $operations = [];
3✔
UNCOV
160
                foreach ($resource->getOperations() ?? [] as $i => $operation) {
3✔
UNCOV
161
                    $operations[$i] = $this->mergeOperationParameters($operation, $parameters);
3✔
162
                }
UNCOV
163
                $resources[$index] = $resource = $resource->withOperations(new Operations($operations)); // @phpstan-ignore-line
3✔
164
            }
165

UNCOV
166
            $graphQlOperations = $resource->getGraphQlOperations();
34✔
UNCOV
167
            if (!$this->graphQlEnabled) {
34✔
168
                continue;
×
169
            }
170

UNCOV
171
            if (null === $graphQlOperations) {
34✔
UNCOV
172
                if (!$hasApiResource) {
31✔
UNCOV
173
                    $resources[$index] = $resources[$index]->withGraphQlOperations([]);
7✔
UNCOV
174
                    continue;
7✔
175
                }
176

177
                // Add default GraphQL operations on the first resource
UNCOV
178
                if (0 === $index) {
28✔
UNCOV
179
                    $resources[$index] = $this->addDefaultGraphQlOperations($resources[$index]);
24✔
180
                }
UNCOV
181
                continue;
28✔
182
            }
183

UNCOV
184
            $resources[$index] = $this->completeGraphQlOperations($resources[$index]);
11✔
UNCOV
185
            $graphQlOperations = $resources[$index]->getGraphQlOperations();
11✔
186

UNCOV
187
            $graphQlOperationsWithDefaults = [];
11✔
UNCOV
188
            foreach ($graphQlOperations as $operation) {
11✔
UNCOV
189
                [$key, $operation] = $this->getOperationWithDefaults($resource, $operation);
11✔
UNCOV
190
                if ($parameters) {
11✔
191
                    $operation = $this->mergeOperationParameters($operation, $parameters);
×
192
                }
193

UNCOV
194
                $graphQlOperationsWithDefaults[$key] = $operation;
11✔
195
            }
196

UNCOV
197
            $resources[$index] = $resources[$index]->withGraphQlOperations($graphQlOperationsWithDefaults);
11✔
198
        }
199

UNCOV
200
        return $resources;
34✔
201
    }
202

203
    private function hasResourceAttributes(\ReflectionClass $reflectionClass): bool
204
    {
UNCOV
205
        foreach ($reflectionClass->getAttributes() as $attribute) {
42✔
UNCOV
206
            if (is_a($attribute->getName(), ApiResource::class, true) || is_subclass_of($attribute->getName(), HttpOperation::class) || is_subclass_of($attribute->getName(), GraphQlOperation::class)) {
42✔
UNCOV
207
                return true;
34✔
208
            }
209
        }
210

UNCOV
211
        return false;
12✔
212
    }
213

214
    /**
215
     * Does the resource already have an operation of the $operationClass type?
216
     * Useful to determine if we need to create a new ApiResource when the class has only operation attributes, for example:.
217
     *
218
     * #[Get]
219
     * #[Get(uriTemplate: '/alternate')]
220
     * class Example {}
221
     */
222
    private function hasSameOperation(ApiResource $resource, string $operationClass, HttpOperation $operation): bool
223
    {
UNCOV
224
        foreach ($resource->getOperations() ?? [] as $o) {
4✔
UNCOV
225
            if ($o instanceof $operationClass && $operation->getUriTemplate() === $o->getUriTemplate() && $operation->getName() === $o->getName() && $operation->getRouteName() === $o->getRouteName()) {
4✔
226
                return true;
×
227
            }
228
        }
229

UNCOV
230
        return false;
4✔
231
    }
232

233
    /**
234
     * @template T of Metadata
235
     *
236
     * @param T $resource
237
     *
238
     * @return T
239
     */
240
    private function mergeOperationParameters(Metadata $resource, Parameters $globalParameters): Metadata
241
    {
UNCOV
242
        $parameters = $resource->getParameters() ?? new Parameters();
3✔
UNCOV
243
        foreach ($globalParameters as $parameterName => $parameter) {
3✔
UNCOV
244
            if ($key = $parameter->getKey()) {
3✔
UNCOV
245
                $parameterName = $key;
3✔
246
            }
247

UNCOV
248
            if (!$parameters->has($parameterName, $parameter::class)) {
3✔
UNCOV
249
                $parameters->add($parameterName, $parameter);
3✔
250
            }
251
        }
252

UNCOV
253
        return $resource->withParameters($parameters);
3✔
254
    }
255
}
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