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

api-platform / core / 14635100171

24 Apr 2025 06:39AM UTC coverage: 8.271% (+0.02%) from 8.252%
14635100171

Pull #6904

github

web-flow
Merge c9cefd82e into a3e5e53ea
Pull Request #6904: feat(graphql): added support for graphql subscriptions to work for actions

0 of 73 new or added lines in 3 files covered. (0.0%)

1999 existing lines in 144 files now uncovered.

13129 of 158728 relevant lines covered (8.27%)

13.6 hits per line

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

94.12
/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\ResolverFactoryInterface;
20
use ApiPlatform\GraphQl\Type\Definition\TypeInterface;
21
use ApiPlatform\Metadata\FilterInterface;
22
use ApiPlatform\Metadata\GraphQl\Mutation;
23
use ApiPlatform\Metadata\GraphQl\Operation;
24
use ApiPlatform\Metadata\GraphQl\Query;
25
use ApiPlatform\Metadata\GraphQl\Subscription;
26
use ApiPlatform\Metadata\InflectorInterface;
27
use ApiPlatform\Metadata\OpenApiParameterFilterInterface;
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 FieldsBuilderEnumInterface
52
{
53
    private readonly ContextAwareTypeBuilderInterface $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 $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())
56
    {
57
        $this->typeBuilder = $typeBuilder;
161✔
58
    }
59

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

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

83
        $fieldName = lcfirst('item_query' === $operation->getName() ? $operation->getShortName() : $operation->getName().$operation->getShortName());
157✔
84

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

90
            return [$fieldName => array_merge($fieldConfiguration, $configuration)];
157✔
91
        }
92

93
        return [];
×
94
    }
95

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

105
        $fieldName = lcfirst('collection_query' === $operation->getName() ? $operation->getShortName() : $operation->getName().$operation->getShortName());
159✔
106

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

112
            return [$this->inflector->pluralize($fieldName) => array_merge($fieldConfiguration, $configuration)];
159✔
113
        }
114

115
        return [];
×
116
    }
117

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

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

131
        $mutationFields[$operation->getName().$operation->getShortName()] = $fieldConfiguration ?? [];
143✔
132

133
        return $mutationFields;
143✔
134
    }
135

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

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

UNCOV
149
        if (!$fieldConfiguration) {
133✔
150
            return [];
×
151
        }
152

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

UNCOV
159
        $subscriptionFields[$subscriptionName.$operation->getShortName().'Subscribe'] = $fieldConfiguration;
133✔
160

UNCOV
161
        return $subscriptionFields;
133✔
162
    }
163

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

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

UNCOV
180
            return [];
3✔
181
        }
182

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

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

UNCOV
195
            if ($input) {
4✔
UNCOV
196
                $fields['clientMutationId'] = $clientMutationId;
4✔
197
            }
198

UNCOV
199
            return $fields;
4✔
200
        }
201

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

209
        ++$depth; // increment the depth for the call to getResourceFieldConfiguration.
152✔
210

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

220
                if (
221
                    !$propertyTypes
152✔
222
                    || (!$input && false === $propertyMetadata->isReadable())
152✔
223
                    || ($input && false === $propertyMetadata->isWritable())
152✔
224
                ) {
225
                    continue;
56✔
226
                }
227

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

239
        if ($operation instanceof Mutation && $input) {
152✔
240
            $fields['clientMutationId'] = $clientMutationId;
38✔
241
        }
242

243
        return $fields;
152✔
244
    }
245

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

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

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

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

274
        return $enumCases;
6✔
275
    }
276

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

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

290
        return $args;
159✔
291
    }
292

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

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

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

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

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

337
            $fields[$key] = ['type' => $type, 'name' => $key];
135✔
338
        }
339

340
        return new InputObjectType(['name' => $name, 'fields' => $fields]);
135✔
341
    }
342

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

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

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

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

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

389
            $graphqlType = $this->convertType($type, $input, $resourceOperation, $rootOperation, $resourceClass ?? '', $rootResource, $property, $depth, $forceNullable);
159✔
390

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

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

409
            $args = [];
159✔
410

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

417
                    $args = $this->getFilterArgs($args, $resourceClass, $rootResource, $resourceOperation, $rootOperation, $property, $depth);
159✔
418

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

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

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

447
        return null;
32✔
448
    }
449

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

459
            if (!str_contains($key, ':property')) {
135✔
460
                $args[$key] = ['type' => GraphQLType::string()];
135✔
461

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

466
                continue;
135✔
467
            }
468

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

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

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

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

489
                $args[$parsedKey[0]] = $this->parameterToObjectType($flattenFields, $parsedKey[0]);
135✔
490
            }
491

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

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

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

508
        return $args;
159✔
509
    }
510

511
    private function getGraphQlPaginationArgs(Operation $queryOperation): array
512
    {
513
        $paginationType = $this->pagination->getGraphQlPaginationType($queryOperation);
145✔
514

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

UNCOV
536
        $paginationOptions = $this->pagination->getOptions();
133✔
537

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

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

UNCOV
552
        return $args;
133✔
553
    }
554

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

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

UNCOV
566
            $entityClass = $resourceClass;
133✔
UNCOV
567
            if ($options = $resourceOperation->getStateOptions()) {
133✔
UNCOV
568
                if (class_exists(Options::class) && $options instanceof Options && $options->getEntityClass()) {
133✔
UNCOV
569
                    $entityClass = $options->getEntityClass();
133✔
570
                }
571

UNCOV
572
                if (class_exists(ODMOptions::class) && $options instanceof ODMOptions && $options->getDocumentClass()) {
133✔
UNCOV
573
                    $entityClass = $options->getDocumentClass();
133✔
574
                }
575
            }
