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

api-platform / core / 9348911355

03 Jun 2024 10:58AM UTC coverage: 66.524% (-0.3%) from 66.816%
9348911355

push

github

soyuka
chore: skip behat deprecation

16418 of 24680 relevant lines covered (66.52%)

40.32 hits per line

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

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

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

53
    public function __construct(private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly ResourceClassResolverInterface $resourceClassResolver, private readonly TypesContainerInterface $typesContainer, 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)
54
    {
55
        if ($typeBuilder instanceof TypeBuilderInterface) {
×
56
            @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);
×
57
        }
58
        $this->typeBuilder = $typeBuilder;
×
59
    }
60

61
    /**
62
     * {@inheritdoc}
63
     */
64
    public function getNodeQueryFields(): array
65
    {
66
        return [
×
67
            'type' => $this->typeBuilder->getNodeInterface(),
×
68
            'args' => [
×
69
                'id' => ['type' => GraphQLType::nonNull(GraphQLType::id())],
×
70
            ],
×
71
            'resolve' => ($this->itemResolverFactory)(),
×
72
        ];
×
73
    }
74

75
    /**
76
     * {@inheritdoc}
77
     */
78
    public function getItemQueryFields(string $resourceClass, Operation $operation, array $configuration): array
79
    {
80
        if ($operation instanceof Query && $operation->getNested()) {
×
81
            return [];
×
82
        }
83

84
        $fieldName = lcfirst('item_query' === $operation->getName() ? $operation->getShortName() : $operation->getName().$operation->getShortName());
×
85

86
        if ($fieldConfiguration = $this->getResourceFieldConfiguration(null, $operation->getDescription(), $operation->getDeprecationReason(), new Type(Type::BUILTIN_TYPE_OBJECT, true, $resourceClass), $resourceClass, false, $operation)) {
×
87
            $args = $this->resolveResourceArgs($configuration['args'] ?? [], $operation);
×
88
            $extraArgs = $this->resolveResourceArgs($operation->getExtraArgs() ?? [], $operation);
×
89
            $configuration['args'] = $args ?: $configuration['args'] ?? ['id' => ['type' => GraphQLType::nonNull(GraphQLType::id())]] + $extraArgs;
×
90

91
            return [$fieldName => array_merge($fieldConfiguration, $configuration)];
×
92
        }
93

94
        return [];
×
95
    }
96

97
    /**
98
     * {@inheritdoc}
99
     */
100
    public function getCollectionQueryFields(string $resourceClass, Operation $operation, array $configuration): array
101
    {
102
        if ($operation instanceof Query && $operation->getNested()) {
×
103
            return [];
×
104
        }
105

106
        $fieldName = lcfirst('collection_query' === $operation->getName() ? $operation->getShortName() : $operation->getName().$operation->getShortName());
×
107

108
        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)) {
×
109
            $args = $this->resolveResourceArgs($configuration['args'] ?? [], $operation);
×
110
            $extraArgs = $this->resolveResourceArgs($operation->getExtraArgs() ?? [], $operation);
×
111
            $configuration['args'] = $args ?: $configuration['args'] ?? $fieldConfiguration['args'] + $extraArgs;
×
112

113
            return [Inflector::pluralize($fieldName) => array_merge($fieldConfiguration, $configuration)];
×
114
        }
115

116
        return [];
×
117
    }
118

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

128
        if ($fieldConfiguration = $this->getResourceFieldConfiguration(null, $description, $operation->getDeprecationReason(), $resourceType, $resourceClass, false, $operation)) {
×
129
            $fieldConfiguration['args'] += ['input' => $this->getResourceFieldConfiguration(null, null, $operation->getDeprecationReason(), $resourceType, $resourceClass, true, $operation)];
×
130
        }
131

132
        $mutationFields[$operation->getName().$operation->getShortName()] = $fieldConfiguration ?? [];
×
133

134
        return $mutationFields;
×
135
    }
136

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

146
        if ($fieldConfiguration = $this->getResourceFieldConfiguration(null, $description, $operation->getDeprecationReason(), $resourceType, $resourceClass, false, $operation)) {
×
147
            $fieldConfiguration['args'] += ['input' => $this->getResourceFieldConfiguration(null, null, $operation->getDeprecationReason(), $resourceType, $resourceClass, true, $operation)];
×
148
        }
