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

api-platform / core / 13925245599

18 Mar 2025 02:06PM UTC coverage: 61.074% (-0.9%) from 61.973%
13925245599

Pull #7031

github

web-flow
Merge 8990d8b26 into 7cb5a6db8
Pull Request #7031: fix: header parameter should be case insensitive

3 of 4 new or added lines in 1 file covered. (75.0%)

167 existing lines in 27 files now uncovered.

11314 of 18525 relevant lines covered (61.07%)

51.02 hits per line

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

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

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

12
declare(strict_types=1);
13

14
namespace ApiPlatform\GraphQl\Type;
15

16
use ApiPlatform\Doctrine\Odm\State\Options as ODMOptions;
17
use ApiPlatform\Doctrine\Orm\State\Options;
18
use ApiPlatform\GraphQl\Exception\InvalidTypeException;
19
use ApiPlatform\GraphQl\Resolver\Factory\ResolverFactory;
20
use ApiPlatform\GraphQl\Resolver\Factory\ResolverFactoryInterface;
21
use ApiPlatform\GraphQl\Type\Definition\TypeInterface;
22
use ApiPlatform\Metadata\FilterInterface;
23
use ApiPlatform\Metadata\GraphQl\Mutation;
24
use ApiPlatform\Metadata\GraphQl\Operation;
25
use ApiPlatform\Metadata\GraphQl\Query;
26
use ApiPlatform\Metadata\GraphQl\Subscription;
27
use ApiPlatform\Metadata\InflectorInterface;
28
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
29
use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
30
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
31
use ApiPlatform\Metadata\ResourceClassResolverInterface;
32
use ApiPlatform\Metadata\Util\Inflector;
33
use ApiPlatform\State\Pagination\Pagination;
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\Type;
42
use Symfony\Component\Serializer\NameConverter\AdvancedNameConverterInterface;
43
use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter;
44
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
45

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

55
    public function __construct(private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly ResourceClassResolverInterface $resourceClassResolver, private readonly TypesContainerInterface $typesContainer, ContextAwareTypeBuilderInterface|TypeBuilderEnumInterface|TypeBuilderInterface $typeBuilder, private readonly TypeConverterInterface $typeConverter, private readonly ResolverFactoryInterface $itemResolverFactory, private readonly ?ResolverFactoryInterface $collectionResolverFactory, private readonly ?ResolverFactoryInterface $itemMutationResolverFactory, private readonly ?ResolverFactoryInterface $itemSubscriptionResolverFactory, private readonly ContainerInterface $filterLocator, private readonly Pagination $pagination, private readonly ?NameConverterInterface $nameConverter, private readonly string $nestingSeparator, private readonly ?InflectorInterface $inflector = new Inflector())
56
    {
57
        if ($typeBuilder instanceof TypeBuilderInterface) {
27✔
58
            @trigger_error(\sprintf('$typeBuilder argument of FieldsBuilder implementing "%s" is deprecated since API Platform 3.1. It has to implement "%s" instead.', TypeBuilderInterface::class, TypeBuilderEnumInterface::class), \E_USER_DEPRECATED);
×
59
        }
60
        if ($typeBuilder instanceof TypeBuilderEnumInterface) {
27✔
61
            @trigger_error(\sprintf('$typeBuilder argument of TypeConverter implementing "%s" is deprecated since API Platform 3.3. It has to implement "%s" instead.', TypeBuilderEnumInterface::class, ContextAwareTypeBuilderInterface::class), \E_USER_DEPRECATED);
×
62
        }
63
        $this->typeBuilder = $typeBuilder;
27✔
64
    }
65

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

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

89
        $fieldName = lcfirst('item_query' === $operation->getName() ? $operation->getShortName() : $operation->getName().$operation->getShortName());
27✔
90

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

96
            return [$fieldName => array_merge($fieldConfiguration, $configuration)];
27✔
97
        }
98

99
        return [];
×
100
    }
101

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

111
        $fieldName = lcfirst('collection_query' === $operation->getName() ? $operation->getShortName() : $operation->getName().$operation->getShortName());
27✔
112

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

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

121
        return [];
×
122
    }
123

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

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

137
        $mutationFields[$operation->getName().$operation->getShortName()] = $fieldConfiguration ?? [];
27✔
138

139
        return $mutationFields;
27✔
140
    }
141

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

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

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

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

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

167
        return $subscriptionFields;
27✔
168
    }
169

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

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

186
            return [];
×
187
        }
188

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

196
        if ('delete' === $operation->getName()) {
27✔
197
            $fields = [
×
198
                'id' => $idField,
×
199
            ];
×
200

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

205
            return $fields;
×
206
        }
