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

api-platform / core / 21001095414

14 Jan 2026 04:10PM UTC coverage: 0.0% (-29.1%) from 29.095%
21001095414

Pull #7595

github

web-flow
Merge 620cc601d into 73402fc61
Pull Request #7595: feat: mcp bundle tool integration

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

15042 existing lines in 491 files now uncovered.

0 of 58241 relevant lines covered (0.0%)

0.0 hits per line

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

0.0
/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\McpResource;
21
use ApiPlatform\Metadata\McpTool;
22
use ApiPlatform\Metadata\Metadata;
23
use ApiPlatform\Metadata\Operations;
24
use ApiPlatform\Metadata\Parameter;
25
use ApiPlatform\Metadata\Parameters;
26
use ApiPlatform\Metadata\Util\CamelCaseToSnakeCaseNameConverter;
27
use Psr\Log\LoggerInterface;
28
use Psr\Log\NullLogger;
29

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

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

49
    private function isResourceMetadata(string $name): bool
50
    {
NEW
51
        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) || is_a($name, McpTool::class, true) || is_a($name, McpResource::class, true);
×
52
    }
53

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

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

UNCOV
84
            if ($metadata instanceof ApiResource) {
×
UNCOV
85
                $hasApiResource = true;
×
UNCOV
86
                $resource = $this->getResourceWithDefaults($resourceClass, $shortName, $metadata);
×
UNCOV
87
                $operations = [];
×
UNCOV
88
                foreach ($resource->getOperations() ?? new Operations() as $operation) {
×
UNCOV
89
                    [$key, $operation] = $this->getOperationWithDefaults($resource, $operation);
×
UNCOV
90
                    $operations[$key] = $operation;
×
91
                }
UNCOV
92
                if ($operations) {
×
UNCOV
93
                    $resource = $resource->withOperations(new Operations($operations));
×
94
                }
95

NEW
96
                if ($mcp = $resource->getMcp()) {
×
NEW
97
                    $processedMcp = [];
×
NEW
98
                    foreach ($mcp as $key => $mcpOperation) {
×
NEW
99
                        if (null === $mcpOperation->getName()) {
×
NEW
100
                            $mcpOperation = $mcpOperation->withName($key);
×
101
                        }
102

NEW
103
                        [, $mcpOperation] = $this->getOperationWithDefaults($resource, $mcpOperation);
×
NEW
104
                        $processedMcp[$key] = $mcpOperation;
×
105
                    }
NEW
106
                    $resource = $resource->withMcp($processedMcp);
×
107
                }
108

NEW
109
                $resources[++$index] = $resource;
×
UNCOV
110
                continue;
×
111
            }
112

UNCOV
113
            if ($metadata instanceof GraphQlOperation) {
×
UNCOV
114
                [$key, $operation] = $this->getOperationWithDefaults($resources[$index], $metadata);
×
UNCOV
115
                $graphQlOperations = $resources[$index]->getGraphQlOperations();
×
UNCOV
116
                $graphQlOperations[$key] = $operation;
×
UNCOV
117
                $resources[$index] = $resources[$index]->withGraphQlOperations($graphQlOperations);
×
UNCOV
118
                continue;
×
119
            }
120

NEW
121
            if ($metadata instanceof McpTool || $metadata instanceof McpResource) {
×
NEW
122
                if (-1 === $index) {
×
NEW
123
                    $resources[++$index] = $this->getResourceWithDefaults($resourceClass, $shortName, new ApiResource());
×
124
                }
NEW
125
                [$key, $operation] = $this->getOperationWithDefaults($resources[$index], $metadata);
×
NEW
126
                $mcp = $resources[$index]->getMcp() ?? [];
×
NEW
127
                $mcp[$key] = $operation;
×
NEW
128
                $resources[$index] = $resources[$index]->withMcp($mcp);
×
NEW
129
                continue;
×
130
            }
131

NEW
132
            if (!is_subclass_of($metadata, HttpOperation::class) && !is_subclass_of($metadata, GraphQlOperation::class)) {
×
NEW
133
                continue;
×
134
            }
135

UNCOV
136
            if (-1 === $index || $this->hasSameOperation($resources[$index], $metadata::class, $metadata)) {
×
UNCOV
137
                $resources[++$index] = $this->getResourceWithDefaults($resourceClass, $shortName, new ApiResource());
×
138
            }
139

UNCOV
140
            [$key, $operation] = $this->getOperationWithDefaults($resources[$index], $metadata);
