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

api-platform / core / 20847864477

09 Jan 2026 09:47AM UTC coverage: 29.1% (+0.005%) from 29.095%
20847864477

Pull #7649

github

web-flow
Merge b342dd5db into d640d106b
Pull Request #7649: feat(validator): uuid/ulid parameter validation

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

15050 existing lines in 491 files now uncovered.

16996 of 58406 relevant lines covered (29.1%)

81.8 hits per line

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

88.18
/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\GraphQl\Exception\InvalidTypeException;
17
use ApiPlatform\GraphQl\Resolver\Factory\ResolverFactoryInterface;
18
use ApiPlatform\GraphQl\Type\Definition\TypeInterface;
19
use ApiPlatform\Metadata\FilterInterface;
20
use ApiPlatform\Metadata\GraphQl\Mutation;
21
use ApiPlatform\Metadata\GraphQl\Operation;
22
use ApiPlatform\Metadata\GraphQl\Query;
23
use ApiPlatform\Metadata\GraphQl\Subscription;
24
use ApiPlatform\Metadata\InflectorInterface;
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\Metadata\Util\PropertyInfoToTypeInfoHelper;
31
use ApiPlatform\Metadata\Util\TypeHelper;
32
use ApiPlatform\State\Pagination\Pagination;
33
use ApiPlatform\State\Util\StateOptionsTrait;
34
use GraphQL\Type\Definition\InputObjectType;
35
use GraphQL\Type\Definition\ListOfType;
36
use GraphQL\Type\Definition\NonNull;
37
use GraphQL\Type\Definition\NullableType;
38
use GraphQL\Type\Definition\Type as GraphQLType;
39
use GraphQL\Type\Definition\WrappingType;
40
use Psr\Container\ContainerInterface;
41
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
42
use Symfony\Component\PropertyInfo\Type as LegacyType;
43
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
44
use Symfony\Component\TypeInfo\Type;
45
use Symfony\Component\TypeInfo\Type\CollectionType;
46
use Symfony\Component\TypeInfo\Type\ObjectType;
47
use Symfony\Component\TypeInfo\TypeIdentifier;
48

49
/**
50
 * Builds the GraphQL fields.
51
 *
52
 * @author Alan Poulain <contact@alanpoulain.eu>
53
 */
54
final class FieldsBuilder implements FieldsBuilderEnumInterface
55
{
56
    use StateOptionsTrait;
57

58
    private readonly ContextAwareTypeBuilderInterface $typeBuilder;
59

60
    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 $resolverFactory, private readonly ContainerInterface $filterLocator, private readonly Pagination $pagination, private readonly ?NameConverterInterface $nameConverter, private readonly string $nestingSeparator, private readonly ?InflectorInterface $inflector = new Inflector())
61
    {
UNCOV
62
        $this->typeBuilder = $typeBuilder;
308✔
63
    }
64

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

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

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

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

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

98
        return [];
×
99
    }
100

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

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

UNCOV
112
        if ($fieldConfiguration = $this->getResourceFieldConfiguration(null, $operation->getDescription(), $operation->getDeprecationReason(), Type::collection(Type::object(\stdClass::class), Type::object($resourceClass)), $resourceClass, false, $operation)) {
304✔
UNCOV
113
            $args = $this->resolveResourceArgs($configuration['args'] ?? [], $operation);
304✔
UNCOV
114
            $extraArgs = $this->resolveResourceArgs($operation->getExtraArgs() ?? [], $operation);
304✔
UNCOV
115
            $configuration['args'] = $args ?: $configuration['args'] ?? $fieldConfiguration['args'] + $extraArgs;
304✔
116

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

120
        return [];
×
121
    }
122

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

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

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

UNCOV
138
        return $mutationFields;
290✔
139
    }
140

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

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

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

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

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

166
        return $subscriptionFields;
278✔
167
    }
168

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

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

185
            return [];
6✔
186
        }
187

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

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

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

204
            return $fields;
10✔
205
        }
206

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

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

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