207

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

215
        ++$depth; // increment the depth for the call to getResourceFieldConfiguration.
27✔
216

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

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

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

245
        if ($operation instanceof Mutation && $input) {
27✔
246
            $fields['clientMutationId'] = $clientMutationId;
×
247
        }
248

249
        return $fields;
27✔
250
    }
251

252
    private function isEnumClass(string $resourceClass): bool
253
    {
254
        return is_a($resourceClass, \BackedEnum::class, true);
27✔
255
    }
256

257
    /**
258
     * {@inheritdoc}
259
     */
260
    public function getEnumFields(string $enumClass): array
261
    {
262
        $rEnum = new \ReflectionEnum($enumClass);
3✔
263

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

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

280
        return $enumCases;
3✔
281
    }
282

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

293
            $args[$id]['type'] = $this->typeConverter->resolveType($arg['type']);
27✔
294
        }
295

296
        return $args;
27✔
297
    }
298

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

314
            if (\is_array($l = $field['leafs'])) {
27✔
315
                if (0 === key($l)) {
27✔
316
                    $key = $key;
27✔
317
                    $type = GraphQLType::listOf($type);
27✔
318
                } else {
319
                    $n = [];
27✔
320
                    foreach ($field['leafs'] as $l => $value) {
27✔
321
                        $n[] = ['required' => null, 'name' => $l, 'leafs' => $value, 'type' => 'string', 'description' => null];
27✔
322
                    }
323

324
                    $type = $this->parameterToObjectType($n, $key);
27✔
325
                    if (isset($fields[$key]) && ($t = $fields[$key]['type']) instanceof InputObjectType) {
27✔
326
                        $t = $fields[$key]['type'];
27✔
327
                        $t->config['fields'] = array_merge($t->config['fields'], $type->config['fields']);
27✔
328
                        $type = $t;
27✔
329
                    }
330
                }
331
            }
332

333
            if ($field['required']) {
27✔
334
                $type = GraphQLType::nonNull($type);
×
335
            }
336

337
            if (isset($fields[$key])) {
27✔
338
                if ($type instanceof ListOfType) {
27✔
339
                    $key .= '_list';
27✔
340
                }
341
            }
342

343
            $fields[$key] = ['type' => $type, 'name' => $key];
27✔
344
        }
345

346
        return new InputObjectType(['name' => $name, 'fields' => $fields]);
27✔
347
    }
348

349
    /**
350
     * A simplified version of convert type that does not support resources.
351
     */
352
    private function getParameterType(Type $type): GraphQLType
353
    {
354
        return match ($type->getBuiltinType()) {
27✔
355
            Type::BUILTIN_TYPE_BOOL => GraphQLType::boolean(),
27✔
356
            Type::BUILTIN_TYPE_INT => GraphQLType::int(),
27✔
357
            Type::BUILTIN_TYPE_FLOAT => GraphQLType::float(),
27✔
358
            Type::BUILTIN_TYPE_STRING => GraphQLType::string(),
27✔
359
            Type::BUILTIN_TYPE_ARRAY => GraphQLType::listOf($this->getParameterType($type->getCollectionValueTypes()[0])),
27✔
360
            Type::BUILTIN_TYPE_ITERABLE => GraphQLType::listOf($this->getParameterType($type->getCollectionValueTypes()[0])),
27✔
361
            Type::BUILTIN_TYPE_OBJECT => GraphQLType::string(),
27✔
362
            default => GraphQLType::string(),
27✔
363
        };
27✔
364
    }
365

366
    /**
367
     * Get the field configuration of a resource.
368
     *
369
     * @see http://webonyx.github.io/graphql-php/type-system/object-types/
370
     */
371
    private function getResourceFieldConfiguration(?string $property, ?string $fieldDescription, ?string $deprecationReason, Type $type, string $rootResource, bool $input, Operation $rootOperation, int $depth = 0, bool $forceNullable = false): ?array
