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

api-platform / core / 19171073905

07 Nov 2025 02:17PM UTC coverage: 0.0% (-24.3%) from 24.303%
19171073905

push

github

web-flow
feat(symfony): allow symfony makers namespace configuration (#7497)

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

14716 existing lines in 467 files now uncovered.

0 of 56762 relevant lines covered (0.0%)

0.0 hits per line

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

0.0
/src/GraphQl/Type/FieldsBuilder.php
1
<?php
2

3
/*
4
 * This file is part of the API Platform project.
5
 *
6
 * (c) Kévin Dunglas <dunglas@gmail.com>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11

12
declare(strict_types=1);
13

14
namespace ApiPlatform\GraphQl\Type;
15

16
use ApiPlatform\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\OpenApiParameterFilterInterface;
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\Metadata\Util\PropertyInfoToTypeInfoHelper;
32
use ApiPlatform\Metadata\Util\TypeHelper;
33
use ApiPlatform\State\Pagination\Pagination;
34
use ApiPlatform\State\Util\StateOptionsTrait;
35
use GraphQL\Type\Definition\InputObjectType;
36
use GraphQL\Type\Definition\ListOfType;
37
use GraphQL\Type\Definition\NonNull;
38
use GraphQL\Type\Definition\NullableType;
39
use GraphQL\Type\Definition\Type as GraphQLType;
40
use GraphQL\Type\Definition\WrappingType;
41
use Psr\Container\ContainerInterface;
42
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
43
use Symfony\Component\PropertyInfo\Type as LegacyType;
44
use Symfony\Component\Serializer\NameConverter\AdvancedNameConverterInterface;
45
use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter;
46
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
47
use Symfony\Component\TypeInfo\Type;
48
use Symfony\Component\TypeInfo\Type\CollectionType;
49
use Symfony\Component\TypeInfo\Type\ObjectType;
50
use Symfony\Component\TypeInfo\TypeIdentifier;
51

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

61
    private readonly ContextAwareTypeBuilderInterface $typeBuilder;
62

63
    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())
64
    {
UNCOV
65
        $this->typeBuilder = $typeBuilder;
×
66
    }
67

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

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

UNCOV
91
        $fieldName = lcfirst('item_query' === $operation->getName() ? ($operation->getShortName() ?? $operation->getName()) : $operation->getName().$operation->getShortName());
×
92

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

98
            return [$fieldName => array_merge($fieldConfiguration, $configuration)];
×
99
        }
100

UNCOV
101
        return [];
×
102
    }
103

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

UNCOV
113
        $fieldName = lcfirst('collection_query' === $operation->getName() ? $operation->getShortName() : $operation->getName().$operation->getShortName());
×
114

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

120
            return [$this->inflector->pluralize($fieldName) => array_merge($fieldConfiguration, $configuration)];
×
121
        }
122

UNCOV
123
        return [];
×
124
    }
125

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

UNCOV
135
        if ($fieldConfiguration = $this->getResourceFieldConfiguration(null, $description, $operation->getDeprecationReason(), $resourceType, $resourceClass, false, $operation)) {
×
UNCOV
136
            $fieldConfiguration['args'] += ['input' => $this->getResourceFieldConfiguration(null, null, $operation->getDeprecationReason(), $resourceType, $resourceClass, true, $operation)];
×
137
        }
138

UNCOV
139
        $mutationFields[$operation->getName().$operation->getShortName()] = $fieldConfiguration ?? [];
×
140

UNCOV
141
        return $mutationFields;
×
142
    }
143

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

UNCOV
153
        if ($fieldConfiguration = $this->getResourceFieldConfiguration(null, $description, $operation->getDeprecationReason(), $resourceType, $resourceClass, false, $operation)) {
×
154
            $fieldConfiguration['args'] += ['input' => $this->getResourceFieldConfiguration(null, null, $operation->getDeprecationReason(), $resourceType, $resourceClass, true, $operation)];
×
155
        }
156

UNCOV
157
        if (!$fieldConfiguration) {
×
158
            return [];
×
159
        }
160

161
        $subscriptionName = $operation->getName();
×
162
        // TODO: 3.0 change this
UNCOV
163
        if ('update_subscription' === $subscriptionName) {
×
164
            $subscriptionName = 'update';
×
165
        }
166

UNCOV
167
        $subscriptionFields[$subscriptionName.$operation->getShortName().'Subscribe'] = $fieldConfiguration;
×
168

UNCOV
169
        return $subscriptionFields;
×
170
    }
171

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

UNCOV
183
        if (null !== $ioMetadata && \array_key_exists('class', $ioMetadata) && null === $ioMetadata['class']) {
×
UNCOV
184
            if ($input) {
×
185
                return ['clientMutationId' => $clientMutationId];
×
186
            }
187

UNCOV
188
            return [];
×
189
        }
190

191
        if ($operation instanceof Subscription && $input) {
×
192
            return [
×
UNCOV
193
                'id' => $idField,
×
UNCOV
194
                'clientSubscriptionId' => $clientSubscriptionId,
×
UNCOV
195
            ];
×
196
        }
197

198
        if ('delete' === $operation->getName()) {
×
UNCOV
199
            $fields = [
×
200
                'id' => $idField,
×
201
            ];
×
202

UNCOV
203
            if ($input) {
×
204
                $fields['clientMutationId'] = $clientMutationId;
×
205
            }
206

UNCOV
207
            return $fields;
×
208
        }
209

UNCOV
210
        if (!$input || (!$operation->getResolver() && 'create' !== $operation->getName())) {
×
211
            $fields['id'] = $idField;
×
212
        }
UNCOV
213
        if ($input && $depth >= 1) {
×
UNCOV
214
            $fields['id'] = $optionalIdField;
×
215
        }
216

UNCOV
217
        ++$depth; // increment the depth for the call to getResourceFieldConfiguration.
×
218

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

UNCOV
227
                if (!method_exists(PropertyInfoExtractor::class, 'getType')) {
×
228
                    $propertyTypes = $propertyMetadata->getBuiltinTypes();
×
229

230
                    if (
UNCOV
231
                        !$propertyTypes
×
232
                        || (!$input && false === $propertyMetadata->isReadable())
×
UNCOV
233
                        || ($input && false === $propertyMetadata->isWritable())
×
234
                    ) {
UNCOV
235
                        continue;
×
236
                    }
237

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

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

UNCOV
262
        if ($operation instanceof Mutation && $input) {
×
UNCOV
263
            $fields['clientMutationId'] = $clientMutationId;
×
264
        }
265

UNCOV
266
        return $fields;
×
267
    }
268

269
    private function isEnumClass(string $resourceClass): bool
270
    {
UNCOV
271
        return is_a($resourceClass, \BackedEnum::class, true);
×
272
    }
273

274
    /**
275
     * {@inheritdoc}
276
     */
277
    public function getEnumFields(string $enumClass): array
278
    {
UNCOV
279
        $rEnum = new \ReflectionEnum($enumClass);
×
280

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

UNCOV
290
            $propertyMetadata = $this->propertyMetadataFactory->create($enumClass, $rCase->getName());
×
UNCOV
291
            if ($enumCaseDescription = $propertyMetadata->getDescription()) {
×
UNCOV
292
                $enumCase['description'] = $enumCaseDescription;
×
293
            }
UNCOV
294
            $enumCases[$rCase->getName()] = $enumCase;
×
295
        }
296

UNCOV
297
        return $enumCases;
×
298
    }