UNCOV
224
                if (!method_exists(PropertyInfoExtractor::class, 'getType')) {
290✔
225
                    $propertyTypes = $propertyMetadata->getBuiltinTypes();
×
226

227
                    if (
228
                        !$propertyTypes
×
229
                        || (!$input && false === $propertyMetadata->isReadable())
×
230
                        || ($input && false === $propertyMetadata->isWritable())
×
231
                    ) {
232
                        continue;
×
233
                    }
234

235
                    // guess union/intersect types: check each type until finding a valid one
236
                    foreach ($propertyTypes as $propertyType) {
×
237
                        if ($fieldConfiguration = $this->getResourceFieldConfiguration($property, $propertyMetadata->getDescription(), $propertyMetadata->getDeprecationReason(), $propertyType, $resourceClass, $input, $operation, $depth, null !== $propertyMetadata->getSecurity())) {
×
238
                            $fields['id' === $property ? '_id' : $this->normalizePropertyName($property, $resourceClass)] = $fieldConfiguration;
×
239
                            // stop at the first valid type
240
                            break;
×
241
                        }
242
                    }
243
                } else {
244
                    if (
UNCOV
245
                        !($propertyType = $propertyMetadata->getNativeType())
290✔
UNCOV
246
                        || (!$input && false === $propertyMetadata->isReadable())
290✔
UNCOV
247
                        || ($input && false === $propertyMetadata->isWritable())
290✔
248
                    ) {
UNCOV
249
                        continue;
101✔
250
                    }
251

UNCOV
252
                    if ($fieldConfiguration = $this->getResourceFieldConfiguration($property, $propertyMetadata->getDescription(), $propertyMetadata->getDeprecationReason(), $propertyType, $resourceClass, $input, $operation, $depth, null !== $propertyMetadata->getSecurity())) {
290✔
UNCOV
253
                        $fields['id' === $property ? '_id' : $this->normalizePropertyName($property, $resourceClass)] = $fieldConfiguration;
290✔
254
                    }
255
                }
256
            }
257
        }
258

UNCOV
259
        if ($operation instanceof Mutation && $input) {
290✔
UNCOV
260
            $fields['clientMutationId'] = $clientMutationId;
80✔
261
        }
262

UNCOV
263
        return $fields;
290✔
264
    }
265

266
    private function isEnumClass(string $resourceClass): bool
267
    {
UNCOV
268
        return is_a($resourceClass, \BackedEnum::class, true);
304✔
269
    }
270

271
    /**
272
     * {@inheritdoc}
273
     */
274
    public function getEnumFields(string $enumClass): array
275
    {
UNCOV
276
        $rEnum = new \ReflectionEnum($enumClass);
11✔
277

UNCOV
278
        $enumCases = [];
11✔
279
        /* @var \ReflectionEnumUnitCase|\ReflectionEnumBackedCase */
UNCOV
280
        foreach ($rEnum->getCases() as $rCase) {
11✔
UNCOV
281
            if ($rCase instanceof \ReflectionEnumBackedCase) {
11✔
UNCOV
282
                $enumCase = ['value' => $rCase->getBackingValue()];
11✔
283
            } else {
284
                $enumCase = ['value' => $rCase->getValue()];
×
285
            }
286

UNCOV
287
            $propertyMetadata = $this->propertyMetadataFactory->create($enumClass, $rCase->getName());
11✔
UNCOV
288
            if ($enumCaseDescription = $propertyMetadata->getDescription()) {
11✔
UNCOV
289
                $enumCase['description'] = $enumCaseDescription;
11✔
290
            }
UNCOV
291
            $enumCases[$rCase->getName()] = $enumCase;
11✔
292
        }
293

UNCOV
294
        return $enumCases;
11✔
295
    }
296

297
    /**
298
     * {@inheritdoc}
299
     */
300
    public function resolveResourceArgs(array $args, Operation $operation): array
301
    {
UNCOV
302
        foreach ($args as $id => $arg) {
304✔
303
            if (!isset($arg['type'])) {
278✔
304
                throw new \InvalidArgumentException(\sprintf('The argument "%s" of the custom operation "%s" in %s needs a "type" option.', $id, $operation->getName(), $operation->getShortName()));
×
305
            }
306

307
            $args[$id]['type'] = $this->typeConverter->resolveType($arg['type']);
278✔
308
        }
309

UNCOV
310
        return $args;
304✔
311
    }
312

313
    /**
314
     * Transform the result of a parse_str to a GraphQL object type.
315
     * We should consider merging getFilterArgs and this, `getFilterArgs` uses `convertType` whereas we assume that parameters have only scalar types.
316
     * Note that this method has a lower complexity then the `getFilterArgs` one.
317
     * TODO: Is there a use case with an argument being a complex type (eg: a Resource, Enum etc.)?
318
     *
319
     * @param array<array{name: string, required: bool|null, description: string|null, leafs: string|array, type: string}> $flattenFields
320
     */
