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

api-platform / core / 10508505187

22 Aug 2024 12:55PM UTC coverage: 7.704%. Remained the same
10508505187

push

github

soyuka
Merge 3.4

0 of 4 new or added lines in 4 files covered. (0.0%)

8840 existing lines in 284 files now uncovered.

12477 of 161949 relevant lines covered (7.7%)

22.99 hits per line

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

91.72
/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\InflectorInterface;
26
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
27
use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
28
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
29
use ApiPlatform\Metadata\ResourceClassResolverInterface;
30
use ApiPlatform\Metadata\Util\Inflector;
31
use ApiPlatform\State\Pagination\Pagination;
32
use GraphQL\Type\Definition\InputObjectType;
33
use GraphQL\Type\Definition\ListOfType;
34
use GraphQL\Type\Definition\NonNull;
35
use GraphQL\Type\Definition\NullableType;
36
use GraphQL\Type\Definition\Type as GraphQLType;
37
use GraphQL\Type\Definition\WrappingType;
38
use Psr\Container\ContainerInterface;
39
use Symfony\Component\Config\Definition\Exception\InvalidTypeException;
40
use Symfony\Component\PropertyInfo\Type;
41
use Symfony\Component\Serializer\NameConverter\AdvancedNameConverterInterface;
42
use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter;
43
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
44

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

54
    public function __construct(private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly ResourceClassResolverInterface $resourceClassResolver, private readonly TypesContainerInterface $typesContainer, ContextAwareTypeBuilderInterface $typeBuilder, private readonly TypeConverterInterface $typeConverter, private readonly ResolverFactoryInterface $itemResolverFactory, private readonly ?ResolverFactoryInterface $collectionResolverFactory, private readonly ?ResolverFactoryInterface $itemMutationResolverFactory, private readonly ?ResolverFactoryInterface $itemSubscriptionResolverFactory, private readonly ContainerInterface $filterLocator, private readonly Pagination $pagination, private readonly ?NameConverterInterface $nameConverter, private readonly string $nestingSeparator, private readonly ?InflectorInterface $inflector = new Inflector())
55
    {
UNCOV
56
        $this->typeBuilder = $typeBuilder;
414✔
57
    }
58

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

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

UNCOV
82
        $fieldName = lcfirst('item_query' === $operation->getName() ? $operation->getShortName() : $operation->getName().$operation->getShortName());
408✔
83

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

UNCOV
89
            return [$fieldName => array_merge($fieldConfiguration, $configuration)];
408✔
90
        }
91

92
        return [];
×
93
    }
94

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

UNCOV
104
        $fieldName = lcfirst('collection_query' === $operation->getName() ? $operation->getShortName() : $operation->getName().$operation->getShortName());
408✔
105

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

UNCOV
111
            return [$this->inflector->pluralize($fieldName) => array_merge($fieldConfiguration, $configuration)];
408✔
112
        }
113

114
        return [];
×
115
    }
116

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

UNCOV
126
        if ($fieldConfiguration = $this->getResourceFieldConfiguration(null, $description, $operation->getDeprecationReason(), $resourceType, $resourceClass, false, $operation)) {
408✔
UNCOV
127
            $fieldConfiguration['args'] += ['input' => $this->getResourceFieldConfiguration(null, null, $operation->getDeprecationReason(), $resourceType, $resourceClass, true, $operation)];
408✔
128
        }
129

UNCOV
130
        $mutationFields[$operation->getName().$operation->getShortName()] = $fieldConfiguration ?? [];
408✔
131

UNCOV
132
        return $mutationFields;
408✔
133
    }
134

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

UNCOV
144
        if ($fieldConfiguration = $this->getResourceFieldConfiguration(null, $description, $operation->getDeprecationReason(), $resourceType, $resourceClass, false, $operation)) {
408✔
UNCOV
145
            $fieldConfiguration['args'] += ['input' => $this->getResourceFieldConfiguration(null, null, $operation->getDeprecationReason(), $resourceType, $resourceClass, true, $operation)];
408✔
146
        }
147

UNCOV
148
        if (!$fieldConfiguration) {
408✔
149
            return [];
×
150
        }
151

UNCOV
152
        $subscriptionName = $operation->getName();
408✔
153
        // TODO: 3.0 change this
UNCOV
154
        if ('update_subscription' === $subscriptionName) {
408✔
UNCOV
155
            $subscriptionName = 'update';
408✔
156
        }
157

