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

api-platform / core / 15955912273

29 Jun 2025 01:51PM UTC coverage: 22.057% (-0.03%) from 22.082%
15955912273

Pull #7249

github

web-flow
Merge d9904d788 into a42034dc3
Pull Request #7249: chore: solve some phpstan issues

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

11540 existing lines in 372 files now uncovered.

11522 of 52237 relevant lines covered (22.06%)

11.08 hits per line

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

75.51
/src/GraphQl/Type/TypeBuilder.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\GraphQl\Type;
15

16
use ApiPlatform\GraphQl\Serializer\ItemNormalizer;
17
use ApiPlatform\Metadata\ApiProperty;
18
use ApiPlatform\Metadata\CollectionOperationInterface;
19
use ApiPlatform\Metadata\Exception\OperationNotFoundException;
20
use ApiPlatform\Metadata\GraphQl\Mutation;
21
use ApiPlatform\Metadata\GraphQl\Operation;
22
use ApiPlatform\Metadata\GraphQl\Query;
23
use ApiPlatform\Metadata\GraphQl\Subscription;
24
use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
25
use ApiPlatform\State\Pagination\Pagination;
26
use GraphQL\Type\Definition\EnumType;
27
use GraphQL\Type\Definition\InputObjectType;
28
use GraphQL\Type\Definition\InterfaceType;
29
use GraphQL\Type\Definition\NonNull;
30
use GraphQL\Type\Definition\ObjectType;
31
use GraphQL\Type\Definition\Type as GraphQLType;
32
use Psr\Container\ContainerInterface;
33
use Symfony\Component\PropertyInfo\Type as LegacyType;
34
use Symfony\Component\TypeInfo\Type;
35

36
/**
37
 * Builds the GraphQL types.
38
 *
39
 * @author Alan Poulain <contact@alanpoulain.eu>
40
 */
