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

api-platform / core / 19173909613

07 Nov 2025 04:03PM UTC coverage: 0.0% (-22.0%) from 22.042%
19173909613

Pull #7515

github

web-flow
Merge 63ab5112d into 3df65aa86
Pull Request #7515: fix(jsonld): read identifier with itemUriTemplate

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

11197 existing lines in 362 files now uncovered.

0 of 49873 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\State\Pagination\Pagination;
32
use ApiPlatform\State\Util\StateOptionsTrait;
33
use GraphQL\Type\Definition\InputObjectType;
34
use GraphQL\Type\Definition\ListOfType;
35
use GraphQL\Type\Definition\NonNull;
36
use GraphQL\Type\Definition\NullableType;
37
use GraphQL\Type\Definition\Type as GraphQLType;
38
use GraphQL\Type\Definition\WrappingType;
39
use Psr\Container\ContainerInterface;
40
use Symfony\Component\PropertyInfo\Type;
41
use Symfony\Component\Serializer\NameConverter\AdvancedNameConverterInterface;
42
use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter;
43
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
44

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

54
    private readonly ContextAwareTypeBuilderInterface $typeBuilder;
55

56
    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())
57
    {
UNCOV
58
        $this->typeBuilder = $typeBuilder;
×
59
    }
60

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

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

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

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

UNCOV
91
            return [$fieldName => array_merge($fieldConfiguration, $configuration)];
×
92
        }
93

94
        return [];
×
95
    }
96

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

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

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

UNCOV
113
            return [$this->inflector->pluralize($fieldName) => array_merge($fieldConfiguration, $configuration)];
×
114
        }
115

116
        return [];
×
117
    }
118

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

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

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

UNCOV
134
        return $mutationFields;
×
135
    }
136

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

146
        if ($fieldConfiguration = $this->getResourceFieldConfiguration(null, $description, $operation->getDeprecationReason(), $resourceType, $resourceClass, false, $operation)) {
×
147
            $fieldConfiguration['args'] += ['input' => $this->getResourceFieldConfiguration(null, null, $operation->getDeprecationReason(), $resourceType, $resourceClass, true, $operation)];
×
148
        }
149

150
        if (!$fieldConfiguration) {
×
151
            return [];
×
152
        }
153

154
        $subscriptionName = $operation->getName();
×
155
        // TODO: 3.0 change this
156
        if ('update_subscription' === $subscriptionName) {
×
157
            $subscriptionName = 'update';
×
158
        }
159

160
        $subscriptionFields[$subscriptionName.$operation->getShortName().'Subscribe'] = $fieldConfiguration;
×
161

162
        return $subscriptionFields;
×
163
    }
164

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

UNCOV
176
        if (null !== $ioMetadata && \array_key_exists('class', $ioMetadata) && null === $ioMetadata['class']) {
×
177
            if ($input) {
×
178
                return ['clientMutationId' => $clientMutationId];
×
179
            }
180

181
            return [];
×
182
        }
183

UNCOV
184
        if ($operation instanceof Subscription && $input) {
×
185
            return [
×
186
                'id' => $idField,
×
187
                'clientSubscriptionId' => $clientSubscriptionId,
×
188
            ];
×
189
        }
190

UNCOV
191
        if ('delete' === $operation->getName()) {
×
192
            $fields = [
×
193
                'id' => $idField,
×
194
            ];
×
195

196
            if ($input) {
×
197
                $fields['clientMutationId'] = $clientMutationId;
×
198
            }
199

200
            return $fields;
×
201
        }
202

UNCOV
203
        if (!$input || (!$operation->getResolver() && 'create' !== $operation->getName())) {
×
UNCOV
204
            $fields['id'] = $idField;
×
205
        }
UNCOV
206
        if ($input && $depth >= 1) {
×
207
            $fields['id'] = $optionalIdField;
×
208
        }
209

UNCOV
210
        ++$depth; // increment the depth for the call to getResourceFieldConfiguration.
×
211

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