149

150
        if (!$fieldConfiguration) {
×
151
            return [];
×
152
        }
153

154
        $subscriptionName = $operation->getName();
×
155
        // TODO: 3.0 change this
156
        if ('update_subscription' === $subscriptionName) {
×
157
            $subscriptionName = 'update';
×
158
        }
159

160
        $subscriptionFields[$subscriptionName.$operation->getShortName().'Subscribe'] = $fieldConfiguration;
×
161

162
        return $subscriptionFields;
×
163
    }
164

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

176
        if (null !== $ioMetadata && \array_key_exists('class', $ioMetadata) && null === $ioMetadata['class']) {
×
177
            if ($input) {
×
178
                return ['clientMutationId' => $clientMutationId];
×
179
            }
180

181
            return [];
×
182
        }
183

184
        if ($operation instanceof Subscription && $input) {
×
185
            return [
×
186
                'id' => $idField,
×
187
                'clientSubscriptionId' => $clientSubscriptionId,
×
188
            ];
×
189
        }
190

191
        if ('delete' === $operation->getName()) {
×
192
            $fields = [
×
193
                'id' => $idField,
×
194
            ];
×
195

196
            if ($input) {
×
197
                $fields['clientMutationId'] = $clientMutationId;
×
198
            }
199

200
            return $fields;
×
201
        }
202

203
        if (!$input || (!$operation->getResolver() && 'create' !== $operation->getName())) {
×
204
            $fields['id'] = $idField;
×
205
        }
206
        if ($input && $depth >= 1) {
×
207
            $fields['id'] = $optionalIdField;
×
208
        }
209

210
        ++$depth; // increment the depth for the call to getResourceFieldConfiguration.
×
211

212
        if (null !== $resourceClass) {
×
213
            foreach ($this->propertyNameCollectionFactory->create($resourceClass) as $property) {
×
214
                $context = [
×
215
                    'normalization_groups' => $operation->getNormalizationContext()['groups'] ?? null,
×
216
                    'denormalization_groups' => $operation->getDenormalizationContext()['groups'] ?? null,
×
217
                ];
×
218
                $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $property, $context);
×
219
                $propertyTypes = $propertyMetadata->getBuiltinTypes();
×
220

221
                if (
222
                    !$propertyTypes
×
223
                    || (!$input && false === $propertyMetadata->isReadable())
×
224
                    || ($input && false === $propertyMetadata->isWritable())
×
225
                ) {
226
                    continue;
×
227
                }
228

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

240
        if ($operation instanceof Mutation && $input) {
×
241
            $fields['clientMutationId'] = $clientMutationId;
×
242
        }
243

244
        return $fields;
×
245
    }
246

247
    /**
248
     * {@inheritdoc}
249
     */
250
    public function getEnumFields(string $enumClass): array
251
    {
252
        $rEnum = new \ReflectionEnum($enumClass);
×
253

254
        $enumCases = [];
×
255
        /* @var \ReflectionEnumUnitCase|\ReflectionEnumBackedCase */
256
        foreach ($rEnum->getCases() as $rCase) {
×
257
            if ($rCase instanceof \ReflectionEnumBackedCase) {
×
258
                $enumCase = ['value' => $rCase->getBackingValue()];
×
259
            } else {
260
                $enumCase = ['value' => $rCase->getValue()];
×
261
            }
262

263
            $propertyMetadata = $this->propertyMetadataFactory->create($enumClass, $rCase->getName());
×
264
            if ($enumCaseDescription = $propertyMetadata->getDescription()) {
×
265
                $enumCase['description'] = $enumCaseDescription;
×
266
            }
267
            $enumCases[$rCase->getName()] = $enumCase;
×
268
        }
269

270
        return $enumCases;
×
271
    }
272

273
    /**
274
     * {@inheritdoc}
275
     */
276
    public function resolveResourceArgs(array $args, Operation $operation): array
277
    {
278
        foreach ($args as $id => $arg) {
×
279
            if (!isset($arg['type'])) {
×
280
                throw new \InvalidArgumentException(sprintf('The argument "%s" of the custom operation "%s" in %s needs a "type" option.', $id, $operation->getName(), $operation->getShortName()));
×
281
            }
282

283
            $args[$id]['type'] = $this->typeConverter->resolveType($arg['type']);
×
284
        }
285

286
        return $args;
×
287
    }