321
    private function parameterToObjectType(array $flattenFields, string $name): InputObjectType
322
    {
UNCOV
323
        $fields = [];
280✔
UNCOV
324
        foreach ($flattenFields as $field) {
280✔
UNCOV
325
            $key = $field['name'];
280✔
UNCOV
326
            $type = \in_array($field['type'], TypeIdentifier::values(), true) ? Type::builtin($field['type']) : Type::object($field['type']);
280✔
UNCOV
327
            if (!$field['required']) {
280✔
UNCOV
328
                $type = Type::nullable($type);
280✔
329
            }
330

UNCOV
331
            $type = $this->getParameterType($type);
280✔
UNCOV
332
            if (\is_array($l = $field['leafs'])) {
280✔
UNCOV
333
                if (0 === key($l)) {
280✔
UNCOV
334
                    $key = $key;
280✔
UNCOV
335
                    $type = GraphQLType::listOf($type);
280✔
336
                } else {
UNCOV
337
                    $n = [];
280✔
UNCOV
338
                    foreach ($field['leafs'] as $l => $value) {
280✔
UNCOV
339
                        $n[] = ['required' => null, 'name' => $l, 'leafs' => $value, 'type' => 'string', 'description' => null];
280✔
340
                    }
341

UNCOV
342
                    $type = $this->parameterToObjectType($n, $key);
280✔
UNCOV
343
                    if (isset($fields[$key]) && ($t = $fields[$key]['type']) instanceof InputObjectType) {
280✔
344
                        $t = $fields[$key]['type'];
×
345
                        $t->config['fields'] = array_merge($t->config['fields'], $type->config['fields']);
×
346
                        $type = $t;
×
347
                    }
348
                }
349
            }
350

UNCOV
351
            if ($field['required']) {
280✔
352
                $type = GraphQLType::nonNull($type);
×
353
            }
354

UNCOV
355
            if (isset($fields[$key])) {
280✔
356
                if ($type instanceof ListOfType) {
×
357
                    $key .= '_list';
×
358
                } elseif ($fields[$key]['type'] instanceof InputObjectType && !$type instanceof InputObjectType) {
×
359
                    continue;
×
360
                }
361
            }
362

UNCOV
363
            $fields[$key] = ['type' => $type, 'name' => $key];
280✔
364
        }
365

UNCOV
366
        return new InputObjectType(['name' => $name, 'fields' => $fields]);
280✔
367
    }
368

369
    /**
370
     * A simplified version of convert type that does not support resources.
371
     */
372
    private function getParameterType(Type $type): GraphQLType
373
    {
UNCOV
374
        if ($type->isIdentifiedBy(TypeIdentifier::BOOL)) {
280✔
375
            return GraphQLType::boolean();
×
376
        }
377

UNCOV
378
        if ($type->isIdentifiedBy(TypeIdentifier::INT)) {
280✔
379
            return GraphQLType::int();
×
380
        }
381

UNCOV
382
        if ($type->isIdentifiedBy(TypeIdentifier::FLOAT)) {
280✔
383
            return GraphQLType::float();
×
384
        }
385

UNCOV
386
        if ($type->isIdentifiedBy(TypeIdentifier::STRING, TypeIdentifier::OBJECT)) {
280✔
UNCOV
387
            return GraphQLType::string();
280✔
388
        }
389

390
        if ($type instanceof CollectionType) {
×
391
            return GraphQLType::listOf($this->getParameterType($type->getCollectionValueType()));
×
392
        }
393

394
        return GraphQLType::string();
×
395
    }
396

397
    /**
398
     * Get the field configuration of a resource.
399
     *
400
     * @see http://webonyx.github.io/graphql-php/type-system/object-types/
401
     */
402
    private function getResourceFieldConfiguration(?string $property, ?string $fieldDescription, ?string $deprecationReason, Type|LegacyType $type, string $rootResource, bool $input, Operation $rootOperation, int $depth = 0, bool $forceNullable = false): ?array