299

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

UNCOV
310
            $args[$id]['type'] = $this->typeConverter->resolveType($arg['type']);
×
311
        }
312

UNCOV
313
        return $args;
×
314
    }
315

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

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

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

UNCOV
354
            if ($field['required']) {
×
UNCOV
355
                $type = GraphQLType::nonNull($type);
×
356
            }
357

UNCOV
358
            if (isset($fields[$key])) {
×
UNCOV
359
                if ($type instanceof ListOfType) {
×
UNCOV
360
                    $key .= '_list';
×
361
                }
362
            }
363

UNCOV
364
            $fields[$key] = ['type' => $type, 'name' => $key];
×
365
        }
366

UNCOV
367
        return new InputObjectType(['name' => $name, 'fields' => $fields]);
×
368
    }
369

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

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

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

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

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

UNCOV
395
        return GraphQLType::string();
×
396
    }
397

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

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

UNCOV
412
            $valueType = $type;
×
UNCOV
413
            if ($isCollectionType) {
×
UNCOV
414
                $valueType = TypeHelper::getCollectionValueType($type);
×
415
            }
416

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

UNCOV
423
            $isResourceClass = $valueType->isSatisfiedBy($typeIsResourceClass);
×
424

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

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

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

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

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

UNCOV
455
            $args = [];