288

289
    /**
290
     * Get the field configuration of a resource.
291
     *
292
     * @see http://webonyx.github.io/graphql-php/type-system/object-types/
293
     */
294
    private function getResourceFieldConfiguration(?string $property, ?string $fieldDescription, ?string $deprecationReason, Type $type, string $rootResource, bool $input, Operation $rootOperation, int $depth = 0, bool $forceNullable = false): ?array
295
    {
296
        try {
297
            $isCollectionType = $this->typeBuilder->isCollection($type);
×
298

299
            if (
300
                $isCollectionType
×
301
                && $collectionValueType = $type->getCollectionValueTypes()[0] ?? null
×
302
            ) {
303
                $resourceClass = $collectionValueType->getClassName();
×
304
            } else {
305
                $resourceClass = $type->getClassName();
×
306
            }
307

308
            $resourceOperation = $rootOperation;
×
309
            if ($resourceClass && $depth >= 1 && $this->resourceClassResolver->isResourceClass($resourceClass)) {
×
310
                $resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass);
×
311
                $resourceOperation = $resourceMetadataCollection->getOperation($isCollectionType ? 'collection_query' : 'item_query');
×
312
            }
313

314
            if (!$resourceOperation instanceof Operation) {
×
315
                throw new \LogicException('The resource operation should be a GraphQL operation.');
×
316
            }
317

318
            $graphqlType = $this->convertType($type, $input, $resourceOperation, $rootOperation, $resourceClass ?? '', $rootResource, $property, $depth, $forceNullable);
×
319

320
            $graphqlWrappedType = $graphqlType;
×
321
            if ($graphqlType instanceof WrappingType) {
×
322
                if (method_exists($graphqlType, 'getInnermostType')) {
×
323
                    $graphqlWrappedType = $graphqlType->getInnermostType();
×
324
                } else {
325
                    $graphqlWrappedType = $graphqlType->getWrappedType(true);
×
326
                }
327
            }
328
            $isStandardGraphqlType = \in_array($graphqlWrappedType, GraphQLType::getStandardTypes(), true);
×
329
            if ($isStandardGraphqlType) {
×
330
                $resourceClass = '';
×
331
            }
332

333
            // Check mercure attribute if it's a subscription at the root level.
334
            if ($rootOperation instanceof Subscription && null === $property && !$rootOperation->getMercure()) {
×
335
                return null;
×
336
            }
337

338
            $args = [];
×
339

340
            if (!$input && !$rootOperation instanceof Mutation && !$rootOperation instanceof Subscription && !$isStandardGraphqlType && $isCollectionType) {
×
341
                if ($this->pagination->isGraphQlEnabled($resourceOperation)) {
×
342
                    $args = $this->getGraphQlPaginationArgs($resourceOperation);
×
343
                }
344

345
                $args = $this->getFilterArgs($args, $resourceClass, $rootResource, $resourceOperation, $rootOperation, $property, $depth);
×
346
            }
347

348
            if ($this->itemResolverFactory instanceof ResolverFactory) {
×
349
                if ($isStandardGraphqlType || $input) {
×
350
                    $resolve = null;
×
351
                } else {
352
                    $resolve = ($this->itemResolverFactory)($resourceClass, $rootResource, $resourceOperation, $this->propertyMetadataFactory);
×
353
                }
354
            } else {
355
                if ($isStandardGraphqlType || $input) {
×
356
                    $resolve = null;
×
357
                } elseif (($rootOperation instanceof Mutation || $rootOperation instanceof Subscription) && $depth <= 0) {
×
358
                    $resolve = $rootOperation instanceof Mutation ? ($this->itemMutationResolverFactory)($resourceClass, $rootResource, $resourceOperation) : ($this->itemSubscriptionResolverFactory)($resourceClass, $rootResource, $resourceOperation);
×
359
                } elseif ($this->typeBuilder->isCollection($type)) {
×
360
                    $resolve = ($this->collectionResolverFactory)($resourceClass, $rootResource, $resourceOperation);
×
361
                } else {
362
                    $resolve = ($this->itemResolverFactory)($resourceClass, $rootResource, $resourceOperation);
×
363
                }
364
            }
365

366
            return [
×
367
                'type' => $graphqlType,
×
368
                'description' => $fieldDescription,
×
369
                'args' => $args,
×
370
                'resolve' => $resolve,
×
371
                'deprecationReason' => $deprecationReason,
×
372
            ];
×
373
        } catch (InvalidTypeException) {
×
374
            // just ignore invalid types
375
        }