403
    {
UNCOV
404
        if ($type instanceof LegacyType) {
304✔
405
            $type = PropertyInfoToTypeInfoHelper::convertLegacyTypesToType([$type]);
×
406
        }
407

408
        try {
UNCOV
409
            $isCollectionType = $type->isSatisfiedBy(fn ($t) => $t instanceof CollectionType) && ($v = TypeHelper::getCollectionValueType($type)) && TypeHelper::getClassName($v);
304✔
410

UNCOV
411
            $valueType = $type;
304✔
UNCOV
412
            if ($isCollectionType) {
304✔
UNCOV
413
                $valueType = TypeHelper::getCollectionValueType($type);
304✔
414
            }
415

416
            /** @var class-string|null $resourceClass */
UNCOV
417
            $resourceClass = null;
304✔
UNCOV
418
            $typeIsResourceClass = function (Type $type) use (&$resourceClass): bool {
304✔
UNCOV
419
                return $type instanceof ObjectType && $this->resourceClassResolver->isResourceClass($resourceClass = $type->getClassName());
304✔
UNCOV
420
            };
304✔
421

UNCOV
422
            $isResourceClass = $valueType->isSatisfiedBy($typeIsResourceClass);
304✔
423

UNCOV
424
            $resourceOperation = $rootOperation;
304✔
UNCOV
425
            if ($resourceClass && $depth >= 1 && $isResourceClass) {
304✔
UNCOV
426
                $resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass);
191✔
UNCOV
427
                $resourceOperation = $resourceMetadataCollection->getOperation($isCollectionType ? 'collection_query' : 'item_query');
191✔
428
            }
429

UNCOV
430
            if (!$resourceOperation instanceof Operation) {
304✔
431
                throw new \LogicException('The resource operation should be a GraphQL operation.');
×
432
            }
433

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

UNCOV
436
            $graphqlWrappedType = $graphqlType;
304✔
UNCOV
437
            if ($graphqlType instanceof WrappingType) {
304✔
UNCOV
438
                if (method_exists($graphqlType, 'getInnermostType')) {
304✔
UNCOV
439
                    $graphqlWrappedType = $graphqlType->getInnermostType();
304✔
440
                } else {
441
                    $graphqlWrappedType = $graphqlType->getWrappedType(true);
×
442
                }
443
            }
UNCOV
444
            $isStandardGraphqlType = \in_array($graphqlWrappedType, GraphQLType::getStandardTypes(), true);
304✔
UNCOV
445
            if ($isStandardGraphqlType) {
304✔
UNCOV
446
                $resourceClass = '';
290✔
447
            }
448

449
            // Check mercure attribute if it's a subscription at the root level.
UNCOV
450
            if ($rootOperation instanceof Subscription && null === $property && !$rootOperation->getMercure()) {
304✔
451
                return null;
×
452
            }
453

UNCOV
454
            $args = [];
304✔
455

UNCOV
456
            if (!$input && !$rootOperation instanceof Mutation && !$rootOperation instanceof Subscription && !$isStandardGraphqlType) {
304✔
UNCOV
457
                if ($isCollectionType) {
304✔
UNCOV
458
                    if (!$this->isEnumClass($resourceClass) && $this->pagination->isGraphQlEnabled($resourceOperation)) {
304✔
UNCOV
459
                        $args = $this->getGraphQlPaginationArgs($resourceOperation);
290✔
460
                    }
461

UNCOV
462
                    $args = $this->getFilterArgs($args, $resourceClass, $rootResource, $resourceOperation, $rootOperation, $property, $depth);
304✔
463

464
                    // Also register parameter args in the types container
465
                    // Note: This is a workaround, for more information read the comment on the parameterToObjectType function.
UNCOV
466
                    foreach ($this->getParameterArgs($rootOperation) as $key => $arg) {
304✔
UNCOV
467
                        if ($arg instanceof InputObjectType || (\is_array($arg) && isset($arg['name']))) {
280✔
UNCOV
468
                            $this->typesContainer->set(\is_array($arg) ? $arg['name'] : $arg->name(), $arg);
280✔
469
                        }
UNCOV
470
                        $args[$key] = $arg;
280✔
471
                    }
472
                }
473
            }
474

UNCOV
475
            if ($isStandardGraphqlType || $input) {
304✔
UNCOV
476
                $resolve = null;
304✔
477
            } else {
UNCOV
478
                $resolve = ($this->resolverFactory)($resourceClass, $rootResource, $resourceOperation, $this->propertyMetadataFactory);
304✔
479
            }
480

UNCOV
481
            return [
304✔
UNCOV
482
                'type' => $graphqlType,
304✔
UNCOV
483
                'description' => $fieldDescription,
304✔
UNCOV
484
                'args' => $args,
304✔
UNCOV
485
                'resolve' => $resolve,
304✔
UNCOV
486
                'deprecationReason' => $deprecationReason,
304✔
UNCOV
487
            ];
304✔
UNCOV
488
        } catch (InvalidTypeException) {
58✔
489
            // just ignore invalid types
490
        }
