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

api-platform / core / 10557514433

26 Aug 2024 09:45AM UTC coverage: 7.704% (-0.003%) from 7.707%
10557514433

push

github

web-flow
feat(laravel): provide a trait in addition to the annotation (#6543)

* feat(laravel): provide a trait in addition to the annotation

* fix

* fix

* add support for ApiProperty and refactoring

* fix Eloquent property collection

* fix PHPStan

8 of 179 new or added lines in 11 files covered. (4.47%)

2 existing lines in 2 files now uncovered.

12489 of 162109 relevant lines covered (7.7%)

22.98 hits per line

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

95.4
/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
    {
NEW
42
        $this->logger = $logger ?? new NullLogger();
2,248✔
NEW
43
        $this->defaults = $defaults;
2,248✔
NEW
44
        $this->camelCaseToSnakeCaseNameConverter = new CamelCaseToSnakeCaseNameConverter();
2,248✔
45
    }
46

47
    private function isResourceMetadata(string $name): bool
48
    {
NEW
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);
42✔
50
    }
51

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

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

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

NEW
99
            if (!is_subclass_of($metadata, HttpOperation::class) && !is_subclass_of($metadata, GraphQlOperation::class)) {
7✔
NEW
100
                continue;
×
101
            }
102

NEW
103
            if ($metadata instanceof GraphQlOperation) {
7✔
NEW
104
                [$key, $operation] = $this->getOperationWithDefaults($resources[$index], $metadata);
3✔
NEW
105
                $graphQlOperations = $resources[$index]->getGraphQlOperations();
3✔
NEW
106
                $graphQlOperations[$key] = $operation;
3✔
NEW
107
                $resources[$index] = $resources[$index]->withGraphQlOperations($graphQlOperations);
3✔
NEW
108
                continue;
3✔
109
            }
110

NEW
111
            if (-1 === $index || $this->hasSameOperation($resources[$index], $metadata::class, $metadata)) {
7✔
NEW
112
                $resources[++$index] = $this->getResourceWithDefaults($resourceClass, $shortName, new ApiResource());
7✔
113
            }
114

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

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

NEW
129
            if (null === $resource->getOperations()) {
34✔
NEW
130
                $operations = [];
23✔
NEW
131
                foreach ($this->getDefaultHttpOperations($resource) as $operation) {
23✔
NEW
132
                    [$key, $operation] = $this->getOperationWithDefaults($resource, $operation, true);
23✔
NEW
133
                    $operations[$key] = $operation;
23✔
134
                }
NEW
135
                $resources[$index] = $resource = $resource->withOperations(new Operations($operations));
23✔
136
            }
137

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

NEW
146
            $graphQlOperations = $resource->getGraphQlOperations();
34✔
NEW
147
            if (!$this->graphQlEnabled) {
34✔
NEW
148
                continue;
×
149
            }
150

NEW
151
            if (null === $graphQlOperations) {
34✔
NEW
152
                if (!$hasApiResource) {
31✔
NEW
153
                    $resources[$index] = $resources[$index]->withGraphQlOperations([]);
7✔
NEW
154
                    continue;
7✔
155
                }
156

157
                // Add default GraphQL operations on the first resource
NEW
158
                if (0 === $index) {
28✔
NEW
159
                    $resources[$index] = $this->addDefaultGraphQlOperations($resources[$index]);
24✔
160
                }
NEW
161
                continue;
28✔
162
            }
163

NEW
164
            $resources[$index] = $this->completeGraphQlOperations($resources[$index]);
11✔
NEW
165
            $graphQlOperations = $resources[$index]->getGraphQlOperations();
11✔
166

NEW
167
            $graphQlOperationsWithDefaults = [];
11✔
NEW
168
            foreach ($graphQlOperations as $operation) {
11✔
NEW
169
                [$key, $operation] = $this->getOperationWithDefaults($resource, $operation);
11✔
NEW
170
                if ($parameters) {
11✔
NEW
171
                    $operation = $this->mergeOperationParameters($operation, $parameters);
3✔
172
                }
173

NEW
174
                $graphQlOperationsWithDefaults[$key] = $operation;
11✔
175
            }
176

NEW
177
            $resources[$index] = $resources[$index]->withGraphQlOperations($graphQlOperationsWithDefaults);
11✔
178
        }
179

NEW
180
        return $resources;
42✔
181
    }
182

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

NEW
199
        return false;
4✔
200
    }
201

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

NEW
217
            if (!$parameters->has($parameterName, $parameter::class)) {
3✔
NEW
218
                $parameters->add($parameterName, $parameter);
3✔
219
            }
220
        }
221

NEW
222
        return $resource->withParameters($parameters);
3✔
223
    }
224
}
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