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

api-platform / core / 15133993414

20 May 2025 09:30AM UTC coverage: 26.313% (-1.2%) from 27.493%
15133993414

Pull #7161

github

web-flow
Merge e2c03d45f into 5459ba375
Pull Request #7161: fix(metadata): infer parameter string type from schema

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

11019 existing lines in 363 files now uncovered.

12898 of 49018 relevant lines covered (26.31%)

34.33 hits per line

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

94.01
/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\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\PropertyInfo\Type;
40
use Symfony\Component\Serializer\NameConverter\AdvancedNameConverterInterface;
41
use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter;
42
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
43

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

53
    public function __construct(private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly ResourceClassResolverInterface $resourceClassResolver, private readonly TypesContainerInterface $typesContainer, ContextAwareTypeBuilderInterface $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())
54
    {
UNCOV
55
        $this->typeBuilder = $typeBuilder;
148✔
56
    }
57

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

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

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

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

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

91
        return [];
×
92
    }
93

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

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

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

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

113
        return [];
×
114
    }
115

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

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

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

UNCOV
131
        return $mutationFields;
138✔
132
    }
133

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

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

147
        if (!$fieldConfiguration) {
133✔
148
            return [];
×
149
        }
150

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

157
        $subscriptionFields[$subscriptionName.$operation->getShortName().'Subscribe'] = $fieldConfiguration;
133✔
158

159
        return $subscriptionFields;
133✔
160
    }
161

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

UNCOV
173
        if (null !== $ioMetadata && \array_key_exists('class', $ioMetadata) && null === $ioMetadata['class']) {
140✔
174
            if ($input) {
4✔
175
                return ['clientMutationId' => $clientMutationId];
3✔
176
            }
177

178
            return [];
3✔
179
        }
180

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

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

193
            if ($input) {
4✔
194
                $fields['clientMutationId'] = $clientMutationId;
4✔
195
            }
196

197
            return $fields;
4✔
198
        }
199

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

UNCOV
207
        ++$depth; // increment the depth for the call to getResourceFieldConfiguration.
139✔
208

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

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

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

UNCOV
237
        if ($operation instanceof Mutation && $input) {
139✔
UNCOV
238
            $fields['clientMutationId'] = $clientMutationId;
37✔
239
        }
240

UNCOV
241
        return $fields;
139✔
242
    }
243

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

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

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

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

UNCOV
272
        return $enumCases;
5✔
273
    }
274

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

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

UNCOV
288
        return $args;
146✔
289
    }
290

291
    /**
292
     * Transform the result of a parse_str to a GraphQL object type.
293
     * We should consider merging getFilterArgs and this, `getFilterArgs` uses `convertType` whereas we assume that parameters have only scalar types.
294
     * Note that this method has a lower complexity then the `getFilterArgs` one.
295
     * TODO: Is there a use case with an argument being a complex type (eg: a Resource, Enum etc.)?
296
     *
297
     * @param array<array{name: string, required: bool|null, description: string|null, leafs: string|array, type: string}> $flattenFields
298
     */
299
    private function parameterToObjectType(array $flattenFields, string $name): InputObjectType
300
    {
UNCOV
301
        $fields = [];
134✔
UNCOV
302
        foreach ($flattenFields as $field) {
134✔
UNCOV
303
            $key = $field['name'];
134✔
UNCOV
304
            $type = $this->getParameterType(\in_array($field['type'], Type::$builtinTypes, true) ? new Type($field['type'], !$field['required']) : new Type('object', !$field['required'], $field['type']));
134✔
305

UNCOV
306
            if (\is_array($l = $field['leafs'])) {
134✔
UNCOV
307
                if (0 === key($l)) {
134✔
UNCOV
308
                    $key = $key;
134✔
UNCOV
309
                    $type = GraphQLType::listOf($type);
134✔
310
                } else {
UNCOV
311
                    $n = [];
134✔
UNCOV
312
                    foreach ($field['leafs'] as $l => $value) {
134✔
UNCOV
313
                        $n[] = ['required' => null, 'name' => $l, 'leafs' => $value, 'type' => 'string', 'description' => null];
134✔
314
                    }
315

UNCOV
316
                    $type = $this->parameterToObjectType($n, $key);
134✔
UNCOV
317
                    if (isset($fields[$key]) && ($t = $fields[$key]['type']) instanceof InputObjectType) {
134✔
UNCOV
318
                        $t = $fields[$key]['type'];
134✔
UNCOV
319
                        $t->config['fields'] = array_merge($t->config['fields'], $type->config['fields']);
134✔
UNCOV
320
                        $type = $t;
134✔
321
                    }
322
                }
323
            }
324

UNCOV
325
            if ($field['required']) {
134✔
326
                $type = GraphQLType::nonNull($type);
×
327
            }
328

UNCOV
329
            if (isset($fields[$key])) {
134✔
UNCOV
330
                if ($type instanceof ListOfType) {
134✔
UNCOV
331
                    $key .= '_list';
134✔
332
                }
333
            }
334

UNCOV
335
            $fields[$key] = ['type' => $type, 'name' => $key];
134✔
336
        }
337

UNCOV
338
        return new InputObjectType(['name' => $name, 'fields' => $fields]);
134✔
339
    }
