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

api-platform / core / 8483007955

29 Mar 2024 04:11PM UTC coverage: 57.113% (-0.2%) from 57.324%
8483007955

push

github

web-flow
feat(doctrine): parameter filter extension (#6248)

* feat(doctrine): parameter filtering

* feat(graphql): parameter graphql arguments

8 of 89 new or added lines in 6 files covered. (8.99%)

2 existing lines in 1 file now uncovered.

9908 of 17348 relevant lines covered (57.11%)

47.4 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\NameConverterInterface;
42

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

52
    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)
53
    {
54
        if ($typeBuilder instanceof TypeBuilderInterface) {
×
55
            @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);
×
56
        }
57
        if ($typeBuilder instanceof TypeBuilderEnumInterface) {
×
58
            @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);
×
59
        }
60
        $this->typeBuilder = $typeBuilder;
×
61
    }
62

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

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

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

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

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

96
        return [];
×
97
    }
98

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

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

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

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

118
        return [];
×
119
    }
120

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

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

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

136
        return $mutationFields;
×
137
    }
138

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

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

152
        if (!$fieldConfiguration) {
×
153
            return [];
×
154
        }
155

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

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

164
        return $subscriptionFields;
×
165
    }
166

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

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

183
            return [];
×
184
        }
185

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

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

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

202
            return $fields;
×
203
        }
204

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

212
        ++$depth; // increment the depth for the call to getResourceFieldConfiguration.
×
213

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

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

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

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

246
        return $fields;
×
247
    }
248

249
    private function isEnumClass(string $resourceClass): bool
250
    {
251
        return is_a($resourceClass, \BackedEnum::class, true);
×
252
    }
253

254
    /**
255
     * {@inheritdoc}
256
     */
257
    public function getEnumFields(string $enumClass): array
258
    {
259
        $rEnum = new \ReflectionEnum($enumClass);
×
260

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

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

277
        return $enumCases;
×
278
    }
279

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

290
            $args[$id]['type'] = $this->typeConverter->resolveType($arg['type']);
×
291
        }
292

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

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

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

NEW
313
                    $name = key($values);
×
NEW
314
                    $flattenFields[] = ['name' => $name, 'required' => $value['required'] ?? null, 'description' => $value['description'] ?? null, 'leafs' => $values[$name], 'type' => $value['type'] ?? 'string'];
×
315
                }
316

NEW
317
                $args[$parsedKey[0]] = $this->parameterToObjectType($flattenFields, $parsedKey[0]);
×
NEW
318
                continue;
×
319
            }
320

NEW
321
            $args[$key] = ['type' => GraphQLType::string()];
×
322

NEW
323
            if ($parameter->getRequired()) {
×
NEW
324
                $args[$key]['type'] = GraphQLType::nonNull($args[$key]['type']);
×
325
            }
326
        }
327

UNCOV
328
        return $args;
×
329
    }
330

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

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

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

NEW
365
            if ($field['required']) {
×
NEW
366
                $type = GraphQLType::nonNull($type);
×
367
            }
368

NEW
369
            if (isset($fields[$key])) {
×
NEW
370
                if ($type instanceof ListOfType) {
×
NEW
371
                    $key .= '_list';
×
372
                }
373
            }
374

NEW
375
            $fields[$key] = ['type' => $type, 'name' => $key];
×
376
        }
377

NEW
378
        return new InputObjectType(['name' => $name, 'fields' => $fields]);
×
379
    }
380

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

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

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

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

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

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

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

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

447
            $args = [];
×
448

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

454
                $args = $this->getFilterArgs($args, $resourceClass, $rootResource, $resourceOperation, $rootOperation, $property, $depth);
×
455
            }
456

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

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

486
        return null;
×
487
    }
488

489
    private function getGraphQlPaginationArgs(Operation $queryOperation): array
490
    {
491
        $paginationType = $this->pagination->getGraphQlPaginationType($queryOperation);
×
492

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

514
        $paginationOptions = $this->pagination->getOptions();
×
515

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

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

530
        return $args;
×
531
    }
532

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

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

544
            $entityClass = $resourceClass;
×
545
            if ($options = $resourceOperation->getStateOptions()) {
×
546
                if ($options instanceof Options && $options->getEntityClass()) {
×
547
                    $entityClass = $options->getEntityClass();
×
548
                }
549

550
                if ($options instanceof ODMOptions && $options->getDocumentClass()) {
×
551
                    $entityClass = $options->getDocumentClass();
×
552
                }
553
            }
554

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

560
                if (str_ends_with($key, '[]')) {
×
561
                    $graphqlFilterType = GraphQLType::listOf($graphqlFilterType);
×
562
                    $key = substr($key, 0, -2).'_list';
×
563
                }
564

565
                /** @var string $key */
566
                $key = str_replace('.', $this->nestingSeparator, $key);
×
567

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

579
        return $this->convertFilterArgsToTypes($args);
×
580
    }
581

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

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

598
            $args[$key] = $value;
×
599
        }
600

601
        return $args;
×
602
    }
603

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

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

619
            $name = $value['#name'];
×
620

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

626
            unset($value['#name']);
×
627

628
            $filterArgType = GraphQLType::listOf(new InputObjectType([
×
629
                'name' => $name,
×
630
                'fields' => $this->convertFilterArgsToTypes($value),
×
631
            ]));
×
632

633
            $this->typesContainer->set($name, $filterArgType);
×
634

635
            $args[$key] = $filterArgType;
×
636
        }
637

638
        return $args;
×
639
    }
640

641
    /**
642
     * Converts a built-in type to its GraphQL equivalent.
643
     *
644
     * @throws InvalidTypeException
645
     */
646
    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
647
    {
648
        $graphqlType = $this->typeConverter->convertType($type, $input, $rootOperation, $resourceClass, $rootResource, $property, $depth);
×
649

650
        if (null === $graphqlType) {
×
651
            throw new InvalidTypeException(sprintf('The type "%s" is not supported.', $type->getBuiltinType()));
×
652
        }
653

654
        if (\is_string($graphqlType)) {
×
655
            if (!$this->typesContainer->has($graphqlType)) {
×
656
                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));
×
657
            }
658

659
            $graphqlType = $this->typesContainer->get($graphqlType);
×
660
        }
661

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

669
                return $this->typeBuilder->getPaginatedCollectionType($graphqlType, $resourceOperation);
×
670
            }
671

672
            return GraphQLType::listOf($graphqlType);
×
673
        }
674

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

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

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