×
456

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

UNCOV
463
                    $args = $this->getFilterArgs($args, $resourceClass, $rootResource, $resourceOperation, $rootOperation, $property, $depth);
×
464

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

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

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

UNCOV
493
        return null;
×
494
    }
495

496
    /*
497
     * This function is @experimental, read the comment on the parameterToObjectType function for additional information.
498
     * @experimental
499
     */
500
    private function getParameterArgs(Operation $operation, array $args = []): array
501
    {
UNCOV
502
        foreach ($operation->getParameters() ?? [] as $parameter) {
×
UNCOV
503
            $key = $parameter->getKey();
×
504

UNCOV
505
            if (!str_contains($key, ':property')) {
×
506
                $args[$key] = ['type' => GraphQLType::string()];
×
507

UNCOV
508
                if ($parameter->getRequired()) {
×
UNCOV
509
                    $args[$key]['type'] = GraphQLType::nonNull($args[$key]['type']);
×
510
                }
511

UNCOV
512
                continue;
×
513
            }
514

UNCOV
515
            if (!($filterId = $parameter->getFilter()) || !$this->filterLocator->has($filterId)) {
×
UNCOV
516
                continue;
×
517
            }
518

UNCOV
519
            $filter = $this->filterLocator->get($filterId);
×
UNCOV
520
            $parsedKey = explode('[:property]', $key);
×
UNCOV
521
            $flattenFields = [];
×
522

UNCOV
523
            if ($filter instanceof FilterInterface) {
×
UNCOV
524
                foreach ($filter->getDescription($operation->getClass()) as $name => $value) {
×
UNCOV
525
                    $values = [];
×
UNCOV
526
                    parse_str($name, $values);
×
UNCOV
527
                    if (isset($values[$parsedKey[0]])) {
×
UNCOV
528
                        $values = $values[$parsedKey[0]];
×
529
                    }
530

UNCOV
531
                    $name = key($values);
×
UNCOV
532
                    $flattenFields[] = ['name' => $name, 'required' => $value['required'] ?? null, 'description' => $value['description'] ?? null, 'leafs' => $values[$name], 'type' => $value['type'] ?? 'string'];
×
533
                }
534

UNCOV
535
                $args[$parsedKey[0]] = $this->parameterToObjectType($flattenFields, $parsedKey[0]);
×
536
            }
537

UNCOV
538
            if ($filter instanceof OpenApiParameterFilterInterface) {
×
UNCOV
539
                foreach ($filter->getOpenApiParameters($parameter) as $value) {
×
UNCOV
540
                    $values = [];
×
UNCOV
541
                    parse_str($value->getName(), $values);
×
UNCOV
542
                    if (isset($values[$parsedKey[0]])) {
×
UNCOV
543
                        $values = $values[$parsedKey[0]];
×
544
                    }
545

UNCOV
546
                    $name = key($values);
×
UNCOV
547
                    $flattenFields[] = ['name' => $name, 'required' => $value->getRequired(), 'description' => $value->getDescription(), 'leafs' => $values[$name], 'type' => $value->getSchema()['type'] ?? 'string'];
×
548
                }
549

UNCOV
550
                $args[$parsedKey[0]] = $this->parameterToObjectType($flattenFields, $parsedKey[0].$operation->getShortName().$operation->getName());
×
551
            }
552
        }
553

UNCOV
554
        return $args;
×
555
    }
556

557
    private function getGraphQlPaginationArgs(Operation $queryOperation): array
