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

api-platform / core / 9809380648

05 Jul 2024 01:51PM UTC coverage: 63.374% (+0.08%) from 63.297%
9809380648

push

github

web-flow
fix(state): allow to skip parameter validator provider (#6452)

23 of 25 new or added lines in 9 files covered. (92.0%)

717 existing lines in 31 files now uncovered.

11143 of 17583 relevant lines covered (63.37%)

52.79 hits per line

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

80.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 ContextAwareTypeBuilderInterface|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, 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)
54
    {
UNCOV
55
        if ($typeBuilder instanceof TypeBuilderInterface) {
3✔
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
        }
UNCOV
58
        if ($typeBuilder instanceof TypeBuilderEnumInterface) {
3✔
59
            @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);
×
60
        }
UNCOV
61
        $this->typeBuilder = $typeBuilder;
3✔
62
    }
63

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

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

UNCOV
87
        $fieldName = lcfirst('item_query' === $operation->getName() ? $operation->getShortName() : $operation->getName().$operation->getShortName());
3✔
88

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

UNCOV
94
            return [$fieldName => array_merge($fieldConfiguration, $configuration)];
3✔
95
        }
96

97
        return [];
×
98
    }
99

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

UNCOV
109
        $fieldName = lcfirst('collection_query' === $operation->getName() ? $operation->getShortName() : $operation->getName().$operation->getShortName());
3✔
110

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

UNCOV
116
            return [Inflector::pluralize($fieldName) => array_merge($fieldConfiguration, $configuration)];
3✔
117
        }
118

119
        return [];
×
120
    }
121

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

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

UNCOV
135
        $mutationFields[$operation->getName().$operation->getShortName()] = $fieldConfiguration ?? [];
3✔
136

UNCOV
137
        return $mutationFields;
3✔
138
    }
139

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

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

UNCOV
153
        if (!$fieldConfiguration) {
3✔
154
            return [];
×
155
        }
156

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

UNCOV
163
        $subscriptionFields[$subscriptionName.$operation->getShortName().'Subscribe'] = $fieldConfiguration;
3✔
164

UNCOV
165
        return $subscriptionFields;
3✔
166
    }
167

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

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

184
            return [];
×
185
        }
186

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

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

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

203
            return $fields;
×
204
        }
205

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

UNCOV
213
        ++$depth; // increment the depth for the call to getResourceFieldConfiguration.
3✔
214

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

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

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

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

UNCOV
247
        return $fields;
3✔
248
    }
249

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

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

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

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

278
        return $enumCases;
×
279
    }
280

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

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

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

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

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

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

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

UNCOV
322
            $args[$key] = ['type' => GraphQLType::string()];
3✔
323

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

UNCOV
329
        return $args;
3✔
330
    }
331

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

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

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

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

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

UNCOV
376
            $fields[$key] = ['type' => $type, 'name' => $key];
3✔
377
        }
378

UNCOV
379
        return new InputObjectType(['name' => $name, 'fields' => $fields]);
3✔
380
    }
381

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

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

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

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

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

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

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

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

UNCOV
448
            $args = [];
3✔
449

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

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

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

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

487
        return null;
×
488
    }
489

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

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

UNCOV
515
        $paginationOptions = $this->pagination->getOptions();
3✔
516

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

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

UNCOV
531
        return $args;
3✔
532
    }
533

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

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

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

UNCOV
551
                if ($options instanceof ODMOptions && $options->getDocumentClass()) {
3✔
UNCOV
552
                    $entityClass = $options->getDocumentClass();
3✔
553
                }
554
            }
555

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

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

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

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

UNCOV
580
        return $this->convertFilterArgsToTypes($args);
3✔
581
    }
582

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

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

UNCOV
599
            $args[$key] = $value;
3✔
600
        }
601

UNCOV
602
        return $args;
3✔
603
    }
604

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

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

UNCOV
620
            $name = $value['#name'];
3✔
621

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

UNCOV
627
            unset($value['#name']);
3✔
628

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

UNCOV
634
            $this->typesContainer->set($name, $filterArgType);
3✔
635

UNCOV
636
            $args[$key] = $filterArgType;
3✔
637
        }
638

UNCOV
639
        return $args;
3✔
640
    }
641

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

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

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

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

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

UNCOV
670
                return $this->typeBuilder->getPaginatedCollectionType($graphqlType, $resourceOperation);
3✔
671
            }
672

UNCOV
673
            return GraphQLType::listOf($graphqlType);
3✔
674
        }
675

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

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

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