340

341
    /**
342
     * A simplified version of convert type that does not support resources.
343
     */
344
    private function getParameterType(Type $type): GraphQLType
345
    {
UNCOV
346
        return match ($type->getBuiltinType()) {
134✔
347
            Type::BUILTIN_TYPE_BOOL => GraphQLType::boolean(),
133✔
348
            Type::BUILTIN_TYPE_INT => GraphQLType::int(),
133✔
349
            Type::BUILTIN_TYPE_FLOAT => GraphQLType::float(),
133✔
UNCOV
350
            Type::BUILTIN_TYPE_STRING => GraphQLType::string(),
134✔
351
            Type::BUILTIN_TYPE_ARRAY => GraphQLType::listOf($this->getParameterType($type->getCollectionValueTypes()[0])),
133✔
352
            Type::BUILTIN_TYPE_ITERABLE => GraphQLType::listOf($this->getParameterType($type->getCollectionValueTypes()[0])),
133✔
UNCOV
353
            Type::BUILTIN_TYPE_OBJECT => GraphQLType::string(),
134✔
UNCOV
354
            default => GraphQLType::string(),
134✔
UNCOV
355
        };
134✔
356
    }
357

358
    /**
359
     * Get the field configuration of a resource.
360
     *
361
     * @see http://webonyx.github.io/graphql-php/type-system/object-types/
362
     */
363
    private function getResourceFieldConfiguration(?string $property, ?string $fieldDescription, ?string $deprecationReason, Type $type, string $rootResource, bool $input, Operation $rootOperation, int $depth = 0, bool $forceNullable = false): ?array
364
    {
365
        try {
UNCOV
366
            $isCollectionType = $this->typeBuilder->isCollection($type);
146✔
367

368
            if (
UNCOV
369
                $isCollectionType
146✔
UNCOV
370
                && $collectionValueType = $type->getCollectionValueTypes()[0] ?? null
146✔
371
            ) {
UNCOV
372
                $resourceClass = $collectionValueType->getClassName();
146✔
373
            } else {
UNCOV
374
                $resourceClass = $type->getClassName();
146✔
375
            }
376

UNCOV
377
            $resourceOperation = $rootOperation;
146✔
UNCOV
378
            if ($resourceClass && $depth >= 1 && $this->resourceClassResolver->isResourceClass($resourceClass)) {
146✔
UNCOV
379
                $resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass);
91✔
UNCOV
380
                $resourceOperation = $resourceMetadataCollection->getOperation($isCollectionType ? 'collection_query' : 'item_query');
91✔
381
            }
382

UNCOV
383
            if (!$resourceOperation instanceof Operation) {
146✔
384
                throw new \LogicException('The resource operation should be a GraphQL operation.');
×
385
            }
386

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

UNCOV
389
            $graphqlWrappedType = $graphqlType;
146✔
UNCOV
390
            if ($graphqlType instanceof WrappingType) {
146✔
UNCOV
391
                if (method_exists($graphqlType, 'getInnermostType')) {
146✔
UNCOV
392
                    $graphqlWrappedType = $graphqlType->getInnermostType();
146✔
393
                } else {
394
                    $graphqlWrappedType = $graphqlType->getWrappedType(true);
×
395
                }
396
            }
UNCOV
397
            $isStandardGraphqlType = \in_array($graphqlWrappedType, GraphQLType::getStandardTypes(), true);
146✔
UNCOV
398
            if ($isStandardGraphqlType) {
146✔
UNCOV
399
                $resourceClass = '';
139✔
400
            }
401

402
            // Check mercure attribute if it's a subscription at the root level.
UNCOV
403
            if ($rootOperation instanceof Subscription && null === $property && !$rootOperation->getMercure()) {
146✔
404
                return null;
×
405
            }
406

UNCOV
407
            $args = [];
146✔
408

UNCOV
409
            if (!$input && !$rootOperation instanceof Mutation && !$rootOperation instanceof Subscription && !$isStandardGraphqlType) {
146✔
UNCOV
410
                if ($isCollectionType) {
146✔
UNCOV
411
                    if (!$this->isEnumClass($resourceClass) && $this->pagination->isGraphQlEnabled($resourceOperation)) {
146✔
UNCOV
412
                        $args = $this->getGraphQlPaginationArgs($resourceOperation);
139✔
413
                    }
414

UNCOV
415
                    $args = $this->getFilterArgs($args, $resourceClass, $rootResource, $resourceOperation, $rootOperation, $property, $depth);
146✔
416

417
                    // Also register parameter args in the types container
418
                    // Note: This is a workaround, for more information read the comment on the parameterToObjectType function.
UNCOV
419
                    foreach ($this->getParameterArgs($rootOperation) as $key => $arg) {
146✔
UNCOV
420
                        if ($arg instanceof InputObjectType || (\is_array($arg) && isset($arg['name']))) {
134✔
UNCOV
421
                            $this->typesContainer->set(\is_array($arg) ? $arg['name'] : $arg->name(), $arg);
134✔
422
                        }
UNCOV
423
                        $args[$key] = $arg;
134✔
424
                    }
425
                }
426
            }
427

UNCOV
428
            if ($isStandardGraphqlType || $input) {
146✔
UNCOV
429
                $resolve = null;
146✔
430
            } else {
UNCOV
431
                $resolve = ($this->resolverFactory)($resourceClass, $rootResource, $resourceOperation, $this->propertyMetadataFactory);
146✔
432
            }
433

UNCOV
434
            return [
146✔
UNCOV
435
                'type' => $graphqlType,
146✔
UNCOV
436
                'description' => $fieldDescription,
146✔
UNCOV
437
                'args' => $args,
146✔
UNCOV
438
                'resolve' => $resolve,
146✔
UNCOV
439
                'deprecationReason' => $deprecationReason,
146✔
UNCOV
440
            ];
146✔
UNCOV
441
        } catch (InvalidTypeException) {
31✔
442
            // just ignore invalid types
443
        }