UNCOV
158
        $subscriptionFields[$subscriptionName.$operation->getShortName().'Subscribe'] = $fieldConfiguration;
408✔
159

UNCOV
160
        return $subscriptionFields;
408✔
161
    }
162

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

UNCOV
174
        if (null !== $ioMetadata && \array_key_exists('class', $ioMetadata) && null === $ioMetadata['class']) {
390✔
UNCOV
175
            if ($input) {
12✔
UNCOV
176
                return ['clientMutationId' => $clientMutationId];
9✔
177
            }
178

UNCOV
179
            return [];
9✔
180
        }
181

UNCOV
182
        if ($operation instanceof Subscription && $input) {
387✔
UNCOV
183
            return [
9✔
UNCOV
184
                'id' => $idField,
9✔
UNCOV
185
                'clientSubscriptionId' => $clientSubscriptionId,
9✔
UNCOV
186
            ];
9✔
187
        }
188

UNCOV
189
        if ('delete' === $operation->getName()) {
387✔
UNCOV
190
            $fields = [
14✔
UNCOV
191
                'id' => $idField,
14✔
UNCOV
192
            ];
14✔
193

UNCOV
194
            if ($input) {
14✔
UNCOV
195
                $fields['clientMutationId'] = $clientMutationId;
14✔
196
            }
197

UNCOV
198
            return $fields;
14✔
199
        }
200

UNCOV
201
        if (!$input || (!$operation->getResolver() && 'create' !== $operation->getName())) {
387✔
UNCOV
202
            $fields['id'] = $idField;
381✔
203
        }
UNCOV
204
        if ($input && $depth >= 1) {
387✔
UNCOV
205
            $fields['id'] = $optionalIdField;
12✔
206
        }
207

UNCOV
208
        ++$depth; // increment the depth for the call to getResourceFieldConfiguration.
387✔
209

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

219
                if (
UNCOV
220
                    !$propertyTypes
387✔
UNCOV
221
                    || (!$input && false === $propertyMetadata->isReadable())
387✔
UNCOV
222
                    || ($input && false === $propertyMetadata->isWritable())
387✔
223
                ) {
UNCOV
224
                    continue;
129✔
225
                }
226

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

UNCOV
238
        if ($operation instanceof Mutation && $input) {
387✔
UNCOV
239
            $fields['clientMutationId'] = $clientMutationId;
114✔
240
        }
241

UNCOV
242
        return $fields;
387✔
243
    }
244

245
    private function isEnumClass(string $resourceClass): bool
246
    {
UNCOV
247
        return is_a($resourceClass, \BackedEnum::class, true);
408✔
248
    }
249

250
    /**
251
     * {@inheritdoc}
252
     */
253
    public function getEnumFields(string $enumClass): array
254
    {
UNCOV
255
        $rEnum = new \ReflectionEnum($enumClass);
13✔
256

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

UNCOV
266
            $propertyMetadata = $this->propertyMetadataFactory->create($enumClass, $rCase->getName());
13✔
UNCOV
267
            if ($enumCaseDescription = $propertyMetadata->getDescription()) {
13✔
UNCOV
268
                $enumCase['description'] = $enumCaseDescription;
13✔
269
            }
UNCOV
270
            $enumCases[$rCase->getName()] = $enumCase;
13✔
271
        }
272

UNCOV
273
        return $enumCases;
13✔
274
    }
275

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

UNCOV
286
            $args[$id]['type'] = $this->typeConverter->resolveType($arg['type']);
408✔
287
        }
288

289
        /*
290
         * This is @experimental, read the comment on the parameterToObjectType function as additional information.
291
         */
UNCOV
292
        foreach ($operation->getParameters() ?? [] as $parameter) {
408✔
UNCOV
293
            $key = $parameter->getKey();
408✔
294

UNCOV
295
            if (str_contains($key, ':property')) {
408✔
UNCOV
296
                if (!($filterId = $parameter->getFilter()) || !$this->filterLocator->has($filterId)) {
408✔
297
                    continue;
×
298
                }
299

UNCOV
300
                $parsedKey = explode('[:property]', $key);
408✔
UNCOV
301
                $flattenFields = [];
408✔
UNCOV
302
                foreach ($this->filterLocator->get($filterId)->getDescription($operation->getClass()) as $key => $value) {
408✔
UNCOV
303
                    $values = [];
408✔
UNCOV
304
                    parse_str($key, $values);
408✔
UNCOV
305
                    if (isset($values[$parsedKey[0]])) {
408✔
UNCOV
306
                        $values = $values[$parsedKey[0]];
408✔
307
                    }
308

UNCOV
309
                    $name = key($values);
408✔
UNCOV
310
                    $flattenFields[] = ['name' => $name, 'required' => $value['required'] ?? null, 'description' => $value['description'] ?? null, 'leafs' => $values[$name], 'type' => $value['type'] ?? 'string'];
408✔
311
                }
312

UNCOV
313
                $args[$parsedKey[0]] = $this->parameterToObjectType($flattenFields, $parsedKey[0]);
408✔
UNCOV
314
                continue;
408✔
315
            }
316

UNCOV
317
            $args[$key] = ['type' => GraphQLType::string()];
408✔
318

UNCOV
319
            if ($parameter->getRequired()) {
408✔
320
                $args[$key]['type'] = GraphQLType::nonNull($args[$key]['type']);
×
321
            }
322
        }