576

UNCOV
577
            foreach ($this->filterLocator->get($filterId)->getDescription($entityClass) as $key => $description) {
133✔
UNCOV
578
                $nullable = isset($description['required']) ? !$description['required'] : true;
133✔
UNCOV
579
                $filterType = \in_array($description['type'], Type::$builtinTypes, true) ? new Type($description['type'], $nullable) : new Type('object', $nullable, $description['type']);
133✔
UNCOV
580
                $graphqlFilterType = $this->convertType($filterType, false, $resourceOperation, $rootOperation, $resourceClass, $rootResource, $property, $depth);
133✔
581

UNCOV
582
                if (str_ends_with($key, '[]')) {
133✔
UNCOV
583
                    $graphqlFilterType = GraphQLType::listOf($graphqlFilterType);
133✔
UNCOV
584
                    $key = substr($key, 0, -2).'_list';
133✔
585
                }
586

587
                /** @var string $key */
UNCOV
588
                $key = str_replace('.', $this->nestingSeparator, $key);
133✔
589

UNCOV
590
                parse_str($key, $parsed);
133✔
UNCOV
591
                if (\array_key_exists($key, $parsed) && \is_array($parsed[$key])) {
133✔
592
                    $parsed = [$key => ''];
×
593
                }
UNCOV
594
                array_walk_recursive($parsed, static function (&$v) use ($graphqlFilterType): void {
133✔
UNCOV
595
                    $v = $graphqlFilterType;
133✔
UNCOV
596
                });
133✔
UNCOV
597
                $args = $this->mergeFilterArgs($args, $parsed, $resourceOperation, $key);
133✔
598
            }
599
        }
600

601
        return $this->convertFilterArgsToTypes($args);
159✔
602
    }
603

604
    private function mergeFilterArgs(array $args, array $parsed, ?Operation $operation = null, string $original = ''): array
605
    {
UNCOV
606
        foreach ($parsed as $key => $value) {
133✔
607
            // Never override keys that cannot be merged
UNCOV
608
            if (isset($args[$key]) && !\is_array($args[$key])) {
133✔
UNCOV
609
                continue;
133✔
610
            }
611

UNCOV
612
            if (\is_array($value)) {
133✔
UNCOV
613
                $value = $this->mergeFilterArgs($args[$key] ?? [], $value);
133✔
UNCOV
614
                if (!isset($value['#name'])) {
133✔
UNCOV
615
                    $name = (false === $pos = strrpos($original, '[')) ? $original : substr($original, 0, (int) $pos);
133✔
UNCOV
616
                    $value['#name'] = ($operation ? $operation->getShortName() : '').'Filter_'.strtr($name, ['[' => '_', ']' => '', '.' => '__']);
133✔
617
                }
618
            }
619

UNCOV
620
            $args[$key] = $value;
133✔
621
        }
622

UNCOV
623
        return $args;
133✔
624
    }
625

626
    private function convertFilterArgsToTypes(array $args): array
627
    {
628
        foreach ($args as $key => $value) {
159✔
629
            if (strpos($key, '.')) {
145✔
630
                // Declare relations/nested fields in a GraphQL compatible syntax.
631
                $args[str_replace('.', $this->nestingSeparator, $key)] = $value;
×
632
                unset($args[$key]);
×
633
            }
634
        }
635

636
        foreach ($args as $key => $value) {
159✔
637
            if (!\is_array($value) || !isset($value['#name'])) {
145✔
638
                continue;
145✔
639
            }
640

UNCOV
641
            $name = $value['#name'];
133✔
642

UNCOV
643
            if ($this->typesContainer->has($name)) {
133✔
UNCOV
644
                $args[$key] = $this->typesContainer->get($name);
8✔
UNCOV
645
                continue;
8✔
646
            }
647

UNCOV
648
            unset($value['#name']);
133✔
649

UNCOV
650
            $filterArgType = GraphQLType::listOf(new InputObjectType([
133✔
UNCOV
651
                'name' => $name,
133✔
UNCOV
652
                'fields' => $this->convertFilterArgsToTypes($value),
133✔
UNCOV
653
            ]));
133✔
654

UNCOV
655
            $this->typesContainer->set($name, $filterArgType);
133✔
656

UNCOV
657
            $args[$key] = $filterArgType;
133✔
658
        }
659

660
        return $args;
159✔
661
    }
662

663
    /**
664
     * Converts a built-in type to its GraphQL equivalent.
665
     *
666
     * @throws InvalidTypeException
667
     */
668
    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
669
    {
670
        $graphqlType = $this->typeConverter->convertType($type, $input, $rootOperation, $resourceClass, $rootResource, $property, $depth);
159✔
671

672
        if (null === $graphqlType) {
159✔
673
            throw new InvalidTypeException(\sprintf('The type "%s" is not supported.', $type->getBuiltinType()));
32✔
674
        }
675

676
        if (\is_string($graphqlType)) {
159✔
UNCOV
677
            if (!$this->typesContainer->has($graphqlType)) {
133✔
678
                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));
×
679
            }
680

UNCOV
681
            $graphqlType = $this->typesContainer->get($graphqlType);
133✔
682
        }
683

684
        if ($this->typeBuilder->isCollection($type)) {
159✔
685
            if (!$input && !$this->isEnumClass($resourceClass) && $this->pagination->isGraphQlEnabled($resourceOperation)) {
159✔
686
                return $this->typeBuilder->getPaginatedCollectionType($graphqlType, $resourceOperation);
145✔
687
            }
688

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

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

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