372
    {
373
        try {
374
            $isCollectionType = $this->typeBuilder->isCollection($type);
27✔
375

376
            if (
377
                $isCollectionType
27✔
378
                && $collectionValueType = $type->getCollectionValueTypes()[0] ?? null
27✔
379
            ) {
380
                $resourceClass = $collectionValueType->getClassName();
27✔
381
            } else {
382
                $resourceClass = $type->getClassName();
27✔
383
            }
384

385
            $resourceOperation = $rootOperation;
27✔
386
            if ($resourceClass && $depth >= 1 && $this->resourceClassResolver->isResourceClass($resourceClass)) {
27✔
387
                $resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass);
×
388
                $resourceOperation = $resourceMetadataCollection->getOperation($isCollectionType ? 'collection_query' : 'item_query');
×
389
            }
390

391
            if (!$resourceOperation instanceof Operation) {
27✔
392
                throw new \LogicException('The resource operation should be a GraphQL operation.');
×
393
            }
394

395
            $graphqlType = $this->convertType($type, $input, $resourceOperation, $rootOperation, $resourceClass ?? '', $rootResource, $property, $depth, $forceNullable);
27✔
396

397
            $graphqlWrappedType = $graphqlType;
27✔
398
            if ($graphqlType instanceof WrappingType) {
27✔
399
                if (method_exists($graphqlType, 'getInnermostType')) {
27✔
400
                    $graphqlWrappedType = $graphqlType->getInnermostType();
27✔
401
                } else {
402
                    $graphqlWrappedType = $graphqlType->getWrappedType(true);
×
403
                }
404
            }
405
            $isStandardGraphqlType = \in_array($graphqlWrappedType, GraphQLType::getStandardTypes(), true);
27✔
406
            if ($isStandardGraphqlType) {
27✔
407
                $resourceClass = '';
27✔
408
            }
409

410
            // Check mercure attribute if it's a subscription at the root level.
411
            if ($rootOperation instanceof Subscription && null === $property && !$rootOperation->getMercure()) {
27✔
412
                return null;
×
413
            }
414

415
            $args = [];
27✔
416

417
            if (!$input && !$rootOperation instanceof Mutation && !$rootOperation instanceof Subscription && !$isStandardGraphqlType) {
27✔
418
                if ($isCollectionType) {
27✔
419
                    if (!$this->isEnumClass($resourceClass) && $this->pagination->isGraphQlEnabled($resourceOperation)) {
27✔
420
                        $args = $this->getGraphQlPaginationArgs($resourceOperation);
27✔
421
                    }
422

423
                    $args = $this->getFilterArgs($args, $resourceClass, $rootResource, $resourceOperation, $rootOperation, $property, $depth);
27✔
424
                    $args = $this->getParameterArgs($rootOperation, $args);
27✔
425
                }
426
            }
427

428
            if ($this->itemResolverFactory instanceof ResolverFactory) {
27✔
429
                if ($isStandardGraphqlType || $input) {
27✔
430
                    $resolve = null;
27✔
431
                } else {
432
                    $resolve = ($this->itemResolverFactory)($resourceClass, $rootResource, $resourceOperation, $this->propertyMetadataFactory);
27✔
433
                }
434
            } else {
UNCOV
435
                if ($isStandardGraphqlType || $input) {
×
UNCOV
436
                    $resolve = null;
×
UNCOV
437
                } elseif (($rootOperation instanceof Mutation || $rootOperation instanceof Subscription) && $depth <= 0) {
×
UNCOV
438
                    $resolve = $rootOperation instanceof Mutation ? ($this->itemMutationResolverFactory)($resourceClass, $rootResource, $resourceOperation) : ($this->itemSubscriptionResolverFactory)($resourceClass, $rootResource, $resourceOperation);
×
UNCOV
439
                } elseif ($this->typeBuilder->isCollection($type)) {
×
UNCOV
440
                    $resolve = ($this->collectionResolverFactory)($resourceClass, $rootResource, $resourceOperation);
×
441
                } else {
UNCOV
442
                    $resolve = ($this->itemResolverFactory)($resourceClass, $rootResource, $resourceOperation);
×
443
                }
444
            }
445

446
            return [
27✔
447
                'type' => $graphqlType,
27✔
448
                'description' => $fieldDescription,
27✔
449
                'args' => $args,
27✔
450
                'resolve' => $resolve,
27✔
451
                'deprecationReason' => $deprecationReason,
27✔
452
            ];
27✔
453
        } catch (InvalidTypeException) {
3✔
454
            // just ignore invalid types
455
        }
456

457
        return null;
3✔
458
    }
459

460
    /*
461
     * This function is @experimental, read the comment on the parameterToObjectType function for additional information.
462
     * @experimental
463
     */
464
    private function getParameterArgs(Operation $operation, array $args = []): array
