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

api-platform / core / 7142557150

08 Dec 2023 02:28PM UTC coverage: 36.003% (-1.4%) from 37.36%
7142557150

push

github

web-flow
fix(jsonld): remove link to ApiDocumentation when doc is disabled (#6029)

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

2297 existing lines in 182 files now uncovered.

9992 of 27753 relevant lines covered (36.0%)

147.09 hits per line

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

93.59
/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\GraphQl\Operation as GraphQlOperation;
19
use ApiPlatform\Metadata\HttpOperation;
20
use ApiPlatform\Metadata\Operations;
21
use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
22
use ApiPlatform\Metadata\Util\CamelCaseToSnakeCaseNameConverter;
23
use Psr\Log\LoggerInterface;
24
use Psr\Log\NullLogger;
25

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

35
    public function __construct(private readonly ?ResourceMetadataCollectionFactoryInterface $decorated = null, LoggerInterface $logger = null, array $defaults = [], private readonly bool $graphQlEnabled = false)
36
    {
37
        $this->logger = $logger ?? new NullLogger();
2,499✔
38
        $this->defaults = $defaults;
2,499✔
39
        $this->camelCaseToSnakeCaseNameConverter = new CamelCaseToSnakeCaseNameConverter();
2,499✔
40
    }
41

42
    /**
43
     * {@inheritdoc}
44
     */
45
    public function create(string $resourceClass): ResourceMetadataCollection
46
    {
47
        $resourceMetadataCollection = new ResourceMetadataCollection($resourceClass);
84✔
48
        if ($this->decorated) {
84✔
49
            $resourceMetadataCollection = $this->decorated->create($resourceClass);
×
50
        }
51

52
        try {
53
            $reflectionClass = new \ReflectionClass($resourceClass);
84✔
UNCOV
54
        } catch (\ReflectionException) {
×
UNCOV
55
            throw new ResourceClassNotFoundException(sprintf('Resource "%s" not found.', $resourceClass));
×
56
        }
57

58
        if ($this->hasResourceAttributes($reflectionClass)) {
84✔
59
            foreach ($this->buildResourceOperations($reflectionClass->getAttributes(), $resourceClass) as $resource) {
78✔
60
                $resourceMetadataCollection[] = $resource;
78✔
61
            }
62
        }
63

64
        return $resourceMetadataCollection;
84✔
65
    }
66

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

87
        foreach ($attributes as $attribute) {
78✔
88
            if (is_a($attribute->getName(), ApiResource::class, true)) {
78✔
89
                $hasApiResource = true;
78✔
90
                $resource = $this->getResourceWithDefaults($resourceClass, $shortName, $attribute->newInstance());
78✔
91
                $operations = [];
78✔
92
                foreach ($resource->getOperations() ?? new Operations() as $operation) {
78✔
93
                    [$key, $operation] = $this->getOperationWithDefaults($resource, $operation);
45✔
94
                    $operations[$key] = $operation;
45✔
95
                }
96
                if ($operations) {
78✔
97
                    $resource = $resource->withOperations(new Operations($operations));
45✔
98
                }
99
                $resources[++$index] = $resource;
78✔
100
                continue;
78✔
101
            }
102

103
            if (!is_subclass_of($attribute->getName(), HttpOperation::class) && !is_subclass_of($attribute->getName(), GraphQlOperation::class)) {
72✔
104
                continue;
72✔
105
            }
106

107
            $operationAttribute = $attribute->newInstance();
6✔
108

109
            if ($operationAttribute instanceof GraphQlOperation) {
6✔
110
                [$key, $operation] = $this->getOperationWithDefaults($resources[$index], $operationAttribute);
3✔
111
                $graphQlOperations = $resources[$index]->getGraphQlOperations();
3✔
112
                $graphQlOperations[$key] = $operation;
3✔
113
                $resources[$index] = $resources[$index]->withGraphQlOperations($graphQlOperations);
3✔
114
                continue;
3✔
115
            }
116

117
            if (-1 === $index || $this->hasSameOperation($resources[$index], $attribute->getName(), $operationAttribute)) {
6✔
118
                $resources[++$index] = $this->getResourceWithDefaults($resourceClass, $shortName, new ApiResource());
6✔
119
            }
120

121
            [$key, $operation] = $this->getOperationWithDefaults($resources[$index], $operationAttribute);
6✔
122
            $operation = $operation->withPriority(++$operationPriority);
6✔
123
            $operations = $resources[$index]->getOperations() ?? new Operations();
6✔
124
            $resources[$index] = $resources[$index]->withOperations($operations->add($key, $operation)->sort());
6✔
125
        }
126

127
        // Loop again and set default operations if none where found
128
        foreach ($resources as $index => $resource) {
78✔
129
            if (null === $resource->getOperations()) {
78✔
130
                $operations = [];
54✔
131
                foreach ($this->getDefaultHttpOperations($resource) as $operation) {
54✔
132
                    [$key, $operation] = $this->getOperationWithDefaults($resource, $operation, true);
54✔
133
                    $operations[$key] = $operation;
54✔
134
                }
135
                $resources[$index] = $resource->withOperations(new Operations($operations));
54✔
136
            }
137

138
            $graphQlOperations = $resource->getGraphQlOperations();
78✔
139

140
            if (!$this->graphQlEnabled) {
78✔
UNCOV
141
                continue;
×
142
            }
143

144
            if (null === $graphQlOperations) {
78✔
145
                if (!$hasApiResource) {
66✔
146
                    $resources[$index] = $resources[$index]->withGraphQlOperations([]);
6✔
147
                    continue;
6✔
148
                }
149

150
                // Add default GraphQL operations on the first resource
151
                if (0 === $index) {
66✔
152
                    $resources[$index] = $this->addDefaultGraphQlOperations($resources[$index]);
63✔
153
                }
154
                continue;
66✔
155
            }
156

157
            $resources[$index] = $this->completeGraphQlOperations($resources[$index]);
21✔
158
            $graphQlOperations = $resources[$index]->getGraphQlOperations();
21✔
159

160
            $graphQlOperationsWithDefaults = [];
21✔
161
            foreach ($graphQlOperations as $operation) {
21✔
162
                [$key, $operation] = $this->getOperationWithDefaults($resource, $operation);
21✔
163
                $graphQlOperationsWithDefaults[$key] = $operation;
21✔
164
            }
165

166
            $resources[$index] = $resources[$index]->withGraphQlOperations($graphQlOperationsWithDefaults);
21✔
167
        }
168

169
        return $resources;
78✔
170
    }
171

172
    private function hasResourceAttributes(\ReflectionClass $reflectionClass): bool
173
    {
174
        foreach ($reflectionClass->getAttributes() as $attribute) {
84✔
175
            if (is_a($attribute->getName(), ApiResource::class, true) || is_subclass_of($attribute->getName(), HttpOperation::class) || is_subclass_of($attribute->getName(), GraphQlOperation::class)) {
84✔
176
                return true;
78✔
177
            }
178
        }
179

180
        return false;
12✔
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
    {
193
        foreach ($resource->getOperations() ?? [] as $o) {
6✔
194
            if ($o instanceof $operationClass && $operation->getUriTemplate() === $o->getUriTemplate() && $operation->getName() === $o->getName() && $operation->getRouteName() === $o->getRouteName()) {
6✔
195
                return true;
×
196
            }
197
        }
198

199
        return false;
6✔
200
    }
201
}
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

© 2026 Coveralls, Inc