41
final class TypeBuilder implements ContextAwareTypeBuilderInterface
42
{
43
    private $defaultFieldResolver;
44

45
    public function __construct(private readonly TypesContainerInterface $typesContainer, callable $defaultFieldResolver, private ?ContainerInterface $fieldsBuilderLocator, private readonly Pagination $pagination)
46
    {
UNCOV
47
        $this->fieldsBuilderLocator = $fieldsBuilderLocator;
13✔
UNCOV
48
        $this->defaultFieldResolver = $defaultFieldResolver;
13✔
49
    }
50

51
    public function setFieldsBuilderLocator(ContainerInterface $fieldsBuilderLocator): void
52
    {
53
        $this->fieldsBuilderLocator = $fieldsBuilderLocator;
×
54
    }
55

56
    /**
57
     * {@inheritdoc}
58
     */
59
    public function getResourceObjectType(ResourceMetadataCollection $resourceMetadataCollection, Operation $operation, ?ApiProperty $propertyMetadata = null, array $context = []): GraphQLType
60
    {
UNCOV
61
        $shortName = $operation->getShortName();
13✔
UNCOV
62
        $operationName = $operation->getName();
13✔
UNCOV
63
        $input = $context['input'];
13✔
UNCOV
64
        $depth = $context['depth'] ?? 0;
13✔
UNCOV
65
        $wrapped = $context['wrapped'] ?? false;
13✔
66

UNCOV
67
        if ($operation instanceof Mutation) {
13✔
UNCOV
68
            $shortName = $operationName.ucfirst($shortName);
5✔
69
        }
70

UNCOV
71
        if ($operation instanceof Subscription) {
13✔
72
            $shortName = $operationName.ucfirst($shortName).'Subscription';
×
73
        }
74

UNCOV
75
        if ($input) {
13✔
UNCOV
76
            if ($depth > 0) {
5✔
77
                $shortName .= 'Nested';
×
78
            }
UNCOV
79
            $shortName .= 'Input';
5✔
UNCOV
80
        } elseif ($operation instanceof Mutation || $operation instanceof Subscription) {
13✔
UNCOV
81
            if ($depth > 0) {
5✔
82
                $shortName .= 'Nested';
×
83
            }
UNCOV
84
            $shortName .= 'Payload';
5✔
85
        }
86

UNCOV
87
        if ('item_query' === $operationName || 'collection_query' === $operationName) {
13✔
88
            // Test if the collection/item has different groups
UNCOV
89
            if ($resourceMetadataCollection->getOperation($operation instanceof CollectionOperationInterface ? 'item_query' : 'collection_query')->getNormalizationContext() !== $operation->getNormalizationContext()) {
13✔
90
                $shortName .= $operation instanceof CollectionOperationInterface ? 'Collection' : 'Item';
×
91
            }
92
        }
93

UNCOV
94
        if ($wrapped && ($operation instanceof Mutation || $operation instanceof Subscription)) {
13✔
95
            $shortName .= 'Data';
×
96
        }
97

UNCOV
98
        $resourceObjectType = null;
13✔
UNCOV
99
        if (!$this->typesContainer->has($shortName)) {
13✔
UNCOV
100
            $resourceObjectType = $this->getResourceObjectTypeConfiguration($shortName, $resourceMetadataCollection, $operation, $context);
13✔
UNCOV
101
            $this->typesContainer->set($shortName, $resourceObjectType);
13✔
102
        }
103

UNCOV
104
        $resourceObjectType = $resourceObjectType ?? $this->typesContainer->get($shortName);
13✔
UNCOV
105
        if (!($resourceObjectType instanceof ObjectType || $resourceObjectType instanceof NonNull || $resourceObjectType instanceof InputObjectType)) {
13✔
106
            throw new \LogicException(\sprintf('Expected GraphQL type "%s" to be %s.', $shortName, implode('|', [ObjectType::class, NonNull::class, InputObjectType::class])));
×
107
        }
108

UNCOV
109
        $required = $propertyMetadata?->isRequired() ?? true;
13✔
UNCOV
110
        if ($required && $input) {
13✔
UNCOV
111
            $resourceObjectType = GraphQLType::nonNull($resourceObjectType);
5✔
112
        }
113

UNCOV
114
        return $resourceObjectType;
13✔
115
    }
116

117
    /**
118
     * {@inheritdoc}
119
     */
120
    public function getNodeInterface(): InterfaceType
121
    {
UNCOV
122
        if ($this->typesContainer->has('Node')) {
13✔
UNCOV
123
            $nodeInterface = $this->typesContainer->get('Node');
13✔
UNCOV
124
            if (!$nodeInterface instanceof InterfaceType) {
13✔
125
                throw new \LogicException(\sprintf('Expected GraphQL type "Node" to be %s.', InterfaceType::class));
×
126
            }
127

UNCOV
128
            return $nodeInterface;
13✔
129
        }
130

UNCOV
131
        $nodeInterface = new InterfaceType([
13✔
UNCOV
132
            'name' => 'Node',
13✔
UNCOV
133
            'description' => 'A node, according to the Relay specification.',
13✔
UNCOV
134
            'fields' => [
13✔
UNCOV
135
                'id' => [
13✔
UNCOV
136
                    'type' => GraphQLType::nonNull(GraphQLType::id()),
13✔
UNCOV
137
                    'description' => 'The id of this node.',
13✔
UNCOV
138
                ],
13✔
UNCOV
139
            ],
13✔
UNCOV
140
            'resolveType' => function ($value): ?GraphQLType {
13✔
141
                if (!isset($value[ItemNormalizer::ITEM_RESOURCE_CLASS_KEY])) {
×
142
                    return null;
×
143
                }
144

145
                $shortName = (new \ReflectionClass($value[ItemNormalizer::ITEM_RESOURCE_CLASS_KEY]))->getShortName();
×
146

147
                return $this->typesContainer->has($shortName) ? $this->typesContainer->get($shortName) : null;
×
UNCOV
148
            },
13✔
UNCOV
149
        ]);
13✔
150

UNCOV
151
        $this->typesContainer->set('Node', $nodeInterface);
13✔
152

UNCOV
153
        return $nodeInterface;
13✔
154
    }
155

156
    /**
157
     * {@inheritdoc}
158
     */
159
    public function getResourcePaginatedCollectionType(GraphQLType $resourceType, string $resourceClass, Operation $operation): GraphQLType
160
    {
161
        @trigger_error('Using getResourcePaginatedCollectionType method of TypeBuilder is deprecated since API Platform 3.1. Use getPaginatedCollectionType method instead.', \E_USER_DEPRECATED);
×
162

163
        return $this->getPaginatedCollectionType($resourceType, $operation);
×
164
    }
165

166
    /**
167
     * {@inheritdoc}
168
     */
169
    public function getPaginatedCollectionType(GraphQLType $resourceType, Operation $operation): GraphQLType
170
    {
UNCOV
171
        $namedType = GraphQLType::getNamedType($resourceType);
6✔
172
        // graphql-php 15: name() exists
UNCOV
173
        $shortName = method_exists($namedType, 'name') ? $namedType->name() : $namedType->name;
6✔
UNCOV
174
        $paginationType = $this->pagination->getGraphQlPaginationType($operation);
6✔
175

UNCOV
176
        $connectionTypeKey = \sprintf('%s%sConnection', $shortName, ucfirst($paginationType));
6✔
UNCOV
177
        if ($this->typesContainer->has($connectionTypeKey)) {
6✔
UNCOV
178
            return $this->typesContainer->get($connectionTypeKey);
4✔
179
        }
180

UNCOV
181
        $fields = 'cursor' === $paginationType ?
6✔
UNCOV
182
            $this->getCursorBasedPaginationFields($resourceType) :
6✔
183
            $this->getPageBasedPaginationFields($resourceType);
×
184

UNCOV
185
        $configuration = [
6✔
UNCOV
186
            'name' => $connectionTypeKey,
6✔
UNCOV
187
            'description' => \sprintf("%s connection for $shortName.", ucfirst($paginationType)),
6✔
UNCOV
188
            'fields' => $fields,
6✔
UNCOV
189
        ];
6✔
190

UNCOV
191
        $resourcePaginatedCollectionType = new ObjectType($configuration);
6✔
UNCOV
192
        $this->typesContainer->set($connectionTypeKey, $resourcePaginatedCollectionType);
6✔
193

UNCOV
194
        return $resourcePaginatedCollectionType;
6✔
195
    }
196

197
    public function getEnumType(Operation $operation): GraphQLType
198
    {
UNCOV
199
        $enumName = $operation->getShortName();
1✔
200

UNCOV
201
        if ($this->typesContainer->has($enumName)) {
1✔
202
            return $this->typesContainer->get($enumName);
×
203
        }
204

205
        /** @var FieldsBuilderEnumInterface $fieldsBuilder */
UNCOV
206
        $fieldsBuilder = $this->fieldsBuilderLocator->get('api_platform.graphql.fields_builder');
1✔
UNCOV
207
        $enumCases = [];
1✔
UNCOV
208
        $enumCases = $fieldsBuilder->getEnumFields($operation->getClass());
1✔
209

UNCOV
210
        $enumConfig = [
1✔
UNCOV
211
            'name' => $enumName,
1✔
UNCOV
212
            'values' => $enumCases,
1✔
UNCOV
213
        ];
1✔
UNCOV
214
        if ($enumDescription = $operation->getDescription()) {
1✔
215
            $enumConfig['description'] = $enumDescription;
×
216
        }
217

UNCOV
218
        $enumType = new EnumType($enumConfig);
1✔
UNCOV
219
        $this->typesContainer->set($enumName, $enumType);
1✔
220

UNCOV
221
        return $enumType;
1✔
222
    }
223

224
    /**
225
     * {@inheritdoc}
226
     */
227
    public function isCollection(LegacyType $type): bool
228
    {
229
        trigger_deprecation('api-platform/graphql', '4.2', 'The "%s()" method is deprecated and will be removed.', __METHOD__, self::class);
×
230

231
        return $type->isCollection() && ($collectionValueType = $type->getCollectionValueTypes()[0] ?? null) && null !== $collectionValueType->getClassName();
×
232
    }
233

234
    private function getCursorBasedPaginationFields(GraphQLType $resourceType): array
235
    {
UNCOV
236
        $namedType = GraphQLType::getNamedType($resourceType);
6✔
237
        // graphql-php 15: name() exists
UNCOV
238
        $shortName = method_exists($namedType, 'name') ? $namedType->name() : $namedType->name;
6✔
239

UNCOV
240
        $edgeObjectTypeConfiguration = [
6✔
UNCOV
241
            'name' => "{$shortName}Edge",
6✔
UNCOV
242
            'description' => "Edge of $shortName.",
6✔
UNCOV
243
            'fields' => [
6✔
UNCOV
244
                'node' => $resourceType,
6✔
UNCOV
245
                'cursor' => GraphQLType::nonNull(GraphQLType::string()),
6✔
UNCOV
246
            ],
6✔
UNCOV
247
        ];
6✔
UNCOV
248
        $edgeObjectType = new ObjectType($edgeObjectTypeConfiguration);
6✔
UNCOV
249
        $this->typesContainer->set("{$shortName}Edge", $edgeObjectType);
6✔
250

UNCOV
251
        $pageInfoObjectTypeConfiguration = [
6✔
UNCOV
252
            'name' => "{$shortName}PageInfo",
6✔
UNCOV
253
            'description' => 'Information about the current page.',
6✔
UNCOV
254
            'fields' => [
6✔
UNCOV
255
                'endCursor' => GraphQLType::string(),
6✔
UNCOV
256
                'startCursor' => GraphQLType::string(),
6✔
UNCOV
257
                'hasNextPage' => GraphQLType::nonNull(GraphQLType::boolean()),
6✔
UNCOV
258
                'hasPreviousPage' => GraphQLType::nonNull(GraphQLType::boolean()),
6✔
UNCOV
259
            ],
6✔
UNCOV
260
        ];
6✔
UNCOV
261
        $pageInfoObjectType = new ObjectType($pageInfoObjectTypeConfiguration);
6✔
UNCOV
262
        $this->typesContainer->set("{$shortName}PageInfo", $pageInfoObjectType);
6✔
263

UNCOV
264
        return [
6✔
UNCOV
265
            'edges' => GraphQLType::listOf($edgeObjectType),
6✔
UNCOV
266
            'pageInfo' => GraphQLType::nonNull($pageInfoObjectType),
6✔
UNCOV
267
            'totalCount' => GraphQLType::nonNull(GraphQLType::int()),
6✔
UNCOV
268
        ];
6✔
269
    }
270

271
    private function getPageBasedPaginationFields(GraphQLType $resourceType): array
272
    {
273
        $namedType = GraphQLType::getNamedType($resourceType);
×
274
        // graphql-php 15: name() exists
275
        $shortName = method_exists($namedType, 'name') ? $namedType->name() : $namedType->name;
×
276

277
        $paginationInfoObjectTypeConfiguration = [
×
278
            'name' => "{$shortName}PaginationInfo",
×
279
            'description' => 'Information about the pagination.',
×
280
            'fields' => [
×
281
                'itemsPerPage' => GraphQLType::nonNull(GraphQLType::int()),
×
282
                'lastPage' => GraphQLType::nonNull(GraphQLType::int()),
×
283
                'totalCount' => GraphQLType::nonNull(GraphQLType::int()),
×
284
                'currentPage' => GraphQLType::nonNull(GraphQLType::int()),
×
285
                'hasNextPage' => GraphQLType::nonNull(GraphQLType::boolean()),
×
286
            ],
×
287
        ];
×
288
        $paginationInfoObjectType = new ObjectType($paginationInfoObjectTypeConfiguration);
×
289
        $this->typesContainer->set("{$shortName}PaginationInfo", $paginationInfoObjectType);
×
290

291
        return [
×
292
            'collection' => GraphQLType::listOf($resourceType),
×
293
            'paginationInfo' => GraphQLType::nonNull($paginationInfoObjectType),
×
294
        ];
×
295
    }
296

297
    private function getQueryOperation(ResourceMetadataCollection $resourceMetadataCollection): ?Operation
298
    {
UNCOV
299
        foreach ($resourceMetadataCollection as $resourceMetadata) {
1✔
UNCOV
300
            foreach ($resourceMetadata->getGraphQlOperations() as $operation) {
1✔
301
                // Filter the custom queries.
UNCOV
302
                if ($operation instanceof Query && !$operation->getResolver()) {
1✔
UNCOV
303
                    return $operation;
1✔
304
                }
305
            }
306
        }
307

308
        return null;
×
309
    }
310

311
    private function getResourceObjectTypeConfiguration(string $shortName, ResourceMetadataCollection $resourceMetadataCollection, Operation $operation, array $context = []): InputObjectType|ObjectType
312
    {
UNCOV
313
        $operationName = $operation->getName();
13✔
UNCOV
314
        $resourceClass = $operation->getClass();
13✔
UNCOV
315
        $input = $context['input'];
13✔
UNCOV
316
        $depth = $context['depth'] ?? 0;
13✔
UNCOV
317
        $wrapped = $context['wrapped'] ?? false;
13✔
318

UNCOV
319
        $ioMetadata = $input ? $operation->getInput() : $operation->getOutput();
13✔
UNCOV
320
        if (null !== $ioMetadata && \array_key_exists('class', $ioMetadata) && null !== $ioMetadata['class']) {
13✔
321
            $resourceClass = $ioMetadata['class'];
×
322
        }
323

UNCOV
324
        $wrapData = !$wrapped && ($operation instanceof Mutation || $operation instanceof Subscription) && !$input && $depth < 1;
13✔
325

UNCOV
326
        $configuration = [
13✔
UNCOV
327
            'name' => $shortName,
13✔
UNCOV
328
            'description' => $operation->getDescription(),
13✔
UNCOV
329
            'resolveField' => $this->defaultFieldResolver,
13✔
UNCOV
330
            'fields' => function () use ($resourceClass, $operation, $operationName, $resourceMetadataCollection, $input, $wrapData, $depth, $ioMetadata) {
13✔
UNCOV
331
                if ($wrapData) {
13✔
UNCOV
332
                    $queryNormalizationContext = $this->getQueryOperation($resourceMetadataCollection)?->getNormalizationContext() ?? [];
1✔
333

334
                    try {
UNCOV
335
                        $mutationNormalizationContext = $operation instanceof Mutation || $operation instanceof Subscription ? ($resourceMetadataCollection->getOperation($operationName)->getNormalizationContext() ?? []) : [];
1✔
336
                    } catch (OperationNotFoundException) {
×
337
                        $mutationNormalizationContext = [];
×
338
                    }
339
                    // Use a new type for the wrapped object only if there is a specific normalization context for the mutation or the subscription.
340
                    // If not, use the query type in order to ensure the client cache could be used.
UNCOV
341
                    $useWrappedType = $queryNormalizationContext !== $mutationNormalizationContext;
1✔
342

UNCOV
343
                    $wrappedOperationName = $operationName;
1✔
344

UNCOV
345
                    if (!$useWrappedType) {
1✔
UNCOV
346
                        $wrappedOperationName = $operation instanceof Query ? $operationName : 'item_query';
1✔
347
                    }
348

UNCOV
349
                    $wrappedOperation = $resourceMetadataCollection->getOperation($wrappedOperationName);
1✔
350

UNCOV
351
                    $fields = [
1✔
UNCOV
352
                        lcfirst($wrappedOperation->getShortName()) => $this->getResourceObjectType($resourceMetadataCollection, $wrappedOperation instanceof Operation ? $wrappedOperation : null, null, [
1✔
UNCOV
353
                            'input' => $input,
1✔
UNCOV
354
                            'wrapped' => true,
1✔
UNCOV
355
                            'depth' => $depth,
1✔
UNCOV
356
                        ]),
1✔
UNCOV
357
                    ];
1✔
358

UNCOV
359
                    if ($operation instanceof Subscription) {
1✔
360
                        $fields['clientSubscriptionId'] = GraphQLType::string();
×
361
                        if ($operation->getMercure()) {
×
362
                            $fields['mercureUrl'] = GraphQLType::string();
×
363
                        }
364

365
                        return $fields;
×
366
                    }
367

UNCOV
368
                    return $fields + ['clientMutationId' => GraphQLType::string()];
1✔
369
                }
370

UNCOV
371
                $fieldsBuilder = $this->fieldsBuilderLocator->get('api_platform.graphql.fields_builder');
13✔
UNCOV
372
                $fields = $fieldsBuilder->getResourceObjectTypeFields($resourceClass, $operation, $input, $depth, $ioMetadata);
13✔
373

UNCOV
374
                if ($input && $operation instanceof Mutation && null !== $mutationArgs = $operation->getArgs()) {
13✔
375
                    return $fieldsBuilder->resolveResourceArgs($mutationArgs, $operation) + ['clientMutationId' => $fields['clientMutationId']];
×
376
                }
UNCOV
377
                if ($input && $operation instanceof Mutation && null !== $extraMutationArgs = $operation->getExtraArgs()) {
13✔
378
                    return $fields + $fieldsBuilder->resolveResourceArgs($extraMutationArgs, $operation);
×
379
                }
380

UNCOV
381
                return $fields;
13✔
UNCOV
382
            },
13✔
UNCOV
383
            'interfaces' => $wrapData ? [] : [$this->getNodeInterface()],
13✔
UNCOV
384
        ];
13✔
385

UNCOV
386
        return $input ? new InputObjectType($configuration) : new ObjectType($configuration);
13✔
387
    }
388
}
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