444

UNCOV
445
        return null;
31✔
446
    }
447

448
    /*
449
     * This function is @experimental, read the comment on the parameterToObjectType function for additional information.
450
     * @experimental
451
     */
452
    private function getParameterArgs(Operation $operation, array $args = []): array
453
    {
UNCOV
454
        foreach ($operation->getParameters() ?? [] as $parameter) {
146✔
UNCOV
455
            $key = $parameter->getKey();
134✔
456

UNCOV
457
            if (!str_contains($key, ':property')) {
134✔
UNCOV
458
                $args[$key] = ['type' => GraphQLType::string()];
134✔
459

UNCOV
460
                if ($parameter->getRequired()) {
134✔
461
                    $args[$key]['type'] = GraphQLType::nonNull($args[$key]['type']);
×
462
                }
463

UNCOV
464
                continue;
134✔
465
            }
466

UNCOV
467
            if (!($filterId = $parameter->getFilter()) || !$this->filterLocator->has($filterId)) {
134✔
468
                continue;
×
469
            }
470

UNCOV
471
            $filter = $this->filterLocator->get($filterId);
134✔
UNCOV
472
            $parsedKey = explode('[:property]', $key);
134✔
UNCOV
473
            $flattenFields = [];
134✔
474

UNCOV
475
            if ($filter instanceof FilterInterface) {
134✔
UNCOV
476
                foreach ($filter->getDescription($operation->getClass()) as $name => $value) {
134✔
UNCOV
477
                    $values = [];
134✔
UNCOV
478
                    parse_str($name, $values);
134✔
UNCOV
479
                    if (isset($values[$parsedKey[0]])) {
134✔
UNCOV
480
                        $values = $values[$parsedKey[0]];
134✔
481
                    }
482

UNCOV
483
                    $name = key($values);
134✔
UNCOV
484
                    $flattenFields[] = ['name' => $name, 'required' => $value['required'] ?? null, 'description' => $value['description'] ?? null, 'leafs' => $values[$name], 'type' => $value['type'] ?? 'string'];
134✔
485
                }
486

UNCOV
487
                $args[$parsedKey[0]] = $this->parameterToObjectType($flattenFields, $parsedKey[0]);
134✔
488
            }
489

UNCOV
490
            if ($filter instanceof OpenApiParameterFilterInterface) {
134✔
UNCOV
491
                foreach ($filter->getOpenApiParameters($parameter) as $value) {
134✔
UNCOV
492
                    $values = [];
134✔
UNCOV
493
                    parse_str($value->getName(), $values);
134✔
UNCOV
494
                    if (isset($values[$parsedKey[0]])) {
134✔
UNCOV
495
                        $values = $values[$parsedKey[0]];
134✔
496
                    }
497

UNCOV
498
                    $name = key($values);
134✔
UNCOV
499
                    $flattenFields[] = ['name' => $name, 'required' => $value->getRequired(), 'description' => $value->getDescription(), 'leafs' => $values[$name], 'type' => $value->getSchema()['type'] ?? 'string'];
134✔
500
                }
501

UNCOV
502
                $args[$parsedKey[0]] = $this->parameterToObjectType($flattenFields, $parsedKey[0].$operation->getShortName().$operation->getName());
134✔
503
            }
504
        }
505

UNCOV
506
        return $args;
146✔
507
    }