465
    {
466
        foreach ($operation->getParameters() ?? [] as $parameter) {
27✔
467
            $key = $parameter->getKey();
27✔
468

469
            if (!str_contains($key, ':property')) {
27✔
470
                $args[$key] = ['type' => GraphQLType::string()];
27✔
471

472
                if ($parameter->getRequired()) {
27✔
473
                    $args[$key]['type'] = GraphQLType::nonNull($args[$key]['type']);
×
474
                }
475

476
                continue;
27✔
477
            }
478

479
            if (!($filterId = $parameter->getFilter()) || !$this->filterLocator->has($filterId)) {
27✔
480
                continue;
×
481
            }
482

483
            $filter = $this->filterLocator->get($filterId);
27✔
484
            $parsedKey = explode('[:property]', $key);
27✔
485
            $flattenFields = [];
27✔
486

487
            if ($filter instanceof FilterInterface) {
27✔
488
                foreach ($filter->getDescription($operation->getClass()) as $name => $value) {
27✔
489
                    $values = [];
27✔
490
                    parse_str($name, $values);
27✔
491
                    if (isset($values[$parsedKey[0]])) {
27✔
492
                        $values = $values[$parsedKey[0]];
27✔
493
                    }
494

495
                    $name = key($values);
27✔
496
                    $flattenFields[] = ['name' => $name, 'required' => $value['required'] ?? null, 'description' => $value['description'] ?? null, 'leafs' => $values[$name], 'type' => $value['type'] ?? 'string'];
27✔
497
                }
498

499
                $args[$parsedKey[0]] = $this->parameterToObjectType($flattenFields, $parsedKey[0]);
27✔
500
            }
501
        }
502

503
        return $args;
27✔
504
    }
505

506
    private function getGraphQlPaginationArgs(Operation $queryOperation): array
507
    {
508
        $paginationType = $this->pagination->getGraphQlPaginationType($queryOperation);
27✔
509

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

531
        $paginationOptions = $this->pagination->getOptions();
27✔
532

533
        $args = [
27✔
534
            $paginationOptions['page_parameter_name'] => [
27✔
535
                'type' => GraphQLType::int(),
27✔
536
                'description' => 'Returns the current page.',
27✔
537
            ],
27✔
538
        ];
27✔
539

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

547
        return $args;
27✔
548
    }
549

550
    private function getFilterArgs(array $args, ?string $resourceClass, string $rootResource, Operation $resourceOperation, Operation $rootOperation, ?string $property, int $depth): array
551
    {
552
        if (null === $resourceClass) {
27✔
553
            return $args;
×
554
        }
555

556
        foreach ($resourceOperation->getFilters() ?? [] as $filterId) {
27✔
557
            if (!$this->filterLocator->has($filterId)) {
27✔
558
                continue;
×
559
            }
560

561
            $entityClass = $resourceClass;
27✔
562
            if ($options = $resourceOperation->getStateOptions()) {
27✔
563
                if (class_exists(Options::class) && $options instanceof Options && $options->getEntityClass()) {
27✔
564
                    $entityClass = $options->getEntityClass();
27✔
565
                }
566

567
                if (class_exists(ODMOptions::class) && $options instanceof ODMOptions && $options->getDocumentClass()) {
27✔
568
                    $entityClass = $options->getDocumentClass();
27✔
569
                }
570
            }
571

572
            foreach ($this->filterLocator->get($filterId)->getDescription($entityClass) as $key => $description) {
27✔
573
                $nullable = isset($description['required']) ? !$description['required'] : true;
27✔
574
                $filterType = \in_array($description['type'], Type::$builtinTypes, true) ? new Type($description['type'], $nullable) : new Type('object', $nullable, $description['type']);
27✔
575
                $graphqlFilterType = $this->convertType($filterType, false, $resourceOperation, $rootOperation, $resourceClass, $rootResource, $property, $depth);
27✔
576

577
                if (str_ends_with($key, '[]')) {
27✔
578
                    $graphqlFilterType = GraphQLType::listOf($graphqlFilterType);
27✔
579
                    $key = substr($key, 0, -2).'_list';
27✔
580
                }
581

582
                /** @var string $key */
583
                $key = str_replace('.', $this->nestingSeparator, $key);
27✔
584

585
                parse_str($key, $parsed);
27✔
586
                if (\array_key_exists($key, $parsed) && \is_array($parsed[$key])) {
27✔
587
                    $parsed = [$key => ''];
×
588
                }
589
                array_walk_recursive($parsed, static function (&$v) use ($graphqlFilterType): void {
27✔
590
                    $v = $graphqlFilterType;
27✔
591
                });
27✔
592
                $args = $this->mergeFilterArgs($args, $parsed, $resourceOperation, $key);
27✔
593
            }
594
        }
595

596
        return $this->convertFilterArgsToTypes($args);
27✔
597
    }
598