221
                if (
UNCOV
222
                    !$propertyTypes
×
UNCOV
223
                    || (!$input && false === $propertyMetadata->isReadable())
×
UNCOV
224
                    || ($input && false === $propertyMetadata->isWritable())
×
225
                ) {
UNCOV
226
                    continue;
×
227
                }
228

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

UNCOV
240
        if ($operation instanceof Mutation && $input) {
×
UNCOV
241
            $fields['clientMutationId'] = $clientMutationId;
×
242
        }
243

UNCOV
244
        return $fields;
×
245
    }
246

247
    private function isEnumClass(string $resourceClass): bool
248
    {
UNCOV
249
        return is_a($resourceClass, \BackedEnum::class, true);
×
250
    }
251

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

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

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

UNCOV
275
        return $enumCases;
×
276
    }
277

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

288
            $args[$id]['type'] = $this->typeConverter->resolveType($arg['type']);
×
289
        }
290

UNCOV
291
        return $args;
×
292
    }
293

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

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

UNCOV
319
                    $type = $this->parameterToObjectType($n, $key);
×
UNCOV
320
                    if (isset($fields[$key]) && ($t = $fields[$key]['type']) instanceof InputObjectType) {
×
UNCOV
321
                        $t = $fields[$key]['type'];
×
UNCOV
322
                        $t->config['fields'] = array_merge($t->config['fields'], $type->config['fields']);
×
UNCOV
323
                        $type = $t;
×
324
                    }
325
                }
326
            }
327

UNCOV
328
            if ($field['required']) {
×
329
                $type = GraphQLType::nonNull($type);
×
330
            }
331

UNCOV
332
            if (isset($fields[$key])) {
×
UNCOV
333
                if ($type instanceof ListOfType) {
×
UNCOV
334
                    $key .= '_list';
×
335
                }
336
            }
337

UNCOV
338
            $fields[$key] = ['type' => $type, 'name' => $key];
×
339
        }
340

UNCOV
341
        return new InputObjectType(['name' => $name, 'fields' => $fields]);
×
342
    }
343

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

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

371
            if (
UNCOV
372
                $isCollectionType
×
UNCOV
373
                && $collectionValueType = $type->getCollectionValueTypes()[0] ?? null
×
374
            ) {
UNCOV
375
                $resourceClass = $collectionValueType->getClassName();
×
376
            } else {
UNCOV
377
                $resourceClass = $type->getClassName();
×
378
            }
379

UNCOV
380
            $resourceOperation = $rootOperation;
×
UNCOV
381
            if ($resourceClass && $depth >= 1 && $this->resourceClassResolver->isResourceClass($resourceClass)) {
×
UNCOV
382
                $resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass);
×
UNCOV
383
                $resourceOperation = $resourceMetadataCollection->getOperation($isCollectionType ? 'collection_query' : 'item_query');
×
384
            }
385

UNCOV
386
            if (!$resourceOperation instanceof Operation) {
×
387
                throw new \LogicException('The resource operation should be a GraphQL operation.');
×
388
            }
389

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

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

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

UNCOV
410
            $args = [];
×
411

UNCOV
412
            if (!$input && !$rootOperation instanceof Mutation && !$rootOperation instanceof Subscription && !$isStandardGraphqlType) {
×
UNCOV
413
                if ($isCollectionType) {
×
UNCOV
414
                    if (!$this->isEnumClass($resourceClass) && $this->pagination->isGraphQlEnabled($resourceOperation)) {
×
UNCOV
415
                        $args = $this->getGraphQlPaginationArgs($resourceOperation);
×
416
                    }
417

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

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

UNCOV
431
            if ($isStandardGraphqlType || $input) {
×
UNCOV
432
                $resolve = null;
×
433
            } else {
UNCOV
434
                $resolve = ($this->resolverFactory)($resourceClass, $rootResource, $resourceOperation, $this->propertyMetadataFactory);
×
435
            }
436

UNCOV
437
            return [
×
UNCOV
438
                'type' => $graphqlType,
×
UNCOV
439
                'description' => $fieldDescription,
×
UNCOV
440
                'args' => $args,
×
UNCOV
441
                'resolve' => $resolve,
×
UNCOV
442
                'deprecationReason' => $deprecationReason,
×
UNCOV
443
            ];
×
UNCOV
444
        } catch (InvalidTypeException) {
×
445
            // just ignore invalid types
446
        }
447

UNCOV
448
        return null;
×
449
    }