323

UNCOV
324
        return $args;
408✔
325
    }
326

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

UNCOV
342
            if (\is_array($l = $field['leafs'])) {
408✔
UNCOV
343
                if (0 === key($l)) {
408✔
UNCOV
344
                    $key = $key;
408✔
UNCOV
345
                    $type = GraphQLType::listOf($type);
408✔
346
                } else {
UNCOV
347
                    $n = [];
408✔
UNCOV
348
                    foreach ($field['leafs'] as $l => $value) {
408✔
UNCOV
349
                        $n[] = ['required' => null, 'name' => $l, 'leafs' => $value, 'type' => 'string', 'description' => null];
408✔
350
                    }
351

UNCOV
352
                    $type = $this->parameterToObjectType($n, $key);
408✔
UNCOV
353
                    if (isset($fields[$key]) && ($t = $fields[$key]['type']) instanceof InputObjectType) {
408✔
UNCOV
354
                        $t = $fields[$key]['type'];
408✔
UNCOV
355
                        $t->config['fields'] = array_merge($t->config['fields'], $type->config['fields']);
408✔
UNCOV
356
                        $type = $t;
408✔
357
                    }
358
                }
359
            }
360

UNCOV
361
            if ($field['required']) {
408✔
362
                $type = GraphQLType::nonNull($type);
×
363
            }
364

UNCOV
365
            if (isset($fields[$key])) {
408✔
UNCOV
366
                if ($type instanceof ListOfType) {
408✔
UNCOV
367
                    $key .= '_list';
408✔
368
                }
369
            }
370

UNCOV
371
            $fields[$key] = ['type' => $type, 'name' => $key];
408✔
372
        }
373

UNCOV
374
        return new InputObjectType(['name' => $name, 'fields' => $fields]);
408✔
375
    }
376

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

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

404
            if (
UNCOV
405
                $isCollectionType
408✔
UNCOV
406
                && $collectionValueType = $type->getCollectionValueTypes()[0] ?? null
408✔
407
            ) {
UNCOV
408
                $resourceClass = $collectionValueType->getClassName();
408✔
409
            } else {
UNCOV
410
                $resourceClass = $type->getClassName();
408✔
411
            }
412

UNCOV
413
            $resourceOperation = $rootOperation;
408✔
UNCOV
414
            if ($resourceClass && $depth >= 1 && $this->resourceClassResolver->isResourceClass($resourceClass)) {
408✔
UNCOV
415
                $resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass);
279✔
UNCOV
416
                $resourceOperation = $resourceMetadataCollection->getOperation($isCollectionType ? 'collection_query' : 'item_query');
279✔
417
            }
418

UNCOV
419
            if (!$resourceOperation instanceof Operation) {
408✔
420
                throw new \LogicException('The resource operation should be a GraphQL operation.');
×
421
            }
422

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

UNCOV
425
            $graphqlWrappedType = $graphqlType;
408✔
UNCOV
426
            if ($graphqlType instanceof WrappingType) {
408✔
UNCOV
427
                if (method_exists($graphqlType, 'getInnermostType')) {
408✔
UNCOV
428
                    $graphqlWrappedType = $graphqlType->getInnermostType();
408✔
429
                } else {
430
                    $graphqlWrappedType = $graphqlType->getWrappedType(true);
×
431
                }
432
            }
UNCOV
433
            $isStandardGraphqlType = \in_array($graphqlWrappedType, GraphQLType::getStandardTypes(), true);
408✔
UNCOV
434
            if ($isStandardGraphqlType) {
408✔
UNCOV
435
                $resourceClass = '';
387✔
436
            }
437

438
            // Check mercure attribute if it's a subscription at the root level.
