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

api-platform / core / 11276503720

10 Oct 2024 02:49PM UTC coverage: 61.85%. Remained the same
11276503720

push

github

web-flow
doc: error guide remove useless configuration (#6712)

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>

11394 of 18422 relevant lines covered (61.85%)

66.73 hits per line

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

86.56
/src/GraphQl/Type/FieldsBuilder.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\Doctrine\Odm\State\Options as ODMOptions;
17
use ApiPlatform\Doctrine\Orm\State\Options;
18
use ApiPlatform\GraphQl\Exception\InvalidTypeException;
19
use ApiPlatform\GraphQl\Resolver\Factory\ResolverFactory;
20
use ApiPlatform\GraphQl\Resolver\Factory\ResolverFactoryInterface;
21
use ApiPlatform\GraphQl\Type\Definition\TypeInterface;
22
use ApiPlatform\Metadata\GraphQl\Mutation;
23
use ApiPlatform\Metadata\GraphQl\Operation;
24
use ApiPlatform\Metadata\GraphQl\Query;
25
use ApiPlatform\Metadata\GraphQl\Subscription;
26
use ApiPlatform\Metadata\InflectorInterface;
27
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
28
use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
29
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
30
use ApiPlatform\Metadata\ResourceClassResolverInterface;
31
use ApiPlatform\Metadata\Util\Inflector;
32
use ApiPlatform\State\Pagination\Pagination;
33
use GraphQL\Type\Definition\InputObjectType;
34
use GraphQL\Type\Definition\ListOfType;
35
use GraphQL\Type\Definition\NonNull;
36
use GraphQL\Type\Definition\NullableType;
37
use GraphQL\Type\Definition\Type as GraphQLType;
38
use GraphQL\Type\Definition\WrappingType;
39
use Psr\Container\ContainerInterface;
40
use Symfony\Component\PropertyInfo\Type;
41
use Symfony\Component\Serializer\NameConverter\AdvancedNameConverterInterface;
42
use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter;
43
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
44

45
/**
46
 * Builds the GraphQL fields.
47
 *
48
 * @author Alan Poulain <contact@alanpoulain.eu>
49
 */
50
final class FieldsBuilder implements FieldsBuilderInterface, FieldsBuilderEnumInterface
51
{
52
    private readonly ContextAwareTypeBuilderInterface|TypeBuilderEnumInterface|TypeBuilderInterface $typeBuilder;
53

54
    public function __construct(private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly ResourceClassResolverInterface $resourceClassResolver, private readonly TypesContainerInterface $typesContainer, ContextAwareTypeBuilderInterface|TypeBuilderEnumInterface|TypeBuilderInterface $typeBuilder, private readonly TypeConverterInterface $typeConverter, private readonly ResolverFactoryInterface $itemResolverFactory, private readonly ?ResolverFactoryInterface $collectionResolverFactory, private readonly ?ResolverFactoryInterface $itemMutationResolverFactory, private readonly ?ResolverFactoryInterface $itemSubscriptionResolverFactory, private readonly ContainerInterface $filterLocator, private readonly Pagination $pagination, private readonly ?NameConverterInterface $nameConverter, private readonly string $nestingSeparator, private readonly ?InflectorInterface $inflector = new Inflector())
55
    {
56
        if ($typeBuilder instanceof TypeBuilderInterface) {
35✔
57
            @trigger_error(\sprintf('$typeBuilder argument of FieldsBuilder implementing "%s" is deprecated since API Platform 3.1. It has to implement "%s" instead.', TypeBuilderInterface::class, TypeBuilderEnumInterface::class), \E_USER_DEPRECATED);
×
58
        }
59
        if ($typeBuilder instanceof TypeBuilderEnumInterface) {
35✔
60
            @trigger_error(\sprintf('$typeBuilder argument of TypeConverter implementing "%s" is deprecated since API Platform 3.3. It has to implement "%s" instead.', TypeBuilderEnumInterface::class, ContextAwareTypeBuilderInterface::class), \E_USER_DEPRECATED);
×
61
        }
62
        $this->typeBuilder = $typeBuilder;
35✔
63
    }
64

65
    /**
66
     * {@inheritdoc}
67
     */
68
    public function getNodeQueryFields(): array
69
    {
70
        return [
35✔
71
            'type' => $this->typeBuilder->getNodeInterface(),
35✔
72
            'args' => [
35✔
73
                'id' => ['type' => GraphQLType::nonNull(GraphQLType::id())],
35✔
74
            ],
35✔
75
            'resolve' => ($this->itemResolverFactory)(),
35✔
76
        ];
35✔
77
    }
78

79
    /**
80
     * {@inheritdoc}
81
     */
82
    public function getItemQueryFields(string $resourceClass, Operation $operation, array $configuration): array
83
    {
84
        if ($operation instanceof Query && $operation->getNested()) {
35✔
85
            return [];
35✔
86
        }
87

88
        $fieldName = lcfirst('item_query' === $operation->getName() ? $operation->getShortName() : $operation->getName().$operation->getShortName());
35✔
89

90
        if ($fieldConfiguration = $this->getResourceFieldConfiguration(null, $operation->getDescription(), $operation->getDeprecationReason(), new Type(Type::BUILTIN_TYPE_OBJECT, true, $resourceClass), $resourceClass, false, $operation)) {
35✔
91
            $args = $this->resolveResourceArgs($configuration['args'] ?? [], $operation);
35✔
92
            $extraArgs = $this->resolveResourceArgs($operation->getExtraArgs() ?? [], $operation);
35✔
93
            $configuration['args'] = $args ?: $configuration['args'] ?? ['id' => ['type' => GraphQLType::nonNull(GraphQLType::id())]] + $extraArgs;
35✔
94

95
            return [$fieldName => array_merge($fieldConfiguration, $configuration)];
35✔
96
        }
97

98
        return [];
×
99
    }
100

101
    /**
102
     * {@inheritdoc}
103
     */
104
    public function getCollectionQueryFields(string $resourceClass, Operation $operation, array $configuration): array
105
    {
106
        if ($operation instanceof Query && $operation->getNested()) {
35✔
107
            return [];
35✔
108
        }
109

110
        $fieldName = lcfirst('collection_query' === $operation->getName() ? $operation->getShortName() : $operation->getName().$operation->getShortName());
35✔
111

112
        if ($fieldConfiguration = $this->getResourceFieldConfiguration(null, $operation->getDescription(), $operation->getDeprecationReason(), new Type(Type::BUILTIN_TYPE_OBJECT, false, null, true, null, new Type(Type::BUILTIN_TYPE_OBJECT, false, $resourceClass)), $resourceClass, false, $operation)) {
35✔
113
            $args = $this->resolveResourceArgs($configuration['args'] ?? [], $operation);
35✔
114
            $extraArgs = $this->resolveResourceArgs($operation->getExtraArgs() ?? [], $operation);
35✔
115
            $configuration['args'] = $args ?: $configuration['args'] ?? $fieldConfiguration['args'] + $extraArgs;
35✔
116

117
            return [$this->inflector->pluralize($fieldName) => array_merge($fieldConfiguration, $configuration)];
35✔
118
        }
119

120
        return [];
×
121
    }
122

123
    /**
124
     * {@inheritdoc}
125
     */
126
    public function getMutationFields(string $resourceClass, Operation $operation): array
127
    {
128
        $mutationFields = [];
35✔
129
        $resourceType = new Type(Type::BUILTIN_TYPE_OBJECT, true, $resourceClass);
35✔
130
        $description = $operation->getDescription() ?? ucfirst("{$operation->getName()}s a {$operation->getShortName()}.");
35✔
131

132
        if ($fieldConfiguration = $this->getResourceFieldConfiguration(null, $description, $operation->getDeprecationReason(), $resourceType, $resourceClass, false, $operation)) {
35✔
133
            $fieldConfiguration['args'] += ['input' => $this->getResourceFieldConfiguration(null, null, $operation->getDeprecationReason(), $resourceType, $resourceClass, true, $operation)];
35✔
134
        }
135

136
        $mutationFields[$operation->getName().$operation->getShortName()] = $fieldConfiguration ?? [];
35✔
137

138
        return $mutationFields;
35✔
139
    }
140

141
    /**
142
     * {@inheritdoc}
143
     */
144
    public function getSubscriptionFields(string $resourceClass, Operation $operation): array
145
    {
146
        $subscriptionFields = [];
35✔
147
        $resourceType = new Type(Type::BUILTIN_TYPE_OBJECT, true, $resourceClass);
35✔
148
        $description = $operation->getDescription() ?? \sprintf('Subscribes to the action event of a %s.', $operation->getShortName());
35✔
149

150
        if ($fieldConfiguration = $this->getResourceFieldConfiguration(null, $description, $operation->getDeprecationReason(), $resourceType, $resourceClass, false, $operation)) {
35✔
151
            $fieldConfiguration['args'] += ['input' => $this->getResourceFieldConfiguration(null, null, $operation->getDeprecationReason(), $resourceType, $resourceClass, true, $operation)];
35✔
152
        }
153

154
        if (!$fieldConfiguration) {
35✔
155
            return [];
×
156
        }
157

158
        $subscriptionName = $operation->getName();
35✔
159
        // TODO: 3.0 change this
160
        if ('update_subscription' === $subscriptionName) {
35✔
161
            $subscriptionName = 'update';
35✔
162
        }
163

164
        $subscriptionFields[$subscriptionName.$operation->getShortName().'Subscribe'] = $fieldConfiguration;
35✔
165

166
        return $subscriptionFields;
35✔
167
    }
168

169
    /**
170
     * {@inheritdoc}
171
     */
172
    public function getResourceObjectTypeFields(?string $resourceClass, Operation $operation, bool $input, int $depth = 0, ?array $ioMetadata = null): array
173
    {
174
        $fields = [];
35✔
175
        $idField = ['type' => GraphQLType::nonNull(GraphQLType::id())];
35✔
176
        $optionalIdField = ['type' => GraphQLType::id()];
35✔
177
        $clientMutationId = GraphQLType::string();
35✔
178
        $clientSubscriptionId = GraphQLType::string();
35✔
179

180
        if (null !== $ioMetadata && \array_key_exists('class', $ioMetadata) && null === $ioMetadata['class']) {
35✔
181
            if ($input) {
×
182
                return ['clientMutationId' => $clientMutationId];
×
183
            }
184

185
            return [];
×
186
        }
187

188
        if ($operation instanceof Subscription && $input) {
35✔
189
            return [
×
190
                'id' => $idField,
×
191
                'clientSubscriptionId' => $clientSubscriptionId,
×
192
            ];
×
193
        }
194

195
        if ('delete' === $operation->getName()) {
35✔
196
            $fields = [
×
197
                'id' => $idField,
×
198
            ];
×
199

200
            if ($input) {
×
201
                $fields['clientMutationId'] = $clientMutationId;
×
202
            }
203

204
            return $fields;
×
205
        }
206

207
        if (!$input || (!$operation->getResolver() && 'create' !== $operation->getName())) {
35✔
208
            $fields['id'] = $idField;
35✔
209
        }
210
        if ($input && $depth >= 1) {
35✔
211
            $fields['id'] = $optionalIdField;
×
212
        }
213

214
        ++$depth; // increment the depth for the call to getResourceFieldConfiguration.
35✔
215

216
        if (null !== $resourceClass) {
35✔
217
            foreach ($this->propertyNameCollectionFactory->create($resourceClass) as $property) {
35✔
218
                $context = [
35✔
219
                    'normalization_groups' => $operation->getNormalizationContext()['groups'] ?? null,
35✔
220
                    'denormalization_groups' => $operation->getDenormalizationContext()['groups'] ?? null,
35✔
221
                ];
35✔
222
                $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $property, $context);
35✔
223
                $propertyTypes = $propertyMetadata->getBuiltinTypes();
35✔
224

225
                if (
226
                    !$propertyTypes
35✔
227
                    || (!$input && false === $propertyMetadata->isReadable())
35✔
228
                    || ($input && false === $propertyMetadata->isWritable())
35✔
229
                ) {
230
                    continue;
24✔
231
                }
232

233
                // guess union/intersect types: check each type until finding a valid one
234
                foreach ($propertyTypes as $propertyType) {
35✔
235
                    if ($fieldConfiguration = $this->getResourceFieldConfiguration($property, $propertyMetadata->getDescription(), $propertyMetadata->getDeprecationReason(), $propertyType, $resourceClass, $input, $operation, $depth, null !== $propertyMetadata->getSecurity())) {
35✔
236
                        $fields['id' === $property ? '_id' : $this->normalizePropertyName($property, $resourceClass)] = $fieldConfiguration;
35✔
237
                        // stop at the first valid type
238
                        break;
35✔
239
                    }
240
                }
241
            }
242
        }
243

244
        if ($operation instanceof Mutation && $input) {
35✔
245
            $fields['clientMutationId'] = $clientMutationId;
×
246
        }
247

248
        return $fields;
35✔
249
    }
250

251
    private function isEnumClass(string $resourceClass): bool
252
    {
253
        return is_a($resourceClass, \BackedEnum::class, true);
35✔
254
    }
255

256
    /**
257
     * {@inheritdoc}
258
     */
259
    public function getEnumFields(string $enumClass): array
260
    {
261
        $rEnum = new \ReflectionEnum($enumClass);
4✔
262

263
        $enumCases = [];
4✔
264
        /* @var \ReflectionEnumUnitCase|\ReflectionEnumBackedCase */
265
        foreach ($rEnum->getCases() as $rCase) {
4✔
266
            if ($rCase instanceof \ReflectionEnumBackedCase) {
4✔
267
                $enumCase = ['value' => $rCase->getBackingValue()];
4✔
268
            } else {
269
                $enumCase = ['value' => $rCase->getValue()];
×
270
            }
271

272
            $propertyMetadata = $this->propertyMetadataFactory->create($enumClass, $rCase->getName());
4✔
273
            if ($enumCaseDescription = $propertyMetadata->getDescription()) {
4✔
274
                $enumCase['description'] = $enumCaseDescription;
4✔
275
            }
276
            $enumCases[$rCase->getName()] = $enumCase;
4✔
277
        }
278

279
        return $enumCases;
4✔
280
    }
281

282
    /**
283
     * {@inheritdoc}
284
     */
285
    public function resolveResourceArgs(array $args, Operation $operation): array
286
    {
287
        foreach ($args as $id => $arg) {
35✔
288
            if (!isset($arg['type'])) {
35✔
289
                throw new \InvalidArgumentException(\sprintf('The argument "%s" of the custom operation "%s" in %s needs a "type" option.', $id, $operation->getName(), $operation->getShortName()));
×
290
            }
291

292
            $args[$id]['type'] = $this->typeConverter->resolveType($arg['type']);
35✔
293
        }
294

295
        /*
296
         * This is @experimental, read the comment on the parameterToObjectType function as additional information.
297
         */
298
        foreach ($operation->getParameters() ?? [] as $parameter) {
35✔
299
            $key = $parameter->getKey();
35✔
300

301
            if (str_contains($key, ':property')) {
35✔
302
                if (!($filterId = $parameter->getFilter()) || !$this->filterLocator->has($filterId)) {
35✔
303
                    continue;
×
304
                }
305

306
                $parsedKey = explode('[:property]', $key);
35✔
307
                $flattenFields = [];
35✔
308
                foreach ($this->filterLocator->get($filterId)->getDescription($operation->getClass()) as $key => $value) {
35✔
309
                    $values = [];
35✔
310
                    parse_str($key, $values);
35✔
311
                    if (isset($values[$parsedKey[0]])) {
35✔
312
                        $values = $values[$parsedKey[0]];
35✔
313
                    }
314

315
                    $name = key($values);
35✔
316
                    $flattenFields[] = ['name' => $name, 'required' => $value['required'] ?? null, 'description' => $value['description'] ?? null, 'leafs' => $values[$name], 'type' => $value['type'] ?? 'string'];
35✔
317
                }
318

319
                $args[$parsedKey[0]] = $this->parameterToObjectType($flattenFields, $parsedKey[0]);
35✔
320
                continue;
35✔
321
            }
322

323
            $args[$key] = ['type' => GraphQLType::string()];
35✔
324

325
            if ($parameter->getRequired()) {
35✔
326
                $args[$key]['type'] = GraphQLType::nonNull($args[$key]['type']);
×
327
            }
328
        }
329

330
        return $args;
35✔
331
    }
332

333
    /**
334
     * Transform the result of a parse_str to a GraphQL object type.
335
     * We should consider merging getFilterArgs and this, `getFilterArgs` uses `convertType` whereas we assume that parameters have only scalar types.
336
     * Note that this method has a lower complexity then the `getFilterArgs` one.
337
     * TODO: Is there a use case with an argument being a complex type (eg: a Resource, Enum etc.)?
338
     *
339
     * @param array<array{name: string, required: bool|null, description: string|null, leafs: string|array, type: string}> $flattenFields
340
     */
341
    private function parameterToObjectType(array $flattenFields, string $name): InputObjectType
342
    {
343
        $fields = [];
35✔
344
        foreach ($flattenFields as $field) {
35✔
345
            $key = $field['name'];
35✔
346
            $type = $this->getParameterType(\in_array($field['type'], Type::$builtinTypes, true) ? new Type($field['type'], !$field['required']) : new Type('object', !$field['required'], $field['type']));
35✔
347

348
            if (\is_array($l = $field['leafs'])) {
35✔
349
                if (0 === key($l)) {
35✔
350
                    $key = $key;
35✔
351
                    $type = GraphQLType::listOf($type);
35✔
352
                } else {
353
                    $n = [];
35✔
354
                    foreach ($field['leafs'] as $l => $value) {
35✔
355
                        $n[] = ['required' => null, 'name' => $l, 'leafs' => $value, 'type' => 'string', 'description' => null];
35✔
356
                    }
357

358
                    $type = $this->parameterToObjectType($n, $key);
35✔
359
                    if (isset($fields[$key]) && ($t = $fields[$key]['type']) instanceof InputObjectType) {
35✔
360
                        $t = $fields[$key]['type'];
35✔
361
                        $t->config['fields'] = array_merge($t->config['fields'], $type->config['fields']);
35✔
362
                        $type = $t;
35✔
363
                    }
364
                }
365
            }
366

367
            if ($field['required']) {
35✔
368
                $type = GraphQLType::nonNull($type);
×
369
            }
370

371
            if (isset($fields[$key])) {
35✔
372
                if ($type instanceof ListOfType) {
35✔
373
                    $key .= '_list';
35✔
374
                }
375
            }
376

377
            $fields[$key] = ['type' => $type, 'name' => $key];
35✔
378
        }
379

380
        return new InputObjectType(['name' => $name, 'fields' => $fields]);
35✔
381
    }
382

383
    /**
384
     * A simplified version of convert type that does not support resources.
385
     */
386
    private function getParameterType(Type $type): GraphQLType
387
    {
388
        return match ($type->getBuiltinType()) {
35✔
389
            Type::BUILTIN_TYPE_BOOL => GraphQLType::boolean(),
35✔
390
            Type::BUILTIN_TYPE_INT => GraphQLType::int(),
35✔
391
            Type::BUILTIN_TYPE_FLOAT => GraphQLType::float(),
35✔
392
            Type::BUILTIN_TYPE_STRING => GraphQLType::string(),
35✔
393
            Type::BUILTIN_TYPE_ARRAY => GraphQLType::listOf($this->getParameterType($type->getCollectionValueTypes()[0])),
35✔
394
            Type::BUILTIN_TYPE_ITERABLE => GraphQLType::listOf($this->getParameterType($type->getCollectionValueTypes()[0])),
35✔
395
            Type::BUILTIN_TYPE_OBJECT => GraphQLType::string(),
35✔
396
            default => GraphQLType::string(),
35✔
397
        };
35✔
398
    }
399

400
    /**
401
     * Get the field configuration of a resource.
402
     *
403
     * @see http://webonyx.github.io/graphql-php/type-system/object-types/
404
     */
405
    private function getResourceFieldConfiguration(?string $property, ?string $fieldDescription, ?string $deprecationReason, Type $type, string $rootResource, bool $input, Operation $rootOperation, int $depth = 0, bool $forceNullable = false): ?array
406
    {
407
        try {
408
            $isCollectionType = $this->typeBuilder->isCollection($type);
35✔
409

410
            if (
411
                $isCollectionType
35✔
412
                && $collectionValueType = $type->getCollectionValueTypes()[0] ?? null
35✔
413
            ) {
414
                $resourceClass = $collectionValueType->getClassName();
35✔
415
            } else {
416
                $resourceClass = $type->getClassName();
35✔
417
            }
418

419
            $resourceOperation = $rootOperation;
35✔
420
            if ($resourceClass && $depth >= 1 && $this->resourceClassResolver->isResourceClass($resourceClass)) {
35✔
421
                $resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass);
×
422
                $resourceOperation = $resourceMetadataCollection->getOperation($isCollectionType ? 'collection_query' : 'item_query');
×
423
            }
424

425
            if (!$resourceOperation instanceof Operation) {
35✔
426
                throw new \LogicException('The resource operation should be a GraphQL operation.');
×
427
            }
428

429
            $graphqlType = $this->convertType($type, $input, $resourceOperation, $rootOperation, $resourceClass ?? '', $rootResource, $property, $depth, $forceNullable);
35✔
430

431
            $graphqlWrappedType = $graphqlType;
35✔
432
            if ($graphqlType instanceof WrappingType) {
35✔
433
                if (method_exists($graphqlType, 'getInnermostType')) {
35✔
434
                    $graphqlWrappedType = $graphqlType->getInnermostType();
35✔
435
                } else {
436
                    $graphqlWrappedType = $graphqlType->getWrappedType(true);
×
437
                }
438
            }
439
            $isStandardGraphqlType = \in_array($graphqlWrappedType, GraphQLType::getStandardTypes(), true);
35✔
440
            if ($isStandardGraphqlType) {
35✔
441
                $resourceClass = '';
35✔
442
            }
443

444
            // Check mercure attribute if it's a subscription at the root level.
445
            if ($rootOperation instanceof Subscription && null === $property && !$rootOperation->getMercure()) {
35✔
446
                return null;
×
447
            }
448

449
            $args = [];
35✔
450

451
            if (!$input && !$rootOperation instanceof Mutation && !$rootOperation instanceof Subscription && !$isStandardGraphqlType && $isCollectionType) {
35✔
452
                if (!$this->isEnumClass($resourceClass) && $this->pagination->isGraphQlEnabled($resourceOperation)) {
35✔
453
                    $args = $this->getGraphQlPaginationArgs($resourceOperation);
35✔
454
                }
455

456
                $args = $this->getFilterArgs($args, $resourceClass, $rootResource, $resourceOperation, $rootOperation, $property, $depth);
35✔
457
            }
458

459
            if ($this->itemResolverFactory instanceof ResolverFactory) {
35✔
460
                if ($isStandardGraphqlType || $input) {
27✔
461
                    $resolve = null;
27✔
462
                } else {
463
                    $resolve = ($this->itemResolverFactory)($resourceClass, $rootResource, $resourceOperation, $this->propertyMetadataFactory);
27✔
464
                }
465
            } else {
466
                if ($isStandardGraphqlType || $input) {
8✔
467
                    $resolve = null;
8✔
468
                } elseif (($rootOperation instanceof Mutation || $rootOperation instanceof Subscription) && $depth <= 0) {
8✔
469
                    $resolve = $rootOperation instanceof Mutation ? ($this->itemMutationResolverFactory)($resourceClass, $rootResource, $resourceOperation) : ($this->itemSubscriptionResolverFactory)($resourceClass, $rootResource, $resourceOperation);
8✔
470
                } elseif ($this->typeBuilder->isCollection($type)) {
8✔
471
                    $resolve = ($this->collectionResolverFactory)($resourceClass, $rootResource, $resourceOperation);
8✔
472
                } else {
473
                    $resolve = ($this->itemResolverFactory)($resourceClass, $rootResource, $resourceOperation);
8✔
474
                }
475
            }
476

477
            return [
35✔
478
                'type' => $graphqlType,
35✔
479
                'description' => $fieldDescription,
35✔
480
                'args' => $args,
35✔
481
                'resolve' => $resolve,
35✔
482
                'deprecationReason' => $deprecationReason,
35✔
483
            ];
35✔
484
        } catch (InvalidTypeException) {
4✔
485
            // just ignore invalid types
486
        }
487

488
        return null;
4✔
489
    }