491

UNCOV
492
        return null;
58✔
493
    }
494

495
    /*
496
     * This function is @experimental, read the comment on the parameterToObjectType function for additional information.
497
     * @experimental
498
     */
499
    private function getParameterArgs(Operation $operation, array $args = []): array
500
    {
UNCOV
501
        $groups = [];
304✔
502

UNCOV
503
        foreach ($operation->getParameters() ?? [] as $parameter) {
304✔
UNCOV
504
            $key = $parameter->getKey();
280✔
505

UNCOV
506
            if (str_contains($key, '[')) {
280✔
UNCOV
507
                $key = str_replace('.', $this->nestingSeparator, $key);
280✔
UNCOV
508
                parse_str($key, $values);
280✔
UNCOV
509
                $rootKey = key($values);
280✔
510

UNCOV
511
                $leafs = $values[$rootKey];
280✔
UNCOV
512
                $name = key($leafs);
280✔
513

UNCOV
514
                $filterLeafs = [];
280✔
UNCOV
515
                if (($filterId = $parameter->getFilter()) && $this->filterLocator->has($filterId)) {
280✔
UNCOV
516
                    $filter = $this->filterLocator->get($filterId);
280✔
517

UNCOV
518
                    if ($filter instanceof FilterInterface) {
280✔
UNCOV
519
                        $property = $parameter->getProperty() ?? $name;
280✔
UNCOV
520
                        $property = str_replace('.', $this->nestingSeparator, $property);
280✔
UNCOV
521
                        $description = $filter->getDescription($operation->getClass());
280✔
522

UNCOV
523
                        foreach ($description as $descKey => $descValue) {
280✔
UNCOV
524
                            $descKey = str_replace('.', $this->nestingSeparator, $descKey);
280✔
UNCOV
525
                            parse_str($descKey, $descValues);
280✔
UNCOV
526
                            if (isset($descValues[$property]) && \is_array($descValues[$property])) {
280✔
UNCOV
527
                                $filterLeafs = array_merge($filterLeafs, $descValues[$property]);
280✔
528
                            }
529
                        }
530
                    }
531
                }
532

UNCOV
533
                if ($filterLeafs) {
280✔
UNCOV
534
                    $leafs[$name] = $filterLeafs;
280✔
535
                }
536

UNCOV
537
                $groups[$rootKey][] = [
280✔
UNCOV
538
                    'name' => $name,
280✔
UNCOV
539
                    'leafs' => $leafs[$name],
280✔
UNCOV
540
                    'required' => $parameter->getRequired(),
280✔
UNCOV
541
                    'description' => $parameter->getDescription(),
280✔
UNCOV
542
                    'type' => 'string',
280✔
UNCOV
543
                ];
280✔
UNCOV
544
                continue;
280✔
545
            }
546

UNCOV
547
            $args[$key] = ['type' => GraphQLType::string()];
280✔
548

UNCOV
549
            if ($parameter->getRequired()) {
280✔
550
                $args[$key]['type'] = GraphQLType::nonNull($args[$key]['type']);
×
551
            }
552
        }
553

UNCOV
554
        foreach ($groups as $key => $flattenFields) {
304✔
UNCOV
555
            $name = $key.$operation->getShortName().$operation->getName();
280✔
UNCOV
556
            $inputObject = $this->parameterToObjectType($flattenFields, $name);
280✔
UNCOV
557
            $this->typesContainer->set($name, $inputObject);
280✔
UNCOV
558
            $args[$key] = $inputObject;
280✔
559
        }
560

UNCOV
561
        return $args;
304✔
562
    }
563

564
    private function getGraphQlPaginationArgs(Operation $queryOperation): array