599
    private function mergeFilterArgs(array $args, array $parsed, ?Operation $operation = null, string $original = ''): array
600
    {
601
        foreach ($parsed as $key => $value) {
27✔
602
            // Never override keys that cannot be merged
603
            if (isset($args[$key]) && !\is_array($args[$key])) {
27✔
604
                continue;
27✔
605
            }
606

607
            if (\is_array($value)) {
27✔
608
                $value = $this->mergeFilterArgs($args[$key] ?? [], $value);
27✔
609
                if (!isset($value['#name'])) {
27✔
610
                    $name = (false === $pos = strrpos($original, '[')) ? $original : substr($original, 0, (int) $pos);
27✔
611
                    $value['#name'] = ($operation ? $operation->getShortName() : '').'Filter_'.strtr($name, ['[' => '_', ']' => '', '.' => '__']);
27✔
612
                }
613
            }
614

615
            $args[$key] = $value;
27✔
616
        }
617

618
        return $args;
27✔
619
    }
620

621
    private function convertFilterArgsToTypes(array $args): array
622
    {
623
        foreach ($args as $key => $value) {
27✔
624
            if (strpos($key, '.')) {
27✔
625
                // Declare relations/nested fields in a GraphQL compatible syntax.
626
                $args[str_replace('.', $this->nestingSeparator, $key)] = $value;
×
627
                unset($args[$key]);
×
628
            }
629
        }
630

631
        foreach ($args as $key => $value) {
27✔
632
            if (!\is_array($value) || !isset($value['#name'])) {
27✔
633
                continue;
27✔
634
            }
635

636
            $name = $value['#name'];
27✔
637

638
            if ($this->typesContainer->has($name)) {
27✔
639
                $args[$key] = $this->typesContainer->get($name);
×
640
                continue;
×
641
            }
642

643
            unset($value['#name']);
27✔
644

645
            $filterArgType = GraphQLType::listOf(new InputObjectType([
27✔
646
                'name' => $name,
27✔
647
                'fields' => $this->convertFilterArgsToTypes($value),
27✔
648
            ]));
27✔
649

650
            $this->typesContainer->set($name, $filterArgType);
27✔
651

652
            $args[$key] = $filterArgType;
27✔
653
        }
654

655
        return $args;
27✔
656
    }
657

658
    /**
659
     * Converts a built-in type to its GraphQL equivalent.
660
     *
661
     * @throws InvalidTypeException
662
     */
663
    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
664
    {
665
        $graphqlType = $this->typeConverter->convertType($type, $input, $rootOperation, $resourceClass, $rootResource, $property, $depth);
27✔
666

667
        if (null === $graphqlType) {
27✔
668
            throw new InvalidTypeException(\sprintf('The type "%s" is not supported.', $type->getBuiltinType()));
3✔
669
        }
670

671
        if (\is_string($graphqlType)) {
27✔
672
            if (!$this->typesContainer->has($graphqlType)) {
×
673
                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));
×
674
            }
675

676
            $graphqlType = $this->typesContainer->get($graphqlType);
×
677
        }
678

679
        if ($this->typeBuilder->isCollection($type)) {
27✔
680
            if (!$input && !$this->isEnumClass($resourceClass) && $this->pagination->isGraphQlEnabled($resourceOperation)) {
27✔
681
                // Deprecated path, to remove in API Platform 4.
682
                if ($this->typeBuilder instanceof TypeBuilderInterface) {
27✔
683
                    return $this->typeBuilder->getResourcePaginatedCollectionType($graphqlType, $resourceClass, $resourceOperation);
×
684
                }
685

686
                return $this->typeBuilder->getPaginatedCollectionType($graphqlType, $resourceOperation);
27✔
687
            }
688

689
            return GraphQLType::listOf($graphqlType);
27✔
690
        }
691

692
        return $forceNullable || !$graphqlType instanceof NullableType || $type->isNullable() || ($rootOperation instanceof Mutation && 'update' === $rootOperation->getName())
27✔
693
            ? $graphqlType
27✔
694
            : GraphQLType::nonNull($graphqlType);
27✔
695
    }
696

697
    private function normalizePropertyName(string $property, string $resourceClass): string
698
    {
699
        if (null === $this->nameConverter) {
27✔
700
            return $property;
×
701
        }
702
        if ($this->nameConverter instanceof AdvancedNameConverterInterface || $this->nameConverter instanceof MetadataAwareNameConverter) {
27✔
703
            return $this->nameConverter->normalize($property, $resourceClass);
27✔
704
        }
705

706
        return $this->nameConverter->normalize($property);
×
707
    }
708
}
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