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

api-platform / core / 15133993414

20 May 2025 09:30AM UTC coverage: 26.313% (-1.2%) from 27.493%
15133993414

Pull #7161

github

web-flow
Merge e2c03d45f into 5459ba375
Pull Request #7161: fix(metadata): infer parameter string type from schema

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

11019 existing lines in 363 files now uncovered.

12898 of 49018 relevant lines covered (26.31%)

34.33 hits per line

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

94.87
/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;
34

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

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

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

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

UNCOV
66
        if ($operation instanceof Mutation) {
146✔
UNCOV
67
            $shortName = $operationName.ucfirst($shortName);
138✔
68
        }
69

UNCOV
70
        if ($operation instanceof Subscription) {
146✔
71
            $shortName = $operationName.ucfirst($shortName).'Subscription';
133✔
72
        }
73

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

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

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

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

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

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

UNCOV
113
        return $resourceObjectType;
146✔
114
    }
115

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

UNCOV
127
            return $nodeInterface;
146✔
128
        }
129

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

144
                $shortName = (new \ReflectionClass($value[ItemNormalizer::ITEM_RESOURCE_CLASS_KEY]))->getShortName();
1✔
145

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

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

UNCOV
152
        return $nodeInterface;
146✔
153
    }
154

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

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

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

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

UNCOV
180
        $fields = 'cursor' === $paginationType ?
139✔
UNCOV
181
            $this->getCursorBasedPaginationFields($resourceType) :
139✔
182
            $this->getPageBasedPaginationFields($resourceType);
133✔
183

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

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

UNCOV
193
        return $resourcePaginatedCollectionType;
139✔
194
    }
195

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

UNCOV
200
        if ($this->typesContainer->has($enumName)) {
5✔
201
            return $this->typesContainer->get($enumName);
3✔
202
        }
203

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

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

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

UNCOV
220
        return $enumType;
5✔
221
    }
222

223
    /**
224
     * {@inheritdoc}
225
     */
226
    public function isCollection(Type $type): bool
227
    {
UNCOV
228
        return $type->isCollection() && ($collectionValueType = $type->getCollectionValueTypes()[0] ?? null) && null !== $collectionValueType->getClassName();
146✔
229
    }
230

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

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

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

UNCOV
261
        return [
139✔
UNCOV
262
            'edges' => GraphQLType::listOf($edgeObjectType),
139✔
UNCOV
263
            'pageInfo' => GraphQLType::nonNull($pageInfoObjectType),
139✔
UNCOV
264
            'totalCount' => GraphQLType::nonNull(GraphQLType::int()),
139✔
UNCOV
265
        ];
139✔
266
    }
267

268
    private function getPageBasedPaginationFields(GraphQLType $resourceType): array
269
    {
270
        $namedType = GraphQLType::getNamedType($resourceType);
133✔
271
        // graphql-php 15: name() exists
272
        $shortName = method_exists($namedType, 'name') ? $namedType->name() : $namedType->name;
133✔
273

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

288
        return [
133✔
289
            'collection' => GraphQLType::listOf($resourceType),
133✔
290
            'paginationInfo' => GraphQLType::nonNull($paginationInfoObjectType),
133✔
291
        ];
133✔
292
    }
293

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

305
        return null;
×
306
    }
307

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

UNCOV
316
        $ioMetadata = $input ? $operation->getInput() : $operation->getOutput();
146✔
UNCOV
317
        if (null !== $ioMetadata && \array_key_exists('class', $ioMetadata) && null !== $ioMetadata['class']) {
146✔
318
            $resourceClass = $ioMetadata['class'];
133✔
319
        }
320

UNCOV
321
        $wrapData = !$wrapped && ($operation instanceof Mutation || $operation instanceof Subscription) && !$input && $depth < 1;
146✔
322

UNCOV
323
        $configuration = [
146✔
UNCOV
324
            'name' => $shortName,
146✔
UNCOV
325
            'description' => $operation->getDescription(),
146✔
UNCOV
326
            'resolveField' => $this->defaultFieldResolver,
146✔
UNCOV
327
            'fields' => function () use ($resourceClass, $operation, $operationName, $resourceMetadataCollection, $input, $wrapData, $depth, $ioMetadata) {
146✔
UNCOV
328
                if ($wrapData) {
140✔
UNCOV
329
                    $queryNormalizationContext = $this->getQueryOperation($resourceMetadataCollection)?->getNormalizationContext() ?? [];
42✔
330

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

UNCOV
340
                    $wrappedOperationName = $operationName;
42✔
341

UNCOV
342
                    if (!$useWrappedType) {
42✔
UNCOV
343
                        $wrappedOperationName = $operation instanceof Query ? $operationName : 'item_query';
34✔
344
                    }
345

UNCOV
346
                    $wrappedOperation = $resourceMetadataCollection->getOperation($wrappedOperationName);
42✔
347

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

UNCOV
356
                    if ($operation instanceof Subscription) {
42✔
357
                        $fields['clientSubscriptionId'] = GraphQLType::string();
3✔
358
                        if ($operation->getMercure()) {
3✔
359
                            $fields['mercureUrl'] = GraphQLType::string();
3✔
360
                        }
361

362
                        return $fields;
3✔
363
                    }
364

UNCOV
365
                    return $fields + ['clientMutationId' => GraphQLType::string()];
41✔
366
                }
367

UNCOV
368
                $fieldsBuilder = $this->fieldsBuilderLocator->get('api_platform.graphql.fields_builder');
140✔
UNCOV
369
                $fields = $fieldsBuilder->getResourceObjectTypeFields($resourceClass, $operation, $input, $depth, $ioMetadata);
140✔
370

UNCOV
371
                if ($input && $operation instanceof Mutation && null !== $mutationArgs = $operation->getArgs()) {
140✔
372
                    return $fieldsBuilder->resolveResourceArgs($mutationArgs, $operation) + ['clientMutationId' => $fields['clientMutationId']];
5✔
373
                }
UNCOV
374
                if ($input && $operation instanceof Mutation && null !== $extraMutationArgs = $operation->getExtraArgs()) {
140✔
375
                    return $fields + $fieldsBuilder->resolveResourceArgs($extraMutationArgs, $operation);
6✔
376
                }
377

UNCOV
378
                return $fields;
140✔
UNCOV
379
            },
146✔
UNCOV
380
            'interfaces' => $wrapData ? [] : [$this->getNodeInterface()],
146✔
UNCOV
381
        ];
146✔
382

UNCOV
383
        return $input ? new InputObjectType($configuration) : new ObjectType($configuration);
146✔
384
    }
385
}
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