490

491
    private function getGraphQlPaginationArgs(Operation $queryOperation): array
492
    {
493
        $paginationType = $this->pagination->getGraphQlPaginationType($queryOperation);
35✔
494

495
        if ('cursor' === $paginationType) {
35✔
496
            return [
35✔
497
                'first' => [
35✔
498
                    'type' => GraphQLType::int(),
35✔
499
                    'description' => 'Returns the first n elements from the list.',
35✔
500
                ],
35✔
501
                'last' => [
35✔
502
                    'type' => GraphQLType::int(),
35✔
503
                    'description' => 'Returns the last n elements from the list.',
35✔
504
                ],
35✔
505
                'before' => [
35✔
506
                    'type' => GraphQLType::string(),
35✔
507
                    'description' => 'Returns the elements in the list that come before the specified cursor.',
35✔
508
                ],
35✔
509
                'after' => [
35✔
510
                    'type' => GraphQLType::string(),
35✔
511
                    'description' => 'Returns the elements in the list that come after the specified cursor.',
35✔
512
                ],
35✔
513
            ];
35✔
514
        }
515

516
        $paginationOptions = $this->pagination->getOptions();
35✔
517

518
        $args = [
35✔
519
            $paginationOptions['page_parameter_name'] => [
35✔
520
                'type' => GraphQLType::int(),
35✔
521
                'description' => 'Returns the current page.',
35✔
522
            ],
35✔
523
        ];
35✔
524

525
        if ($paginationOptions['client_items_per_page']) {
35✔
526
            $args[$paginationOptions['items_per_page_parameter_name']] = [
35✔
527
                'type' => GraphQLType::int(),
35✔
528
                'description' => 'Returns the number of items per page.',
35✔
529
            ];
35✔
530
        }
531

532
        return $args;
35✔
533
    }
