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

api-platform / core / 14408211113

11 Apr 2025 04:58PM UTC coverage: 61.931% (+0.9%) from 61.025%
14408211113

Pull #7085

github

web-flow
Merge 2461e799a into c5fb664d1
Pull Request #7085: [fix] inverse ternary operator statements

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

168 existing lines in 28 files now uncovered.

11482 of 18540 relevant lines covered (61.93%)

68.58 hits per line

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

77.32
/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 readonly ContainerInterface $fieldsBuilderLocator, private readonly Pagination $pagination)
45
    {
46
        $this->defaultFieldResolver = $defaultFieldResolver;
35✔
47
    }
48

49
    /**
50
     * {@inheritdoc}
51
     */
52
    public function getResourceObjectType(ResourceMetadataCollection $resourceMetadataCollection, Operation $operation, ?ApiProperty $propertyMetadata = null, array $context = []): GraphQLType
53
    {
54
        $shortName = $operation->getShortName();
35✔
55
        $operationName = $operation->getName();
35✔
56
        $input = $context['input'];
35✔
57
        $depth = $context['depth'] ?? 0;
35✔
58
        $wrapped = $context['wrapped'] ?? false;
35✔
59

60
        if ($operation instanceof Mutation) {
35✔
61
            $shortName = $operationName.ucfirst($shortName);
35✔
62
        }
63

64
        if ($operation instanceof Subscription) {
35✔
65
            $shortName = $operationName.ucfirst($shortName).'Subscription';
35✔
66
        }
67

68
        if ($input) {
35✔
69
            if ($depth > 0) {
35✔
70
                $shortName .= 'Nested';
×
71
            }
72
            $shortName .= 'Input';
35✔
73
        } elseif ($operation instanceof Mutation || $operation instanceof Subscription) {
35✔
74
            if ($depth > 0) {
35✔
75
                $shortName .= 'Nested';
×
76
            }
77
            $shortName .= 'Payload';
35✔
78
        }
79

80
        if ('item_query' === $operationName || 'collection_query' === $operationName) {
35✔
81
            // Test if the collection/item has different groups
82
            if ($resourceMetadataCollection->getOperation($operation instanceof CollectionOperationInterface ? 'collection_query' : 'item_query')->getNormalizationContext() !== $operation->getNormalizationContext()) {
35✔
UNCOV
83
                $shortName .= $operation instanceof CollectionOperationInterface ? 'Collection' : 'Item';
×
84
            }
85
        }
86

87
        if ($wrapped && ($operation instanceof Mutation || $operation instanceof Subscription)) {
35✔
88
            $shortName .= 'Data';
×
89
        }
90

91
        $resourceObjectType = null;
35✔
92
        if (!$this->typesContainer->has($shortName)) {
35✔
93
            $resourceObjectType = $this->getResourceObjectTypeConfiguration($shortName, $resourceMetadataCollection, $operation, $context);
35✔
94
            $this->typesContainer->set($shortName, $resourceObjectType);
35✔
95
        }
96

97
        $resourceObjectType = $resourceObjectType ?? $this->typesContainer->get($shortName);
35✔
98
        if (!($resourceObjectType instanceof ObjectType || $resourceObjectType instanceof NonNull || $resourceObjectType instanceof InputObjectType)) {
35✔
99
            throw new \LogicException(\sprintf('Expected GraphQL type "%s" to be %s.', $shortName, implode('|', [ObjectType::class, NonNull::class, InputObjectType::class])));
×
100
        }
101

102
        $required = $propertyMetadata?->isRequired() ?? true;
35✔
103
        if ($required && $input) {
35✔
104
            $resourceObjectType = GraphQLType::nonNull($resourceObjectType);
35✔
105
        }
106

107
        return $resourceObjectType;
35✔
108
    }
109

110
    /**
111
     * {@inheritdoc}
112
     */
113
    public function getNodeInterface(): InterfaceType
114
    {
115
        if ($this->typesContainer->has('Node')) {
35✔
116
            $nodeInterface = $this->typesContainer->get('Node');
35✔
117
            if (!$nodeInterface instanceof InterfaceType) {
35✔
118
                throw new \LogicException(\sprintf('Expected GraphQL type "Node" to be %s.', InterfaceType::class));
×
119
            }
120

121
            return $nodeInterface;
35✔
122
        }
123

124
        $nodeInterface = new InterfaceType([
35✔
125
            'name' => 'Node',
35✔
126
            'description' => 'A node, according to the Relay specification.',
35✔
127
            'fields' => [
35✔
128
                'id' => [
35✔
129
                    'type' => GraphQLType::nonNull(GraphQLType::id()),
35✔
130
                    'description' => 'The id of this node.',
35✔
131
                ],
35✔
132
            ],
35✔
133
            'resolveType' => function ($value): ?GraphQLType {
35✔
134
                if (!isset($value[ItemNormalizer::ITEM_RESOURCE_CLASS_KEY])) {
×
135
                    return null;
×
136
                }
137

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

140
                return $this->typesContainer->has($shortName) ? $this->typesContainer->get($shortName) : null;
×
141
            },
35✔
142
        ]);
35✔
143

144
        $this->typesContainer->set('Node', $nodeInterface);
35✔
145

146
        return $nodeInterface;
35✔
147
    }
148

149
    /**
150
     * {@inheritdoc}
151
     */
152
    public function getResourcePaginatedCollectionType(GraphQLType $resourceType, string $resourceClass, Operation $operation): GraphQLType