UNCOV
439
            if ($rootOperation instanceof Subscription && null === $property && !$rootOperation->getMercure()) {
408✔
440
                return null;
×
441
            }
442

UNCOV
443
            $args = [];
408✔
444

UNCOV
445
            if (!$input && !$rootOperation instanceof Mutation && !$rootOperation instanceof Subscription && !$isStandardGraphqlType && $isCollectionType) {
408✔
UNCOV
446
                if (!$this->isEnumClass($resourceClass) && $this->pagination->isGraphQlEnabled($resourceOperation)) {
408✔
UNCOV
447
                    $args = $this->getGraphQlPaginationArgs($resourceOperation);
408✔
448
                }
449

UNCOV
450
                $args = $this->getFilterArgs($args, $resourceClass, $rootResource, $resourceOperation, $rootOperation, $property, $depth);
408✔
451
            }
452

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

UNCOV
471
            return [
408✔
UNCOV
472
                'type' => $graphqlType,
408✔
UNCOV
473
                'description' => $fieldDescription,
408✔
UNCOV
474
                'args' => $args,
408✔
UNCOV
475
                'resolve' => $resolve,
408✔
UNCOV
476
                'deprecationReason' => $deprecationReason,
408✔
UNCOV
477
            ];
408✔
UNCOV
478
        } catch (InvalidTypeException) {
86✔
479
            // just ignore invalid types
480
        }
481

UNCOV
482
        return null;
86✔
483
    }
484

485
    private function getGraphQlPaginationArgs(Operation $queryOperation): array
486
    {
UNCOV
487
        $paginationType = $this->pagination->getGraphQlPaginationType($queryOperation);
408✔
488

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

UNCOV
510
        $paginationOptions = $this->pagination->getOptions();
408✔
511

UNCOV
512
        $args = [
408✔
UNCOV
513
            $paginationOptions['page_parameter_name'] => [
408✔
UNCOV
514
                'type' => GraphQLType::int(),
408✔
UNCOV
515
                'description' => 'Returns the current page.',
408✔
UNCOV
516
            ],
408✔
UNCOV
517
        ];
408✔
518

UNCOV
519
        if ($paginationOptions['client_items_per_page']) {
408✔
UNCOV
520
            $args[$paginationOptions['items_per_page_parameter_name']] = [
408✔
UNCOV
521
                'type' => GraphQLType::int(),
408✔
UNCOV
522
                'description' => 'Returns the number of items per page.',
408✔
UNCOV
523
            ];
408✔
524
        }
525

UNCOV
526
        return $args;
408✔
527
    }
528

529
    private function getFilterArgs(array $args, ?string $resourceClass, string $rootResource, Operation $resourceOperation, Operation $rootOperation, ?string $property, int $depth): array
530
    {
UNCOV
531
        if (null === $resourceClass) {
408✔
532
            return $args;
×
533
        }
534

UNCOV
535
        foreach ($resourceOperation->getFilters() ?? [] as $filterId) {
408✔
UNCOV
536
            if (!$this->filterLocator->has($filterId)) {
408✔
537
                continue;
×
538
            }
539

UNCOV
540
            $entityClass = $resourceClass;
408✔
UNCOV
541
            if ($options = $resourceOperation->getStateOptions()) {
408✔
UNCOV
542
                if ($options instanceof Options && $options->getEntityClass()) {
408✔
UNCOV
543
                    $entityClass = $options->getEntityClass();
408✔
544
                }
545

UNCOV
546
                if ($options instanceof ODMOptions && $options->getDocumentClass()) {
408✔
UNCOV
547
                    $entityClass = $options->getDocumentClass();
408✔
548
                }
549
            }
550

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

UNCOV
556
                if (str_ends_with($key, '[]')) {
408✔
UNCOV
557
                    $graphqlFilterType = GraphQLType::listOf($graphqlFilterType);
408✔
UNCOV
558
                    $key = substr($key, 0, -2).'_list';
408✔
559
                }
560

561
                /** @var string $key */
UNCOV
562
                $key = str_replace('.', $this->nestingSeparator, $key);
408✔
563

UNCOV
564
                parse_str($key, $parsed);
408✔
UNCOV
565
                if (\array_key_exists($key, $parsed) && \is_array($parsed[$key])) {
408✔
566
                    $parsed = [$key => ''];
×
567
                }
UNCOV
568
                array_walk_recursive($parsed, static function (&$v) use ($graphqlFilterType): void {
408✔
UNCOV
569
                    $v = $graphqlFilterType;
408✔
UNCOV
570
                });
408✔
UNCOV
571
                $args = $this->mergeFilterArgs($args, $parsed, $resourceOperation, $key);
408✔
572
            }
573
        }