376

377
        return null;
×
378
    }
379

380
    private function getGraphQlPaginationArgs(Operation $queryOperation): array
381
    {
382
        $paginationType = $this->pagination->getGraphQlPaginationType($queryOperation);
×
383

384
        if ('cursor' === $paginationType) {
×
385
            return [
×
386
                'first' => [
×
387
                    'type' => GraphQLType::int(),
×
388
                    'description' => 'Returns the first n elements from the list.',
×
389
                ],
×
390
                'last' => [
×
391
                    'type' => GraphQLType::int(),
×
392
                    'description' => 'Returns the last n elements from the list.',
×
393
                ],
×
394
                'before' => [
×
395
                    'type' => GraphQLType::string(),
×
396
                    'description' => 'Returns the elements in the list that come before the specified cursor.',
×
397
                ],
×
398
                'after' => [
×
399
                    'type' => GraphQLType::string(),
×
400
                    'description' => 'Returns the elements in the list that come after the specified cursor.',
×
401
                ],
×
402
            ];
×
403
        }
404

405
        $paginationOptions = $this->pagination->getOptions();
×
406

407
        $args = [
×
408
            $paginationOptions['page_parameter_name'] => [
×
409
                'type' => GraphQLType::int(),
×
410
                'description' => 'Returns the current page.',
×
411
            ],
×
412
        ];
×
413

414
        if ($paginationOptions['client_items_per_page']) {
×
415
            $args[$paginationOptions['items_per_page_parameter_name']] = [
×
416
                'type' => GraphQLType::int(),
×
417
                'description' => 'Returns the number of items per page.',
×
418
            ];
×
419
        }
420

421
        return $args;
×
422
    }
423

424
    private function getFilterArgs(array $args, ?string $resourceClass, string $rootResource, Operation $resourceOperation, Operation $rootOperation, ?string $property, int $depth): array
425
    {
426
        if (null === $resourceClass) {
×
427
            return $args;
×
428
        }
429

430
        foreach ($resourceOperation->getFilters() ?? [] as $filterId) {
×
431
            if (!$this->filterLocator->has($filterId)) {
×
432
                continue;
×
433
            }
434

435
            $entityClass = $resourceClass;
×
436
            if ($options = $resourceOperation->getStateOptions()) {
×
437
                if ($options instanceof Options && $options->getEntityClass()) {
×
438
                    $entityClass = $options->getEntityClass();
×
439
                }
440

441
                if ($options instanceof ODMOptions && $options->getDocumentClass()) {
×
442
                    $entityClass = $options->getDocumentClass();
×
443
                }
444
            }
445

446
            foreach ($this->filterLocator->get($filterId)->getDescription($entityClass) as $key => $value) {
×
447
                $nullable = isset($value['required']) ? !$value['required'] : true;
×
448
                $filterType = \in_array($value['type'], Type::$builtinTypes, true) ? new Type($value['type'], $nullable) : new Type('object', $nullable, $value['type']);
×
449
                $graphqlFilterType = $this->convertType($filterType, false, $resourceOperation, $rootOperation, $resourceClass, $rootResource, $property, $depth);
×
450

451
                if (str_ends_with($key, '[]')) {
×
452
                    $graphqlFilterType = GraphQLType::listOf($graphqlFilterType);
×
453
                    $key = substr($key, 0, -2).'_list';
×
454
                }
455

456
                /** @var string $key */
457
                $key = str_replace('.', $this->nestingSeparator, $key);
×
458

459
                parse_str($key, $parsed);
×
460
                if (\array_key_exists($key, $parsed) && \is_array($parsed[$key])) {
×
461
                    $parsed = [$key => ''];
×
462
                }
463
                array_walk_recursive($parsed, static function (&$value) use ($graphqlFilterType): void {
×
464
                    $value = $graphqlFilterType;
×
465
                });
×
466
                $args = $this->mergeFilterArgs($args, $parsed, $resourceOperation, $key);
×
467
            }
468
        }
469

470
        return $this->convertFilterArgsToTypes($args);
×
471
    }
472