565
    {
UNCOV
566
        $paginationType = $this->pagination->getGraphQlPaginationType($queryOperation);
290✔
567

UNCOV
568
        if ('cursor' === $paginationType) {
290✔
UNCOV
569
            return [
290✔
UNCOV
570
                'first' => [
290✔
UNCOV
571
                    'type' => GraphQLType::int(),
290✔
UNCOV
572
                    'description' => 'Returns the first n elements from the list.',
290✔
UNCOV
573
                ],
290✔
UNCOV
574
                'last' => [
290✔
UNCOV
575
                    'type' => GraphQLType::int(),
290✔
UNCOV
576
                    'description' => 'Returns the last n elements from the list.',
290✔
UNCOV
577
                ],
290✔
UNCOV
578
                'before' => [
290✔
UNCOV
579
                    'type' => GraphQLType::string(),
290✔
UNCOV
580
                    'description' => 'Returns the elements in the list that come before the specified cursor.',
290✔
UNCOV
581
                ],
290✔
UNCOV
582
                'after' => [
290✔
UNCOV
583
                    'type' => GraphQLType::string(),
290✔
UNCOV
584
                    'description' => 'Returns the elements in the list that come after the specified cursor.',
290✔
UNCOV
585
                ],
290✔
UNCOV
586
            ];
290✔
587
        }
588

589
        $paginationOptions = $this->pagination->getOptions();
278✔
590

591
        $args = [
278✔
592
            $paginationOptions['page_parameter_name'] => [
278✔
593
                'type' => GraphQLType::int(),
278✔
594
                'description' => 'Returns the current page.',
278✔
595
            ],
278✔
596
        ];
278✔
597

598
        if ($paginationOptions['client_items_per_page']) {
278✔
599
            $args[$paginationOptions['items_per_page_parameter_name']] = [
278✔
600
                'type' => GraphQLType::int(),
278✔
601
                'description' => 'Returns the number of items per page.',
278✔
602
            ];
278✔
603
        }
604

605
        return $args;
278✔
606
    }
607

608
    private function getFilterArgs(array $args, ?string $resourceClass, string $rootResource, Operation $resourceOperation, Operation $rootOperation, ?string $property, int $depth): array
609
    {
UNCOV
610
        if (null === $resourceClass) {
304✔
611
            return $args;
×
612
        }
613

UNCOV
614
        foreach ($resourceOperation->getFilters() ?? [] as $filterId) {
304✔
615
            if (!$this->filterLocator->has($filterId)) {
278✔
616
                continue;
×
617
            }
618

619
            $entityClass = $this->getStateOptionsClass($resourceOperation, $resourceOperation->getClass());
278✔
620
            foreach ($this->filterLocator->get($filterId)->getDescription($entityClass) as $key => $description) {
278✔
621
                $filterType = \in_array($description['type'], TypeIdentifier::values(), true) ? Type::builtin($description['type']) : Type::object($description['type']);
278✔
622
                if (!($description['required'] ?? false)) {
278✔
623
                    $filterType = Type::nullable($filterType);
278✔
624
                }
625
                $graphqlFilterType = $this->convertType($filterType, false, $resourceOperation, $rootOperation, $resourceClass, $rootResource, $property, $depth);
278✔
626

627
                if (str_ends_with($key, '[]')) {
278✔
628
                    $graphqlFilterType = GraphQLType::listOf($graphqlFilterType);
278✔
629
                    $key = substr($key, 0, -2).'_list';
278✔
630
                }
631

632
                /** @var string $key */
633
                $key = str_replace('.', $this->nestingSeparator, $key);
278✔
634

635
                parse_str($key, $parsed);
278✔
636
                if (\array_key_exists($key, $parsed) && \is_array($parsed[$key])) {
278✔
637
                    $parsed = [$key => ''];
×
638
                }
639
                array_walk_recursive($parsed, static function (&$v) use ($graphqlFilterType): void {
278✔
640
                    $v = $graphqlFilterType;
278✔
641
                });
278✔
642
                $args = $this->mergeFilterArgs($args, $parsed, $resourceOperation, $key);
278✔
643
            }
644
        }
645

UNCOV
646
        return $this->convertFilterArgsToTypes($args);
304✔
647
    }
648

649
    private function mergeFilterArgs(array $args, array $parsed, ?Operation $operation = null, string $original = ''): array
650
    {
651
        foreach ($parsed as $key => $value) {
278✔
652
            // Never override keys that cannot be merged
653
            if (isset($args[$key]) && !\is_array($args[$key])) {
278✔
654
                continue;
278✔
655
            }
656

657
            if (\is_array($value)) {
278✔
658
                $value = $this->mergeFilterArgs($args[$key] ?? [], $value);
278✔
659
                if (!isset($value['#name'])) {
278✔
660
                    $name = (false === $pos = strrpos($original, '[')) ? $original : substr($original, 0, (int) $pos);
278✔
661
                    $value['#name'] = ($operation ? $operation->getShortName() : '').'Filter_'.strtr($name, ['[' => '_', ']' => '', '.' => '__']);
278✔
662
                }
663
            }
664

665
            $args[$key] = $value;
278✔
666
        }
667

668
        return $args;
278✔
669
    }