574

UNCOV
575
        return $this->convertFilterArgsToTypes($args);
408✔
576
    }
577

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

UNCOV
586
            if (\is_array($value)) {
408✔
UNCOV
587
                $value = $this->mergeFilterArgs($args[$key] ?? [], $value);
408✔
UNCOV
588
                if (!isset($value['#name'])) {
408✔
UNCOV
589
                    $name = (false === $pos = strrpos($original, '[')) ? $original : substr($original, 0, (int) $pos);
408✔
UNCOV
590
                    $value['#name'] = ($operation ? $operation->getShortName() : '').'Filter_'.strtr($name, ['[' => '_', ']' => '', '.' => '__']);
408✔
591
                }
592
            }
593

UNCOV
594
            $args[$key] = $value;
408✔
595
        }
596

UNCOV
597
        return $args;
408✔
598
    }
599

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

UNCOV
610
        foreach ($args as $key => $value) {
408✔
UNCOV
611
            if (!\is_array($value) || !isset($value['#name'])) {
408✔
UNCOV
612
                continue;
408✔
613
            }
614

UNCOV
615
            $name = $value['#name'];
408✔
616

UNCOV
617
            if ($this->typesContainer->has($name)) {
408✔
UNCOV
618
                $args[$key] = $this->typesContainer->get($name);
26✔
UNCOV
619
                continue;
26✔
620
            }
621

UNCOV
622
            unset($value['#name']);
408✔
623

UNCOV
624
            $filterArgType = GraphQLType::listOf(new InputObjectType([
408✔
UNCOV
625
                'name' => $name,
408✔
UNCOV
626
                'fields' => $this->convertFilterArgsToTypes($value),
408✔
UNCOV
627
            ]));
408✔
628

UNCOV
629
            $this->typesContainer->set($name, $filterArgType);
408✔
630

UNCOV
631
            $args[$key] = $filterArgType;
408✔
632
        }
633

UNCOV
634
        return $args;
408✔
635
    }
636

637
    /**
638
     * Converts a built-in type to its GraphQL equivalent.
639
     *
640
     * @throws InvalidTypeException
641
     */
642
    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
643
    {
UNCOV
644
        $graphqlType = $this->typeConverter->convertType($type, $input, $rootOperation, $resourceClass, $rootResource, $property, $depth);
408✔
645

UNCOV
646
        if (null === $graphqlType) {
408✔
UNCOV
647
            throw new InvalidTypeException(\sprintf('The type "%s" is not supported.', $type->getBuiltinType()));
86✔
648
        }
649

UNCOV
650
        if (\is_string($graphqlType)) {
408✔
UNCOV
651
            if (!$this->typesContainer->has($graphqlType)) {
311✔
652
                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));
×
653
            }
654

UNCOV
655
            $graphqlType = $this->typesContainer->get($graphqlType);
311✔
656
        }
657

UNCOV
658
        if ($this->typeBuilder->isCollection($type)) {
408✔
UNCOV
659
            if (!$input && !$this->isEnumClass($resourceClass) && $this->pagination->isGraphQlEnabled($resourceOperation)) {
408✔
UNCOV
660
                return $this->typeBuilder->getPaginatedCollectionType($graphqlType, $resourceOperation);
408✔
661
            }
662

UNCOV
663
            return GraphQLType::listOf($graphqlType);
408✔
664
        }
665

UNCOV
666
        return $forceNullable || !$graphqlType instanceof NullableType || $type->isNullable() || ($rootOperation instanceof Mutation && 'update' === $rootOperation->getName())
408✔
UNCOV
667
            ? $graphqlType
408✔
UNCOV
668
            : GraphQLType::nonNull($graphqlType);
408✔
669
    }
670

671
    private function normalizePropertyName(string $property, string $resourceClass): string
672
    {
UNCOV
673
        if (null === $this->nameConverter) {
381✔
674
            return $property;
×
675
        }
UNCOV
676
        if ($this->nameConverter instanceof AdvancedNameConverterInterface || $this->nameConverter instanceof MetadataAwareNameConverter) {
381✔
UNCOV
677
            return $this->nameConverter->normalize($property, $resourceClass);
381✔
678
        }
679

680
        return $this->nameConverter->normalize($property);
×
681
    }
682
}
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