153
    {
154
        @trigger_error('Using getResourcePaginatedCollectionType method of TypeBuilder is deprecated since API Platform 3.1. Use getPaginatedCollectionType method instead.', \E_USER_DEPRECATED);
×
155

156
        return $this->getPaginatedCollectionType($resourceType, $operation);
×
157
    }
158

159
    /**
160
     * {@inheritdoc}
161
     */
162
    public function getPaginatedCollectionType(GraphQLType $resourceType, Operation $operation): GraphQLType
163
    {
164
        $namedType = GraphQLType::getNamedType($resourceType);
35✔
165
        // graphql-php 15: name() exists
166
        $shortName = method_exists($namedType, 'name') ? $namedType->name() : $namedType->name;
35✔
167
        $paginationType = $this->pagination->getGraphQlPaginationType($operation);
35✔
168

169
        $connectionTypeKey = \sprintf('%s%sConnection', $shortName, ucfirst($paginationType));
35✔
170
        if ($this->typesContainer->has($connectionTypeKey)) {
35✔
171
            return $this->typesContainer->get($connectionTypeKey);
35✔
172
        }
173

174
        $fields = 'cursor' === $paginationType ?
35✔
175
            $this->getCursorBasedPaginationFields($resourceType) :
35✔
176
            $this->getPageBasedPaginationFields($resourceType);
35✔
177

178
        $configuration = [
35✔
179
            'name' => $connectionTypeKey,
35✔
180
            'description' => \sprintf("%s connection for $shortName.", ucfirst($paginationType)),
35✔
181
            'fields' => $fields,
35✔
182
        ];
35✔
183

184
        $resourcePaginatedCollectionType = new ObjectType($configuration);
35✔
185
        $this->typesContainer->set($connectionTypeKey, $resourcePaginatedCollectionType);
35✔
186

187
        return $resourcePaginatedCollectionType;
35✔
188
    }
189

190
    public function getEnumType(Operation $operation): GraphQLType
191
    {
192
        $enumName = $operation->getShortName();
4✔
193

194
        if ($this->typesContainer->has($enumName)) {
4✔
195
            return $this->typesContainer->get($enumName);
×
196
        }
197

198
        /** @var FieldsBuilderEnumInterface|FieldsBuilderInterface $fieldsBuilder */
199
        $fieldsBuilder = $this->fieldsBuilderLocator->get('api_platform.graphql.fields_builder');
4✔
200
        $enumCases = [];
4✔
201
        // Remove the condition in API Platform 4.
202
        if ($fieldsBuilder instanceof FieldsBuilderEnumInterface) {
4✔
203
            $enumCases = $fieldsBuilder->getEnumFields($operation->getClass());
4✔
204
        } else {
205
            @trigger_error(\sprintf('api_platform.graphql.fields_builder service implementing "%s" is deprecated since API Platform 3.1. It has to implement "%s" instead.', FieldsBuilderInterface::class, FieldsBuilderEnumInterface::class), \E_USER_DEPRECATED);
×
206
        }
207

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

216
        $enumType = new EnumType($enumConfig);
4✔
217
        $this->typesContainer->set($enumName, $enumType);
4✔
218

219
        return $enumType;
4✔
220
    }
221

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

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

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

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

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

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

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

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

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

303
        return null;
×
304
    }
305

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

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

319
        $wrapData = !$wrapped && ($operation instanceof Mutation || $operation instanceof Subscription) && !$input && $depth < 1;
35✔
320

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

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

338
                    $wrappedOperationName = $operationName;
×
339

340
                    if (!$useWrappedType) {
×
341
                        $wrappedOperationName = $operation instanceof Query ? $operationName : 'item_query';
×
342
                    }
343

344
                    $wrappedOperation = $resourceMetadataCollection->getOperation($wrappedOperationName);
×
345

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

354
                    if ($operation instanceof Subscription) {
×
355
                        $fields['clientSubscriptionId'] = GraphQLType::string();
×
356
                        if ($operation->getMercure()) {
×
357
                            $fields['mercureUrl'] = GraphQLType::string();
×
358
                        }
359

360
                        return $fields;
×
361
                    }
362

363
                    return $fields + ['clientMutationId' => GraphQLType::string()];
×
364
                }
365

366
                $fieldsBuilder = $this->fieldsBuilderLocator->get('api_platform.graphql.fields_builder');
35✔
367
                $fields = $fieldsBuilder->getResourceObjectTypeFields($resourceClass, $operation, $input, $depth, $ioMetadata);
35✔
368

369
                if ($input && $operation instanceof Mutation && null !== $mutationArgs = $operation->getArgs()) {
35✔
370
                    return $fieldsBuilder->resolveResourceArgs($mutationArgs, $operation) + ['clientMutationId' => $fields['clientMutationId']];
×
371
                }
372
                if ($input && $operation instanceof Mutation && null !== $extraMutationArgs = $operation->getExtraArgs()) {
35✔
373
                    return $fields + $fieldsBuilder->resolveResourceArgs($extraMutationArgs, $operation);
×
374
                }
375

376
                return $fields;
35✔
377
            },
35✔
378
            'interfaces' => $wrapData ? [] : [$this->getNodeInterface()],
35✔
379
        ];
35✔
380

381
        return $input ? new InputObjectType($configuration) : new ObjectType($configuration);
35✔
382
    }
383
}
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