558
    {
UNCOV
559
        $paginationType = $this->pagination->getGraphQlPaginationType($queryOperation);
×
560

UNCOV
561
        if ('cursor' === $paginationType) {
×
UNCOV
562
            return [
×
UNCOV
563
                'first' => [
×
UNCOV
564
                    'type' => GraphQLType::int(),
×
UNCOV
565
                    'description' => 'Returns the first n elements from the list.',
×
UNCOV
566
                ],
×
UNCOV
567
                'last' => [
×
UNCOV
568
                    'type' => GraphQLType::int(),
×
UNCOV
569
                    'description' => 'Returns the last n elements from the list.',
×
UNCOV
570
                ],
×
UNCOV
571
                'before' => [
×
UNCOV
572
                    'type' => GraphQLType::string(),
×
UNCOV
573
                    'description' => 'Returns the elements in the list that come before the specified cursor.',
×
UNCOV
574
                ],
×
UNCOV
575
                'after' => [
×
UNCOV
576
                    'type' => GraphQLType::string(),
×
UNCOV
577
                    'description' => 'Returns the elements in the list that come after the specified cursor.',
×
UNCOV
578
                ],
×
579
            ];
×
580
        }
581

582
        $paginationOptions = $this->pagination->getOptions();
×
583

584
        $args = [
×
585
            $paginationOptions['page_parameter_name'] => [
×
586
                'type' => GraphQLType::int(),
×
UNCOV
587
                'description' => 'Returns the current page.',
×
588
            ],
×
589
        ];
×
590

591
        if ($paginationOptions['client_items_per_page']) {
×
592
            $args[$paginationOptions['items_per_page_parameter_name']] = [
×
UNCOV
593
                'type' => GraphQLType::int(),
×
UNCOV
594
                'description' => 'Returns the number of items per page.',
×
595
            ];
×
596
        }
597

UNCOV
598
        return $args;
×
599
    }
600

601
    private function getFilterArgs(array $args, ?string $resourceClass, string $rootResource, Operation $resourceOperation, Operation $rootOperation, ?string $property, int $depth): array
602
    {
UNCOV
603
        if (null === $resourceClass) {
×
UNCOV
604
            return $args;
×
605
        }
606

UNCOV
607
        foreach ($resourceOperation->getFilters() ?? [] as $filterId) {
×
UNCOV
608
            if (!$this->filterLocator->has($filterId)) {
×
609
                continue;
×
610
            }
611

612
            $entityClass = $this->getStateOptionsClass($resourceOperation, $resourceOperation->getClass());
×
UNCOV
613
            foreach ($this->filterLocator->get($filterId)->getDescription($entityClass) as $key => $description) {
×
614
                $filterType = \in_array($description['type'], TypeIdentifier::values(), true) ? Type::builtin($description['type']) : Type::object($description['type']);
×
UNCOV
615
                if (!($description['required'] ?? false)) {
×
616
                    $filterType = Type::nullable($filterType);
×
617
                }
618
                $graphqlFilterType = $this->convertType($filterType, false, $resourceOperation, $rootOperation, $resourceClass, $rootResource, $property, $depth);
×
619

UNCOV
620
                if (str_ends_with($key, '[]')) {
×
UNCOV
621
                    $graphqlFilterType = GraphQLType::listOf($graphqlFilterType);
×
622
                    $key = substr($key, 0, -2).'_list';
×
623
                }
624

625
                /** @var string $key */
626
                $key = str_replace('.', $this->nestingSeparator, $key);
×
627

628
                parse_str($key, $parsed);
×
629
                if (\array_key_exists($key, $parsed) && \is_array($parsed[$key])) {
×
630
                    $parsed = [$key => ''];
×
631
                }
UNCOV
632
                array_walk_recursive($parsed, static function (&$v) use ($graphqlFilterType): void {
×
UNCOV
633
                    $v = $graphqlFilterType;
×
UNCOV
634
                });
×
UNCOV
635
                $args = $this->mergeFilterArgs($args, $parsed, $resourceOperation, $key);
×
636
            }
637
        }
638

UNCOV
639
        return $this->convertFilterArgsToTypes($args);
×
640
    }
641

642
    private function mergeFilterArgs(array $args, array $parsed, ?Operation $operation = null, string $original = ''): array
643
    {
UNCOV
644
        foreach ($parsed as $key => $value) {
×
645
            // Never override keys that cannot be merged
646
            if (isset($args[$key]) && !\is_array($args[$key])) {
×
647
                continue;
×
648
            }
649

650
            if (\is_array($value)) {
×
UNCOV
651
                $value = $this->mergeFilterArgs($args[$key] ?? [], $value);
×
UNCOV
652
                if (!isset($value['#name'])) {
×
UNCOV
653
                    $name = (false === $pos = strrpos($original, '[')) ? $original : substr($original, 0, (int) $pos);
×
654
                    $value['#name'] = ($operation ? $operation->getShortName() : '').'Filter_'.strtr($name, ['[' => '_', ']' => '', '.' => '__']);
×
655
                }
656
            }
657

UNCOV
658
            $args[$key] = $value;
×
659
        }
660

UNCOV
661
        return $args;
×
662
    }