×
UNCOV
141
            if (null === $operation->getPriority()) {
×
UNCOV
142
                $operation = $operation->withPriority(++$operationPriority);
×
143
            }
UNCOV
144
            $operations = $resources[$index]->getOperations() ?? new Operations();
×
UNCOV
145
            $resources[$index] = $resources[$index]->withOperations($operations->add($key, $operation));
×
146
        }
147

148
        // Loop again and set default operations if none where found
UNCOV
149
        foreach ($resources as $index => $resource) {
×
UNCOV
150
            if (\count($globalParameters) > 0) {
×
UNCOV
151
                $resources[$index] = $resource = $this->mergeOperationParameters($resource, $globalParameters);
×
152
            }
153

UNCOV
154
            if (null === $resource->getOperations()) {
×
UNCOV
155
                $operations = [];
×
UNCOV
156
                foreach ($this->getDefaultHttpOperations($resource) as $operation) {
×
UNCOV
157
                    [$key, $operation] = $this->getOperationWithDefaults($resource, $operation, true);
×
UNCOV
158
                    $operations[$key] = $operation;
×
159
                }
UNCOV
160
                $resources[$index] = $resource = $resource->withOperations(new Operations($operations));
×
161
            }
162

UNCOV
163
            if ($parameters = $resource->getParameters()) {
×
UNCOV
164
                $operations = [];
×
UNCOV
165
                foreach ($resource->getOperations() ?? [] as $i => $operation) {
×
UNCOV
166
                    $operations[$i] = $this->mergeOperationParameters($operation, $parameters);
×
167
                }
UNCOV
168
                $resources[$index] = $resource = $resource->withOperations(new Operations($operations)); // @phpstan-ignore-line
×
169
            }
170

UNCOV
171
            if (!$this->graphQlEnabled) {
×
UNCOV
172
                continue;
×
173
            }
174

UNCOV
175
            $graphQlOperations = $resource->getGraphQlOperations();
×
UNCOV
176
            if (null === $graphQlOperations) {
×
UNCOV
177
                if (!$hasApiResource) {
×
UNCOV
178
                    $resources[$index] = $resources[$index]->withGraphQlOperations([]);
×
UNCOV
179
                    continue;
×
180
                }
181

182
                // Add default GraphQL operations on the first resource
UNCOV
183
                if (0 === $index) {
×
UNCOV
184
                    $resources[$index] = $this->addDefaultGraphQlOperations($resources[$index]);
×
185
                }
UNCOV
186
                continue;
×
187
            }
188

UNCOV
189
            $resources[$index] = $this->completeGraphQlOperations($resources[$index]);
×
UNCOV
190
            $graphQlOperations = $resources[$index]->getGraphQlOperations();
×
191

UNCOV
192
            $graphQlOperationsWithDefaults = [];
×
UNCOV
193
            foreach ($graphQlOperations as $operation) {
×
UNCOV
194
                [$key, $operation] = $this->getOperationWithDefaults($resource, $operation);
×
UNCOV
195
                if ($parameters) {
×
UNCOV
196
                    $operation = $this->mergeOperationParameters($operation, $parameters);
×
197
                }
198

UNCOV
199
                $graphQlOperationsWithDefaults[$key] = $operation;
×
200
            }
201

UNCOV
202
            $resources[$index] = $resources[$index]->withGraphQlOperations($graphQlOperationsWithDefaults);
×
203
        }
204

UNCOV
205
        return $resources;
×
206
    }
207

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

UNCOV
224
        return false;
×
225
    }
226

227
    /**
228
     * @template T of Metadata
229
     *
230
     * @param T $resource
231
     *
232
     * @return T
233
     */
234
    private function mergeOperationParameters(Metadata $resource, Parameters $globalParameters): Metadata
235
    {
UNCOV
236
        $parameters = $resource->getParameters() ?? new Parameters();
×
UNCOV
237
        foreach ($globalParameters as $parameterName => $parameter) {
×
UNCOV
238
            if ($key = $parameter->getKey()) {
×
UNCOV
239
                $parameterName = $key;
×
240
            }
241

UNCOV
242
            if (!$parameters->has($parameterName, $parameter::class)) {
×
UNCOV
243
                $parameters->add($parameterName, $parameter);
×
244
            }
245
        }
246

UNCOV
247
        return $resource->withParameters($parameters);
×
248
    }
249
}
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