450

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

UNCOV
460
            if (!str_contains($key, ':property')) {
×
UNCOV
461
                $args[$key] = ['type' => GraphQLType::string()];
×
462

UNCOV
463
                if ($parameter->getRequired()) {
×
464
                    $args[$key]['type'] = GraphQLType::nonNull($args[$key]['type']);
×
465
                }
466

UNCOV
467
                continue;
×
468
            }
469

UNCOV
470
            if (!($filterId = $parameter->getFilter()) || !$this->filterLocator->has($filterId)) {
×
471
                continue;
×
472
            }
473

UNCOV
474
            $filter = $this->filterLocator->get($filterId);
×
UNCOV
475
            $parsedKey = explode('[:property]', $key);
×
UNCOV
476
            $flattenFields = [];
×
477

UNCOV
478
            if ($filter instanceof FilterInterface) {
×
UNCOV
479
                foreach ($filter->getDescription($operation->getClass()) as $name => $value) {
×
UNCOV
480
                    $values = [];
×
UNCOV
481
                    parse_str($name, $values);
×
UNCOV
482
                    if (isset($values[$parsedKey[0]])) {
×
UNCOV
483
                        $values = $values[$parsedKey[0]];
×
484
                    }
485

UNCOV
486
                    $name = key($values);
×
UNCOV
487
                    $flattenFields[] = ['name' => $name, 'required' => $value['required'] ?? null, 'description' => $value['description'] ?? null, 'leafs' => $values[$name], 'type' => $value['type'] ?? 'string'];
×
488
                }
489

UNCOV
490
                $args[$parsedKey[0]] = $this->parameterToObjectType($flattenFields, $parsedKey[0]);
×
491
            }
492

UNCOV
493
            if ($filter instanceof OpenApiParameterFilterInterface) {
×
UNCOV
494
                foreach ($filter->getOpenApiParameters($parameter) as $value) {
×
UNCOV
495
                    $values = [];
×
UNCOV
496
                    parse_str($value->getName(), $values);
×
UNCOV
497
                    if (isset($values[$parsedKey[0]])) {
×
UNCOV
498
                        $values = $values[$parsedKey[0]];
×
499
                    }
500

UNCOV
501
                    $name = key($values);
×
UNCOV
502
                    $flattenFields[] = ['name' => $name, 'required' => $value->getRequired(), 'description' => $value->getDescription(), 'leafs' => $values[$name], 'type' => $value->getSchema()['type'] ?? 'string'];
×
503
                }
504

UNCOV
505
                $args[$parsedKey[0]] = $this->parameterToObjectType($flattenFields, $parsedKey[0].$operation->getShortName().$operation->getName());
×
506
            }
507
        }
508

UNCOV
509
        return $args;
×
510
    }
511

512
    private function getGraphQlPaginationArgs(Operation $queryOperation): array
513
    {
UNCOV
514
        $paginationType = $this->pagination->getGraphQlPaginationType($queryOperation);
×
515

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

537
        $paginationOptions = $this->pagination->getOptions();
×
538

539
        $args = [
×
540
            $paginationOptions['page_parameter_name'] => [
×
541
                'type' => GraphQLType::int(),
×
542
                'description' => 'Returns the current page.',
×
543
            ],
×
544
        ];
×
545

546
        if ($paginationOptions['client_items_per_page']) {
×
547
            $args[$paginationOptions['items_per_page_parameter_name']] = [
×
548
                'type' => GraphQLType::int(),
×
549
                'description' => 'Returns the number of items per page.',
×
550
            ];
×
551
        }
552

553
        return $args;
×
554
    }
555

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