508

509
    private function getGraphQlPaginationArgs(Operation $queryOperation): array
510
    {
UNCOV
511
        $paginationType = $this->pagination->getGraphQlPaginationType($queryOperation);
139✔
512

UNCOV
513
        if ('cursor' === $paginationType) {
139✔
UNCOV
514
            return [
139✔
UNCOV
515
                'first' => [
139✔
UNCOV
516
                    'type' => GraphQLType::int(),
139✔
UNCOV
517
                    'description' => 'Returns the first n elements from the list.',
139✔
UNCOV
518
                ],
139✔
UNCOV
519
                'last' => [
139✔
UNCOV
520
                    'type' => GraphQLType::int(),
139✔
UNCOV
521
                    'description' => 'Returns the last n elements from the list.',
139✔
UNCOV
522
                ],
139✔
UNCOV
523
                'before' => [
139✔
UNCOV
524
                    'type' => GraphQLType::string(),
139✔
UNCOV
525
                    'description' => 'Returns the elements in the list that come before the specified cursor.',
139✔
UNCOV
526
                ],
139✔
UNCOV
527
                'after' => [
139✔
UNCOV
528
                    'type' => GraphQLType::string(),
139✔
UNCOV
529
                    'description' => 'Returns the elements in the list that come after the specified cursor.',
139✔
UNCOV
530
                ],
139✔
UNCOV
531
            ];
139✔
532
        }
533

534
        $paginationOptions = $this->pagination->getOptions();
133✔
535

536
        $args = [
133✔
537
            $paginationOptions['page_parameter_name'] => [
133✔
538
                'type' => GraphQLType::int(),
133✔
539
                'description' => 'Returns the current page.',
133✔
540
            ],
133✔
541
        ];
133✔
542

543
        if ($paginationOptions['client_items_per_page']) {
133✔
544
            $args[$paginationOptions['items_per_page_parameter_name']] = [
133✔
545
                'type' => GraphQLType::int(),
133✔
546
                'description' => 'Returns the number of items per page.',
133✔
547
            ];
133✔
548
        }
549

550
        return $args;
133✔
551
    }
552

553
    private function getFilterArgs(array $args, ?string $resourceClass, string $rootResource, Operation $resourceOperation, Operation $rootOperation, ?string $property, int $depth): array
554
    {
UNCOV
555
        if (null === $resourceClass) {
146✔
556
            return $args;
×
557
        }
558

UNCOV
559
        foreach ($resourceOperation->getFilters() ?? [] as $filterId) {
146✔
560
            if (!$this->filterLocator->has($filterId)) {
133✔
561
                continue;
×
562
            }
563

564
            foreach ($this->filterLocator->get($filterId)->getDescription($resourceClass) as $key => $description) {
133✔
565
                $nullable = isset($description['required']) ? !$description['required'] : true;
133✔
566
                $filterType = \in_array($description['type'], Type::$builtinTypes, true) ? new Type($description['type'], $nullable) : new Type('object', $nullable, $description['type']);
133✔
567
                $graphqlFilterType = $this->convertType($filterType, false, $resourceOperation, $rootOperation, $resourceClass, $rootResource, $property, $depth);
133✔
568

569
                if (str_ends_with($key, '[]')) {
133✔
570
                    $graphqlFilterType = GraphQLType::listOf($graphqlFilterType);
133✔
571
                    $key = substr($key, 0, -2).'_list';
133✔
572
                }
573

574
                /** @var string $key */
575
                $key = str_replace('.', $this->nestingSeparator, $key);
133✔
576

577
                parse_str($key, $parsed);
133✔
578
                if (\array_key_exists($key, $parsed) && \is_array($parsed[$key])) {
133✔
579
                    $parsed = [$key => ''];
×
580
                }
581
                array_walk_recursive($parsed, static function (&$v) use ($graphqlFilterType): void {
133✔
582
                    $v = $graphqlFilterType;
133✔
583
                });
133✔
584
                $args = $this->mergeFilterArgs($args, $parsed, $resourceOperation, $key);
133✔
585
            }
586
        }
587

UNCOV
588
        return $this->convertFilterArgsToTypes($args);
146✔
589
    }