534

535
    private function getFilterArgs(array $args, ?string $resourceClass, string $rootResource, Operation $resourceOperation, Operation $rootOperation, ?string $property, int $depth): array
536
    {
537
        if (null === $resourceClass) {
35✔
538
            return $args;
×
539
        }
540

541
        foreach ($resourceOperation->getFilters() ?? [] as $filterId) {
35✔
542
            if (!$this->filterLocator->has($filterId)) {
35✔
543
                continue;
×
544
            }
545

546
            $entityClass = $resourceClass;
35✔
547
            if ($options = $resourceOperation->getStateOptions()) {
35✔
548
                if (class_exists(Options::class) && $options instanceof Options && $options->getEntityClass()) {
35✔
549
                    $entityClass = $options->getEntityClass();
35✔
550
                }
551

552
                if (class_exists(ODMOptions::class) && $options instanceof ODMOptions && $options->getDocumentClass()) {
35✔
553
                    $entityClass = $options->getDocumentClass();
35✔
554
                }
555
            }
556

557
            foreach ($this->filterLocator->get($filterId)->getDescription($entityClass) as $key => $description) {
35✔
558
                $nullable = isset($description['required']) ? !$description['required'] : true;
35✔
559
                $filterType = \in_array($description['type'], Type::$builtinTypes, true) ? new Type($description['type'], $nullable) : new Type('object', $nullable, $description['type']);
35✔
560
                $graphqlFilterType = $this->convertType($filterType, false, $resourceOperation, $rootOperation, $resourceClass, $rootResource, $property, $depth);
35✔
561

562
                if (str_ends_with($key, '[]')) {
35✔
563
                    $graphqlFilterType = GraphQLType::listOf($graphqlFilterType);
35✔
564
                    $key = substr($key, 0, -2).'_list';
35✔
565
                }
566

567
                /** @var string $key */
568
                $key = str_replace('.', $this->nestingSeparator, $key);
35✔
569

570
                parse_str($key, $parsed);
35✔
571
                if (\array_key_exists($key, $parsed) && \is_array($parsed[$key])) {
35✔
572
                    $parsed = [$key => ''];
×
573
                }
574
                array_walk_recursive($parsed, static function (&$v) use ($graphqlFilterType): void {
35✔
575
                    $v = $graphqlFilterType;
35✔
576
                });
35✔
577
                $args = $this->mergeFilterArgs($args, $parsed, $resourceOperation, $key);
35✔
578
            }
579
        }
580

581
        return $this->convertFilterArgsToTypes($args);
35✔
582
    }