UNCOV
562
        foreach ($resourceOperation->getFilters() ?? [] as $filterId) {
×
563
            if (!$this->filterLocator->has($filterId)) {
×
564
                continue;
×
565
            }
566

567
            $entityClass = $this->getStateOptionsClass($resourceOperation, $resourceOperation->getClass());
×
568
            foreach ($this->filterLocator->get($filterId)->getDescription($entityClass) as $key => $description) {
×
569
                $nullable = isset($description['required']) ? !$description['required'] : true;
×
570
                $filterType = \in_array($description['type'], Type::$builtinTypes, true) ? new Type($description['type'], $nullable) : new Type('object', $nullable, $description['type']);
×
571
                $graphqlFilterType = $this->convertType($filterType, false, $resourceOperation, $rootOperation, $resourceClass, $rootResource, $property, $depth);
×
572

573
                if (str_ends_with($key, '[]')) {
×
574
                    $graphqlFilterType = GraphQLType::listOf($graphqlFilterType);
×
575
                    $key = substr($key, 0, -2).'_list';
×
576
                }
577

578
                /** @var string $key */
579
                $key = str_replace('.', $this->nestingSeparator, $key);
×
580

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

UNCOV
592
        return $this->convertFilterArgsToTypes($args);
×
593
    }
594

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

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

611
            $args[$key] = $value;
×
612
        }
613

614
        return $args;
×
615
    }
616

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

UNCOV
627
        foreach ($args as $key => $value) {
×
UNCOV
628
            if (!\is_array($value) || !isset($value['#name'])) {
×
UNCOV
629
                continue;
×
630
            }
631

632
            $name = $value['#name'];
×
633

634
            if ($this->typesContainer->has($name)) {
×
635
                $args[$key] = $this->typesContainer->get($name);
×
636
                continue;
×
637
            }
638

639
            unset($value['#name']);
×
640

641
            $filterArgType = GraphQLType::listOf(new InputObjectType([
×
642
                'name' => $name,
×
643
                'fields' => $this->convertFilterArgsToTypes($value),
×
644
            ]));
×
645

646
            $this->typesContainer->set($name, $filterArgType);
×
647

648
            $args[$key] = $filterArgType;
×
649
        }
650

UNCOV
651
        return $args;
×
652
    }
653

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

UNCOV
663
        if (null === $graphqlType) {
×
UNCOV
664
            throw new InvalidTypeException(\sprintf('The type "%s" is not supported.', $type->getBuiltinType()));
×
665
        }
666

UNCOV
667
        if (\is_string($graphqlType)) {
×
668
            if (!$this->typesContainer->has($graphqlType)) {
×
669
                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));
×
670
            }
671

672
            $graphqlType = $this->typesContainer->get($graphqlType);
×
673
        }
674

UNCOV
675
        if ($this->typeBuilder->isCollection($type)) {
×
UNCOV
676
            if (!$input && !$this->isEnumClass($resourceClass) && $this->pagination->isGraphQlEnabled($resourceOperation)) {
×
UNCOV
677
                return $this->typeBuilder->getPaginatedCollectionType($graphqlType, $resourceOperation);
×
678
            }
679

UNCOV
680
            return GraphQLType::listOf($graphqlType);
×
681
        }
682

UNCOV
683
        return $forceNullable || !$graphqlType instanceof NullableType || $type->isNullable() || ($rootOperation instanceof Mutation && 'update' === $rootOperation->getName())
×
UNCOV
684
            ? $graphqlType
×
UNCOV
685
            : GraphQLType::nonNull($graphqlType);
×
686
    }
687

688
    private function normalizePropertyName(string $property, string $resourceClass): string
689
    {
UNCOV
690
        if (null === $this->nameConverter) {
×
691
            return $property;
×
692
        }
UNCOV
693
        if ($this->nameConverter instanceof AdvancedNameConverterInterface || $this->nameConverter instanceof MetadataAwareNameConverter) {
×
UNCOV
694
            return $this->nameConverter->normalize($property, $resourceClass);
×
695
        }
696

697
        return $this->nameConverter->normalize($property);
×
698
    }
699
}
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