663

664
    private function convertFilterArgsToTypes(array $args): array
665
    {
666
        foreach ($args as $key => $value) {
×
UNCOV
667
            if (strpos($key, '.')) {
×
668
                // Declare relations/nested fields in a GraphQL compatible syntax.
UNCOV
669
                $args[str_replace('.', $this->nestingSeparator, $key)] = $value;
×
UNCOV
670
                unset($args[$key]);
×
671
            }
672
        }
673

UNCOV
674
        foreach ($args as $key => $value) {
×
675
            if (!\is_array($value) || !isset($value['#name'])) {
×
UNCOV
676
                continue;
×
677
            }
678

679
            $name = $value['#name'];
×
680

UNCOV
681
            if ($this->typesContainer->has($name)) {
×
682
                $args[$key] = $this->typesContainer->get($name);
×
UNCOV
683
                continue;
×
684
            }
685

686
            unset($value['#name']);
×
687

UNCOV
688
            $filterArgType = GraphQLType::listOf(new InputObjectType([
×
689
                'name' => $name,
×
UNCOV
690
                'fields' => $this->convertFilterArgsToTypes($value),
×
691
            ]));
×
692

UNCOV
693
            $this->typesContainer->set($name, $filterArgType);
×
694

UNCOV
695
            $args[$key] = $filterArgType;
×
696
        }
697

UNCOV
698
        return $args;
×
699
    }
700

701
    /**
702
     * Converts a built-in type to its GraphQL equivalent.
703
     *
704
     * @throws InvalidTypeException
705
     */
706
    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
707
    {
UNCOV
708
        if ($type instanceof LegacyType) {
×
UNCOV
709
            $type = PropertyInfoToTypeInfoHelper::convertLegacyTypesToType([$type]);
×
710
        }
711

UNCOV
712
        $graphqlType = $this->typeConverter->convertPhpType($type, $input, $rootOperation, $resourceClass, $rootResource, $property, $depth);
×
713

UNCOV
714
        if (null === $graphqlType) {
×
715
            throw new InvalidTypeException(\sprintf('The type "%s" is not supported.', (string) $type));
×
716
        }
717

UNCOV
718
        if (\is_string($graphqlType)) {
×
719
            if (!$this->typesContainer->has($graphqlType)) {
×
UNCOV
720
                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));
×
721
            }
722

UNCOV
723
            $graphqlType = $this->typesContainer->get($graphqlType);
×
724
        }
725

UNCOV
726
        if ($type->isSatisfiedBy(fn ($t) => $t instanceof CollectionType) && ($collectionValueType = TypeHelper::getCollectionValueType($type)) && TypeHelper::getClassName($collectionValueType)) {
×
UNCOV
727
            if (!$input && !$this->isEnumClass($resourceClass) && $this->pagination->isGraphQlEnabled($resourceOperation)) {
×
UNCOV
728
                return $this->typeBuilder->getPaginatedCollectionType($graphqlType, $resourceOperation);
×
729
            }
730

UNCOV
731
            return GraphQLType::listOf($graphqlType);
×
732
        }
733

UNCOV
734
        return $forceNullable || !$graphqlType instanceof NullableType || $type->isNullable() || ($rootOperation instanceof Mutation && 'update' === $rootOperation->getName())
×
UNCOV
735
            ? $graphqlType
×
UNCOV
736
            : GraphQLType::nonNull($graphqlType);
×
737
    }
738

739
    private function normalizePropertyName(string $property, string $resourceClass): string
740
    {
UNCOV
741
        if (null === $this->nameConverter) {
×
UNCOV
742
            return $property;
×
743
        }
744
        if ($this->nameConverter instanceof AdvancedNameConverterInterface || $this->nameConverter instanceof MetadataAwareNameConverter) {
×
UNCOV
745
            return $this->nameConverter->normalize($property, $resourceClass);
×
746
        }
747

UNCOV
748
        return $this->nameConverter->normalize($property);
×
749
    }
750
}
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