583

584
    private function mergeFilterArgs(array $args, array $parsed, ?Operation $operation = null, string $original = ''): array
585
    {
586
        foreach ($parsed as $key => $value) {
35✔
587
            // Never override keys that cannot be merged
588
            if (isset($args[$key]) && !\is_array($args[$key])) {
35✔
589
                continue;
35✔
590
            }
591

592
            if (\is_array($value)) {
35✔
593
                $value = $this->mergeFilterArgs($args[$key] ?? [], $value);
35✔
594
                if (!isset($value['#name'])) {
35✔
595
                    $name = (false === $pos = strrpos($original, '[')) ? $original : substr($original, 0, (int) $pos);
35✔
596
                    $value['#name'] = ($operation ? $operation->getShortName() : '').'Filter_'.strtr($name, ['[' => '_', ']' => '', '.' => '__']);
35✔
597
                }
598
            }
599

600
            $args[$key] = $value;
35✔
601
        }
602

603
        return $args;
35✔
604
    }
605

606
    private function convertFilterArgsToTypes(array $args): array
607
    {
608
        foreach ($args as $key => $value) {
35✔
609
            if (strpos($key, '.')) {
35✔
610
                // Declare relations/nested fields in a GraphQL compatible syntax.
611
                $args[str_replace('.', $this->nestingSeparator, $key)] = $value;
×
612
                unset($args[$key]);
×
613
            }
614
        }
615

616
        foreach ($args as $key => $value) {
35✔
617
            if (!\is_array($value) || !isset($value['#name'])) {
35✔
618
                continue;
35✔
619
            }
620

621
            $name = $value['#name'];
35✔
622

623
            if ($this->typesContainer->has($name)) {
35✔
624
                $args[$key] = $this->typesContainer->get($name);
×
625
                continue;
×
626
            }
627

628
            unset($value['#name']);
35✔
629

630
            $filterArgType = GraphQLType::listOf(new InputObjectType([
35✔
631
                'name' => $name,
35✔
632
                'fields' => $this->convertFilterArgsToTypes($value),
35✔
633
            ]));
35✔
634

635
            $this->typesContainer->set($name, $filterArgType);
35✔
636

637
            $args[$key] = $filterArgType;
35✔
638
        }
639

640
        return $args;
35✔
641
    }