670

671
    private function convertFilterArgsToTypes(array $args): array
672
    {
UNCOV
673
        foreach ($args as $key => $value) {
304✔
UNCOV
674
            if (strpos($key, '.')) {
290✔
675
                // Declare relations/nested fields in a GraphQL compatible syntax.
676
                $args[str_replace('.', $this->nestingSeparator, $key)] = $value;
×
677
                unset($args[$key]);
×
678
            }
679
        }
680

UNCOV
681
        foreach ($args as $key => $value) {
304✔
UNCOV
682
            if (!\is_array($value) || !isset($value['#name'])) {
290✔
UNCOV
683
                continue;
290✔
684
            }
685

686
            $name = $value['#name'];
278✔
687

688
            if ($this->typesContainer->has($name)) {
278✔
689
                $args[$key] = $this->typesContainer->get($name);
18✔
690
                continue;
18✔
691
            }
692

693
            unset($value['#name']);
278✔
694

695
            $filterArgType = GraphQLType::listOf(new InputObjectType([
278✔
696
                'name' => $name,
278✔
697
                'fields' => $this->convertFilterArgsToTypes($value),
278✔
698
            ]));
278✔
699

700
            $this->typesContainer->set($name, $filterArgType);
278✔
701

702
            $args[$key] = $filterArgType;
278✔
703
        }
704

UNCOV
705
        return $args;
304✔
706
    }
707

708
    /**
709
     * Converts a built-in type to its GraphQL equivalent.
710
     *
711
     * @throws InvalidTypeException
712
     */
713
    private function convertType(Type|LegacyType $type, bool $input, Operation $resourceOperation, Operation $rootOperation, string $resourceClass, string $rootResource, ?string $property, int $depth, bool $forceNullable = false): GraphQLType|ListOfType|NonNull
714
    {
UNCOV
715
        if ($type instanceof LegacyType) {
304✔
716
            $type = PropertyInfoToTypeInfoHelper::convertLegacyTypesToType([$type]);
×
717
        }
718

UNCOV
719
        $graphqlType = $this->typeConverter->convertPhpType($type, $input, $rootOperation, $resourceClass, $rootResource, $property, $depth);
304✔
720

UNCOV
721
        if (null === $graphqlType) {
304✔
UNCOV
722
            throw new InvalidTypeException(\sprintf('The type "%s" is not supported.', (string) $type));
58✔
723
        }
724

UNCOV
725
        if (\is_string($graphqlType)) {
304✔
726
            if (!$this->typesContainer->has($graphqlType)) {
180✔
727
                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));
×
728
            }
729

730
            $graphqlType = $this->typesContainer->get($graphqlType);
180✔
731
        }
732

UNCOV
733
        if ($type->isSatisfiedBy(fn ($t) => $t instanceof CollectionType) && ($collectionValueType = TypeHelper::getCollectionValueType($type)) && TypeHelper::getClassName($collectionValueType)) {
304✔
UNCOV
734
            if (!$input && !$this->isEnumClass($resourceClass) && $this->pagination->isGraphQlEnabled($resourceOperation)) {
304✔
UNCOV
735
                return $this->typeBuilder->getPaginatedCollectionType($graphqlType, $resourceOperation);
290✔
736
            }
737

UNCOV
738
            return GraphQLType::listOf($graphqlType);
296✔
739
        }
740

UNCOV
741
        return $forceNullable || !$graphqlType instanceof NullableType || $type->isNullable() || ($rootOperation instanceof Mutation && 'update' === $rootOperation->getName())
304✔
UNCOV
742
            ? $graphqlType
304✔
UNCOV
743
            : GraphQLType::nonNull($graphqlType);
304✔
744
    }
745

746
    private function normalizePropertyName(string $property, string $resourceClass): string
747
    {
UNCOV
748
        if (null === $this->nameConverter) {
286✔
749
            return $property;
×
750
        }
751

UNCOV
752
        return $this->nameConverter->normalize($property, $resourceClass);
286✔
753
    }
754
}
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