590

591
    private function mergeFilterArgs(array $args, array $parsed, ?Operation $operation = null, string $original = ''): array
592
    {
593
        foreach ($parsed as $key => $value) {
133✔
594
            // Never override keys that cannot be merged
595
            if (isset($args[$key]) && !\is_array($args[$key])) {
133✔
596
                continue;
133✔
597
            }
598

599
            if (\is_array($value)) {
133✔
600
                $value = $this->mergeFilterArgs($args[$key] ?? [], $value);
133✔
601
                if (!isset($value['#name'])) {
133✔
602
                    $name = (false === $pos = strrpos($original, '[')) ? $original : substr($original, 0, (int) $pos);
133✔
603
                    $value['#name'] = ($operation ? $operation->getShortName() : '').'Filter_'.strtr($name, ['[' => '_', ']' => '', '.' => '__']);
133✔
604
                }
605
            }
606

607
            $args[$key] = $value;
133✔
608
        }
609

610
        return $args;
133✔
611
    }
612

613
    private function convertFilterArgsToTypes(array $args): array
614
    {
UNCOV
615
        foreach ($args as $key => $value) {
146✔
UNCOV
616
            if (strpos($key, '.')) {
139✔
617
                // Declare relations/nested fields in a GraphQL compatible syntax.
618
                $args[str_replace('.', $this->nestingSeparator, $key)] = $value;
×
619
                unset($args[$key]);
×
620
            }
621
        }
622

UNCOV
623
        foreach ($args as $key => $value) {
146✔
UNCOV
624
            if (!\is_array($value) || !isset($value['#name'])) {
139✔
UNCOV
625
                continue;
139✔
626
            }
627

628
            $name = $value['#name'];
133✔
629

630
            if ($this->typesContainer->has($name)) {
133✔
631
                $args[$key] = $this->typesContainer->get($name);
8✔
632
                continue;
8✔
633
            }
634

635
            unset($value['#name']);
133✔
636

637
            $filterArgType = GraphQLType::listOf(new InputObjectType([
133✔
638
                'name' => $name,
133✔
639
                'fields' => $this->convertFilterArgsToTypes($value),
133✔
640
            ]));
133✔
641

642
            $this->typesContainer->set($name, $filterArgType);
133✔
643

644
            $args[$key] = $filterArgType;
133✔
645
        }
646

UNCOV
647
        return $args;
146✔
648
    }
649

650
    /**
651
     * Converts a built-in type to its GraphQL equivalent.
652
     *
653
     * @throws InvalidTypeException
654
     */
655
    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
656
    {
UNCOV
657
        $graphqlType = $this->typeConverter->convertType($type, $input, $rootOperation, $resourceClass, $rootResource, $property, $depth);
146✔
658

UNCOV
659
        if (null === $graphqlType) {
146✔
UNCOV
660
            throw new InvalidTypeException(\sprintf('The type "%s" is not supported.', $type->getBuiltinType()));
31✔
661
        }
662

UNCOV
663
        if (\is_string($graphqlType)) {
146✔
664
            if (!$this->typesContainer->has($graphqlType)) {
133✔
665
                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));
×
666
            }
667

668
            $graphqlType = $this->typesContainer->get($graphqlType);
133✔
669
        }
670

UNCOV
671
        if ($this->typeBuilder->isCollection($type)) {
146✔
UNCOV
672
            if (!$input && !$this->isEnumClass($resourceClass) && $this->pagination->isGraphQlEnabled($resourceOperation)) {
146✔
UNCOV
673
                return $this->typeBuilder->getPaginatedCollectionType($graphqlType, $resourceOperation);
139✔
674
            }
675

UNCOV
676
            return GraphQLType::listOf($graphqlType);
142✔
677
        }
678

UNCOV
679
        return $forceNullable || !$graphqlType instanceof NullableType || $type->isNullable() || ($rootOperation instanceof Mutation && 'update' === $rootOperation->getName())
146✔
UNCOV
680
            ? $graphqlType
146✔
UNCOV
681
            : GraphQLType::nonNull($graphqlType);
146✔
682
    }
683

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

693
        return $this->nameConverter->normalize($property);
×
694
    }
695
}
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

© 2025 Coveralls, Inc