642

643
    /**
644
     * Converts a built-in type to its GraphQL equivalent.
645
     *
646
     * @throws InvalidTypeException
647
     */
648
    private function convertType(Type $type, bool $input, Operation $resourceOperation, Operation $rootOperation, string $resourceClass, string $rootResource, ?string $property, int $depth, bool $forceNullable = false): GraphQLType|ListOfType|NonNull
649
    {
650
        $graphqlType = $this->typeConverter->convertType($type, $input, $rootOperation, $resourceClass, $rootResource, $property, $depth);
35✔
651

652
        if (null === $graphqlType) {
35✔
653
            throw new InvalidTypeException(\sprintf('The type "%s" is not supported.', $type->getBuiltinType()));
4✔
654
        }
655

656
        if (\is_string($graphqlType)) {
35✔
657
            if (!$this->typesContainer->has($graphqlType)) {
×
658
                throw new InvalidTypeException(\sprintf('The GraphQL type %s is not valid. Valid types are: %s. Have you registered this type by implementing %s?', $graphqlType, implode(', ', array_keys($this->typesContainer->all())), TypeInterface::class));
×
659
            }
660

661
            $graphqlType = $this->typesContainer->get($graphqlType);
×
662
        }
663

664
        if ($this->typeBuilder->isCollection($type)) {
35✔
665
            if (!$input && !$this->isEnumClass($resourceClass) && $this->pagination->isGraphQlEnabled($resourceOperation)) {
35✔
666
                // Deprecated path, to remove in API Platform 4.
667
                if ($this->typeBuilder instanceof TypeBuilderInterface) {
35✔
668
                    return $this->typeBuilder->getResourcePaginatedCollectionType($graphqlType, $resourceClass, $resourceOperation);
×
669
                }
670

671
                return $this->typeBuilder->getPaginatedCollectionType($graphqlType, $resourceOperation);
35✔
672
            }
673

674
            return GraphQLType::listOf($graphqlType);
35✔
675
        }
676

677
        return $forceNullable || !$graphqlType instanceof NullableType || $type->isNullable() || ($rootOperation instanceof Mutation && 'update' === $rootOperation->getName())
35✔
678
            ? $graphqlType
35✔
679
            : GraphQLType::nonNull($graphqlType);
35✔
680
    }
681

682
    private function normalizePropertyName(string $property, string $resourceClass): string
683
    {
684
        if (null === $this->nameConverter) {
35✔
685
            return $property;
×
686
        }
687
        if ($this->nameConverter instanceof AdvancedNameConverterInterface || $this->nameConverter instanceof MetadataAwareNameConverter) {
35✔
688
            return $this->nameConverter->normalize($property, $resourceClass);
35✔
689
        }
690

691
        return $this->nameConverter->normalize($property);
×
692
    }
693
}
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