473
    private function mergeFilterArgs(array $args, array $parsed, ?Operation $operation = null, string $original = ''): array
474
    {
475
        foreach ($parsed as $key => $value) {
×
476
            // Never override keys that cannot be merged
477
            if (isset($args[$key]) && !\is_array($args[$key])) {
×
478
                continue;
×
479
            }
480

481
            if (\is_array($value)) {
×
482
                $value = $this->mergeFilterArgs($args[$key] ?? [], $value);
×
483
                if (!isset($value['#name'])) {
×
484
                    $name = (false === $pos = strrpos($original, '[')) ? $original : substr($original, 0, (int) $pos);
×
485
                    $value['#name'] = ($operation ? $operation->getShortName() : '').'Filter_'.strtr($name, ['[' => '_', ']' => '', '.' => '__']);
×
486
                }
487
            }
488

489
            $args[$key] = $value;
×
490
        }
491

492
        return $args;
×
493
    }
494

495
    private function convertFilterArgsToTypes(array $args): array
496
    {
497
        foreach ($args as $key => $value) {
×
498
            if (strpos($key, '.')) {
×
499
                // Declare relations/nested fields in a GraphQL compatible syntax.
500
                $args[str_replace('.', $this->nestingSeparator, $key)] = $value;
×
501
                unset($args[$key]);
×
502
            }
503
        }
504

505
        foreach ($args as $key => $value) {
×
506
            if (!\is_array($value) || !isset($value['#name'])) {
×
507
                continue;
×
508
            }
509

510
            $name = $value['#name'];
×
511

512
            if ($this->typesContainer->has($name)) {
×
513
                $args[$key] = $this->typesContainer->get($name);
×
514
                continue;
×
515
            }
516

517
            unset($value['#name']);
×
518

519
            $filterArgType = GraphQLType::listOf(new InputObjectType([
×
520
                'name' => $name,
×
521
                'fields' => $this->convertFilterArgsToTypes($value),
×
522
            ]));
×
523

524
            $this->typesContainer->set($name, $filterArgType);
×
525

526
            $args[$key] = $filterArgType;
×
527
        }
528

529
        return $args;
×
530
    }
531

532
    /**
533
     * Converts a built-in type to its GraphQL equivalent.
534
     *
535
     * @throws InvalidTypeException
536
     */
537
    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
538
    {
539
        $graphqlType = $this->typeConverter->convertType($type, $input, $rootOperation, $resourceClass, $rootResource, $property, $depth);
×
540

541
        if (null === $graphqlType) {
×
542
            throw new InvalidTypeException(sprintf('The type "%s" is not supported.', $type->getBuiltinType()));
×
543
        }
544

545
        if (\is_string($graphqlType)) {
×
546
            if (!$this->typesContainer->has($graphqlType)) {
×
547
                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));
×
548
            }
549

550
            $graphqlType = $this->typesContainer->get($graphqlType);
×
551
        }
552

553
        if ($this->typeBuilder->isCollection($type)) {
×
554
            if (!$input && $this->pagination->isGraphQlEnabled($resourceOperation)) {
×
555
                // Deprecated path, to remove in API Platform 4.
556
                if ($this->typeBuilder instanceof TypeBuilderInterface) {
×
557
                    return $this->typeBuilder->getResourcePaginatedCollectionType($graphqlType, $resourceClass, $resourceOperation);
×
558
                }
559

560
                return $this->typeBuilder->getPaginatedCollectionType($graphqlType, $resourceOperation);
×
561
            }
562

563
            return GraphQLType::listOf($graphqlType);
×
564
        }
565

566
        return $forceNullable || !$graphqlType instanceof NullableType || $type->isNullable() || ($rootOperation instanceof Mutation && 'update' === $rootOperation->getName())
×
567
            ? $graphqlType
×
568
            : GraphQLType::nonNull($graphqlType);
×
569
    }
570

571
    private function normalizePropertyName(string $property, string $resourceClass): string
572
    {
573
        if (null === $this->nameConverter) {
×
574
            return $property;
×
575
        }
576
        if ($this->nameConverter instanceof AdvancedNameConverterInterface || $this->nameConverter instanceof MetadataAwareNameConverter) {
×
577
            return $this->nameConverter->normalize($property, $resourceClass);
×
578
        }
579

580
        return $this->nameConverter->normalize($property);
×
581
    }
582
}
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