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

overblog / GraphQLBundle / 23208575639

17 Mar 2026 05:51PM UTC coverage: 98.553% (+0.007%) from 98.546%
23208575639

Pull #1241

github

web-flow
Merge 6e70843d8 into 7e9f36ef2
Pull Request #1241: resettable schemas, getCurrentSchemaName for TypeResolver, and IsPublic support for InputObject fields

44 of 44 new or added lines in 9 files covered. (100.0%)

53 existing lines in 9 files now uncovered.

4563 of 4630 relevant lines covered (98.55%)

78.11 hits per line

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

97.7
/src/Config/Parser/MetadataParser/MetadataParser.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Overblog\GraphQLBundle\Config\Parser\MetadataParser;
6

7
use Doctrine\Common\Annotations\AnnotationException;
8
use GraphQL\Type\Definition\ResolveInfo;
9
use Overblog\GraphQLBundle\Annotation\Annotation as Meta;
10
use Overblog\GraphQLBundle\Annotation as Metadata;
11
use Overblog\GraphQLBundle\Annotation\InputField;
12
use Overblog\GraphQLBundle\Config\Parser\MetadataParser\TypeGuesser\DocBlockTypeGuesser;
13
use Overblog\GraphQLBundle\Config\Parser\MetadataParser\TypeGuesser\DoctrineTypeGuesser;
14
use Overblog\GraphQLBundle\Config\Parser\MetadataParser\TypeGuesser\TypeGuessingException;
15
use Overblog\GraphQLBundle\Config\Parser\MetadataParser\TypeGuesser\TypeHintTypeGuesser;
16
use Overblog\GraphQLBundle\Config\Parser\PreParserInterface;
17
use Overblog\GraphQLBundle\Relay\Connection\ConnectionInterface;
18
use Overblog\GraphQLBundle\Relay\Connection\EdgeInterface;
19
use Overblog\GraphQLBundle\Transformer\ArgumentsTransformer;
20
use ReflectionClass;
21
use ReflectionClassConstant;
22
use ReflectionException;
23
use ReflectionMethod;
24
use ReflectionNamedType;
25
use ReflectionParameter;
26
use ReflectionProperty;
27
use Reflector;
28
use RuntimeException;
29
use SplFileInfo;
30
use Symfony\Component\Config\Resource\FileResource;
31
use Symfony\Component\DependencyInjection\ContainerBuilder;
32
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
33

34
use function array_filter;
35
use function array_keys;
36
use function array_map;
37
use function array_unshift;
38
use function current;
39
use function file_get_contents;
40
use function implode;
41
use function in_array;
42
use function is_string;
43
use function preg_match;
44
use function sprintf;
45
use function str_replace;
46
use function strlen;
47
use function substr;
48
use function trim;
49

50
use const PHP_VERSION_ID;
51

52
abstract class MetadataParser implements PreParserInterface
53
{
54
    public const ANNOTATION_NAMESPACE = 'Overblog\GraphQLBundle\Annotation\\';
55
    public const METADATA_FORMAT = '%s';
56

57
    private static ClassesTypesMap $map;
58
    private static array $typeGuessers = [];
59
    private static array $providers = [];
60
    private static array $reflections = [];
61

62
    private const GQL_SCALAR = 'scalar';
63
    private const GQL_ENUM = 'enum';
64
    private const GQL_TYPE = 'type';
65
    private const GQL_INPUT = 'input';
66
    private const GQL_UNION = 'union';
67
    private const GQL_INTERFACE = 'interface';
68

69
    /**
70
     * @see https://facebook.github.io/graphql/draft/#sec-Input-and-Output-Types
71
     */
72
    private const VALID_INPUT_TYPES = [self::GQL_SCALAR, self::GQL_ENUM, self::GQL_INPUT];
73
    private const VALID_OUTPUT_TYPES = [self::GQL_SCALAR, self::GQL_TYPE, self::GQL_INTERFACE, self::GQL_UNION, self::GQL_ENUM];
74

75
    /**
76
     * {@inheritdoc}
77
     *
78
     * @throws InvalidArgumentException
79
     * @throws ReflectionException
80
     */
81
    public static function preParse(SplFileInfo $file, ContainerBuilder $container, array $configs = []): void
82
    {
83
        $container->setParameter('overblog_graphql_types.classes_map', self::processFile($file, $container, $configs, true));
111✔
84
    }
85

86
    /**
87
     * @throws InvalidArgumentException
88
     * @throws ReflectionException
89
     */
90
    public static function parse(SplFileInfo $file, ContainerBuilder $container, array $configs = []): array
91
    {
92
        return self::processFile($file, $container, $configs, false);
111✔
93
    }
94

95
    public static function finalize(ContainerBuilder $container): void
96
    {
97
        $parameter = 'overblog_graphql_types.interfaces_map';
88✔
98
        $value = $container->hasParameter($parameter) ? $container->getParameter($parameter) : [];
88✔
99
        foreach (self::$map->interfacesToArray() as $interface => $types) {
88✔
100
            /** @phpstan-ignore-next-line */
101
            if (!isset($value[$interface])) {
3✔
102
                /** @phpstan-ignore-next-line */
103
                $value[$interface] = [];
3✔
104
            }
105
            foreach ($types as $className => $typeName) {
3✔
106
                $value[$interface][$className] = $typeName;
3✔
107
            }
108
        }
109

110
        $container->setParameter('overblog_graphql_types.interfaces_map', $value);
88✔
111
    }
112

113
    /**
114
     * @internal
115
     */
116
    public static function reset(array $configs): void
117
    {
118
        self::$map = new ClassesTypesMap();
196✔
119
        self::$typeGuessers = [
196✔
120
            new DocBlockTypeGuesser(self::$map),
196✔
121
            new TypeHintTypeGuesser(self::$map),
196✔
122
            new DoctrineTypeGuesser(self::$map, $configs['doctrine']['types_mapping']),
196✔
123
        ];
196✔
124
        self::$providers = [];
196✔
125
        self::$reflections = [];
196✔
126
    }
127

128
    /**
129
     * Process a file.
130
     *
131
     * @throws InvalidArgumentException|ReflectionException|AnnotationException
132
     */
133
    private static function processFile(SplFileInfo $file, ContainerBuilder $container, array $configs, bool $preProcess): array
134
    {
135
        $container->addResource(new FileResource($file->getRealPath()));
111✔
136

137
        try {
138
            $className = $file->getBasename('.php');
111✔
139
            if (preg_match('#namespace (.+);#', file_get_contents($file->getRealPath()), $matches)) {
111✔
140
                $className = trim($matches[1]).'\\'.$className;
111✔
141
            }
142

143
            $gqlTypes = [];
111✔
144
            /** @phpstan-ignore-next-line */
145
            $reflectionClass = self::getClassReflection($className);
111✔
146

147
            foreach (static::getMetadatas($reflectionClass) as $classMetadata) {
111✔
148
                if ($classMetadata instanceof Meta) {
111✔
149
                    $gqlTypes = self::classMetadatasToGQLConfiguration(
111✔
150
                        $reflectionClass,
111✔
151
                        $classMetadata,
111✔
152
                        $configs,
111✔
153
                        $gqlTypes,
111✔
154
                        $preProcess
111✔
155
                    );
111✔
156
                }
157
            }
158

159
            return $preProcess ? self::$map->classesToArray() : $gqlTypes;
111✔
160
        } catch (ReflectionException $e) {
48✔
161
            return $gqlTypes;
4✔
162
        } catch (\InvalidArgumentException $e) {
44✔
163
            throw new InvalidArgumentException(sprintf('Failed to parse GraphQL metadata from file "%s".', $file), $e->getCode(), $e);
44✔
164
        }
165
    }
166

167
    /**
168
     * @return array<string,array>
169
     */
170
    private static function classMetadatasToGQLConfiguration(
171
        ReflectionClass $reflectionClass,
172
        Meta $classMetadata,
173
        array $configs,
174
        array $gqlTypes,
175
        bool $preProcess
176
    ): array {
177
        $gqlConfiguration = $gqlType = $gqlName = null;
111✔
178

179
        switch (true) {
180
            case $classMetadata instanceof Metadata\Type:
111✔
181
                $gqlType = self::GQL_TYPE;
111✔
182
                $gqlName = $classMetadata->name ?? $reflectionClass->getShortName();
111✔
183
                if (!$preProcess) {
111✔
184
                    $gqlConfiguration = self::typeMetadataToGQLConfiguration($reflectionClass, $classMetadata, $gqlName, $configs);
111✔
185

186
                    if ($classMetadata instanceof Metadata\Relay\Connection) {
111✔
187
                        if (!$reflectionClass->implementsInterface(ConnectionInterface::class)) {
106✔
188
                            throw new InvalidArgumentException(sprintf('The metadata %s on class "%s" can only be used on class implementing the ConnectionInterface.', self::formatMetadata('Connection'), $reflectionClass->getName()));
×
189
                        }
190

191
                        if (!(isset($classMetadata->edge) xor isset($classMetadata->node))) {
106✔
192
                            throw new InvalidArgumentException(sprintf('The metadata %s on class "%s" is invalid. You must define either the "edge" OR the "node" attribute, but not both.', self::formatMetadata('Connection'), $reflectionClass->getName()));
×
193
                        }
194

195
                        $edgeType = $classMetadata->edge ?? false;
106✔
196
                        if (!$edgeType) {
106✔
197
                            $edgeType = $gqlName.'Edge';
106✔
198
                            $gqlTypes[$edgeType] = [
106✔
199
                                'type' => 'object',
106✔
200
                                'config' => [
106✔
201
                                    'builders' => [
106✔
202
                                        ['builder' => 'relay-edge', 'builderConfig' => ['nodeType' => $classMetadata->node]],
106✔
203
                                    ],
106✔
204
                                ],
106✔
205
                            ];
106✔
206
                        }
207

208
                        if (!isset($gqlConfiguration['config']['builders'])) {
106✔
209
                            $gqlConfiguration['config']['builders'] = [];
106✔
210
                        }
211

212
                        array_unshift($gqlConfiguration['config']['builders'], ['builder' => 'relay-connection', 'builderConfig' => ['edgeType' => $edgeType]]);
106✔
213
                    }
214

215
                    $interfaces = $gqlConfiguration['config']['interfaces'] ?? [];
111✔
216
                    foreach ($interfaces as $interface) {
111✔
217
                        self::$map->addInterfaceType($interface, $gqlName, $reflectionClass->getName());
109✔
218
                    }
219
                }
220
                break;
111✔
221

222
            case $classMetadata instanceof Metadata\Input:
109✔
223
                $gqlType = self::GQL_INPUT;
106✔
224
                $gqlName = $classMetadata->name ?? self::suffixName($reflectionClass->getShortName(), 'Input');
106✔
225
                if (!$preProcess) {
106✔
226
                    $gqlConfiguration = self::inputMetadataToGQLConfiguration($reflectionClass, $classMetadata);
106✔
227
                }
228
                break;
106✔
229

230
            case $classMetadata instanceof Metadata\Scalar:
109✔
231
                $gqlType = self::GQL_SCALAR;
106✔
232
                if (!$preProcess) {
106✔
233
                    $gqlConfiguration = self::scalarMetadataToGQLConfiguration($reflectionClass, $classMetadata);
106✔
234
                }
235
                break;
106✔
236

237
            case $classMetadata instanceof Metadata\Enum:
109✔
238
                $gqlType = self::GQL_ENUM;
106✔
239
                if (!$preProcess) {
106✔
240
                    $gqlConfiguration = self::enumMetadataToGQLConfiguration($reflectionClass, $classMetadata);
106✔
241
                }
242
                break;
106✔
243

244
            case $classMetadata instanceof Metadata\Union:
109✔
245
                $gqlType = self::GQL_UNION;
106✔
246
                if (!$preProcess) {
106✔
247
                    $gqlConfiguration = self::unionMetadataToGQLConfiguration($reflectionClass, $classMetadata);
106✔
248
                }
249
                break;
106✔
250

251
            case $classMetadata instanceof Metadata\TypeInterface:
109✔
252
                $gqlType = self::GQL_INTERFACE;
109✔
253
                if (!$preProcess) {
109✔
254
                    $gqlName = !empty($classMetadata->name) ? $classMetadata->name : $reflectionClass->getShortName();
109✔
255
                    $gqlConfiguration = self::typeInterfaceMetadataToGQLConfiguration($reflectionClass, $classMetadata, $gqlName);
109✔
256
                }
257
                break;
109✔
258

259
            case $classMetadata instanceof Metadata\Provider:
109✔
260
                if ($preProcess) {
109✔
261
                    self::$providers[] = ['reflectionClass' => $reflectionClass, 'metadata' => $classMetadata];
109✔
262
                }
263

264
                return [];
109✔
265
        }
266

267
        if (null !== $gqlType) {
111✔
268
            if (!$gqlName) {
111✔
269
                $gqlName = !empty($classMetadata->name) ? $classMetadata->name : $reflectionClass->getShortName();
109✔
270
            }
271

272
            if ($preProcess) {
111✔
273
                if (self::$map->hasType($gqlName)) {
111✔
274
                    throw new InvalidArgumentException(sprintf('The GraphQL type "%s" has already been registered in class "%s"', $gqlName, self::$map->getType($gqlName)['class']));
4✔
275
                }
276
                self::$map->addClassType($gqlName, $reflectionClass->getName(), $gqlType);
111✔
277
            } else {
278
                $gqlTypes = [$gqlName => $gqlConfiguration] + $gqlTypes;
111✔
279
            }
280
        }
281

282
        return $gqlTypes;
111✔
283
    }
284

285
    /**
286
     * @phpstan-param class-string $className
287
     *
288
     * @throws ReflectionException
289
     */
290
    private static function getClassReflection(string $className): ReflectionClass
291
    {
292
        self::$reflections[$className] ??= new ReflectionClass($className);
111✔
293

294
        return self::$reflections[$className];
111✔
295
    }
296

297
    private static function typeMetadataToGQLConfiguration(
298
        ReflectionClass $reflectionClass,
299
        Metadata\Type $classMetadata,
300
        string $gqlName,
301
        array $configs
302
    ): array {
303
        $isMutation = $isDefault = $isRoot = false;
111✔
304
        if (isset($configs['definitions']['schema'])) {
111✔
305
            $defaultSchemaName = isset($configs['definitions']['schema']['default']) ? 'default' : array_key_first($configs['definitions']['schema']);
109✔
306
            foreach ($configs['definitions']['schema'] as $schemaName => $schema) {
109✔
307
                $schemaQuery = $schema['query'] ?? null;
109✔
308
                $schemaMutation = $schema['mutation'] ?? null;
109✔
309
                $schemaSubscription = $schema['subscription'] ?? null;
109✔
310

311
                if ($gqlName === $schemaQuery) {
109✔
312
                    $isRoot = true;
109✔
313
                    if ($defaultSchemaName === $schemaName) {
109✔
314
                        $isDefault = true;
109✔
315
                    }
316
                } elseif ($gqlName === $schemaMutation) {
109✔
317
                    $isMutation = true;
109✔
318
                    $isRoot = true;
109✔
319
                    if ($defaultSchemaName === $schemaName) {
109✔
320
                        $isDefault = true;
109✔
321
                    }
322
                } elseif ($gqlName === $schemaSubscription) {
109✔
323
                    $isRoot = true;
×
324
                }
325
            }
326
        }
327

328
        $currentValue = $isRoot ? sprintf("service('%s')", self::formatNamespaceForExpression($reflectionClass->getName())) : 'value';
111✔
329

330
        $gqlConfiguration = self::graphQLTypeConfigFromAnnotation($reflectionClass, $classMetadata, $currentValue);
111✔
331

332
        $providerFields = self::getGraphQLFieldsFromProviders($reflectionClass, $isMutation ? Metadata\Mutation::class : Metadata\Query::class, $gqlName, $isDefault);
111✔
333
        $gqlConfiguration['config']['fields'] = array_merge($gqlConfiguration['config']['fields'], $providerFields);
111✔
334

335
        if ($classMetadata instanceof Metadata\Relay\Edge) {
111✔
336
            if (!$reflectionClass->implementsInterface(EdgeInterface::class)) {
106✔
337
                throw new InvalidArgumentException(sprintf('The metadata %s on class "%s" can only be used on class implementing the EdgeInterface.', self::formatMetadata('Edge'), $reflectionClass->getName()));
×
338
            }
339
            if (!isset($gqlConfiguration['config']['builders'])) {
106✔
340
                $gqlConfiguration['config']['builders'] = [];
106✔
341
            }
342
            array_unshift($gqlConfiguration['config']['builders'], ['builder' => 'relay-edge', 'builderConfig' => ['nodeType' => $classMetadata->node]]);
106✔
343
        }
344

345
        return $gqlConfiguration;
111✔
346
    }
347

348
    /**
349
     * @return array{type: 'relay-mutation-payload'|'object', config: array}
350
     */
351
    private static function graphQLTypeConfigFromAnnotation(ReflectionClass $reflectionClass, Metadata\Type $typeAnnotation, string $currentValue): array
352
    {
353
        $typeConfiguration = [];
111✔
354
        $metadatas = static::getMetadatas($reflectionClass);
111✔
355

356
        $fieldsFromProperties = self::getGraphQLTypeFieldsFromAnnotations($reflectionClass, self::getClassProperties($reflectionClass), Metadata\Field::class, $currentValue);
111✔
357
        $fieldsFromMethods = self::getGraphQLTypeFieldsFromAnnotations($reflectionClass, $reflectionClass->getMethods(), Metadata\Field::class, $currentValue);
111✔
358

359
        $typeConfiguration['fields'] = array_merge($fieldsFromProperties, $fieldsFromMethods);
111✔
360
        $typeConfiguration = self::getDescriptionConfiguration($metadatas) + $typeConfiguration;
111✔
361

362
        if (!empty($typeAnnotation->interfaces)) {
111✔
363
            $typeConfiguration['interfaces'] = $typeAnnotation->interfaces;
106✔
364
        } else {
365
            $interfaces = array_keys(self::$map->searchClassesMapBy(function ($gqlType, $configuration) use ($reflectionClass) {
111✔
366
                ['class' => $interfaceClassName] = $configuration;
109✔
367

368
                $interfaceMetadata = self::getClassReflection($interfaceClassName);
109✔
369
                if ($interfaceMetadata->isInterface() && $reflectionClass->implementsInterface($interfaceMetadata->getName())) {
109✔
370
                    return true;
106✔
371
                }
372

373
                return $reflectionClass->isSubclassOf($interfaceClassName);
109✔
374
            }, self::GQL_INTERFACE));
111✔
375

376
            sort($interfaces);
111✔
377
            $typeConfiguration['interfaces'] = $interfaces;
111✔
378
        }
379

380
        if (isset($typeAnnotation->resolveField)) {
111✔
381
            $typeConfiguration['resolveField'] = self::formatExpression($typeAnnotation->resolveField);
106✔
382
        }
383

384
        $buildersAnnotations = self::getMetadataMatching($metadatas, Metadata\FieldsBuilder::class);
111✔
385
        if (!empty($buildersAnnotations)) {
111✔
386
            $typeConfiguration['builders'] = array_map(fn ($fieldsBuilderAnnotation) => ['builder' => $fieldsBuilderAnnotation->name, 'builderConfig' => $fieldsBuilderAnnotation->config], $buildersAnnotations);
106✔
387
        }
388

389
        if (isset($typeAnnotation->isTypeOf)) {
111✔
390
            $typeConfiguration['isTypeOf'] = $typeAnnotation->isTypeOf;
106✔
391
        }
392

393
        $publicMetadata = self::getFirstMetadataMatching($metadatas, Metadata\IsPublic::class);
111✔
394
        if (null !== $publicMetadata) {
111✔
395
            $typeConfiguration['fieldsDefaultPublic'] = self::formatExpression($publicMetadata->value);
106✔
396
        }
397

398
        $accessMetadata = self::getFirstMetadataMatching($metadatas, Metadata\Access::class);
111✔
399
        if (null !== $accessMetadata) {
111✔
400
            $typeConfiguration['fieldsDefaultAccess'] = self::formatExpression($accessMetadata->value);
106✔
401
        }
402

403
        return ['type' => $typeAnnotation->isRelay ? 'relay-mutation-payload' : 'object', 'config' => $typeConfiguration];
111✔
404
    }
405

406
    /**
407
     * Create a GraphQL Interface type configuration from metadatas on properties.
408
     *
409
     * @return array{type: 'interface', config: array}
410
     */
411
    private static function typeInterfaceMetadataToGQLConfiguration(ReflectionClass $reflectionClass, Metadata\TypeInterface $interfaceAnnotation, string $gqlName): array
412
    {
413
        $interfaceConfiguration = [];
109✔
414

415
        $fieldsFromProperties = self::getGraphQLTypeFieldsFromAnnotations($reflectionClass, self::getClassProperties($reflectionClass));
109✔
416
        $fieldsFromMethods = self::getGraphQLTypeFieldsFromAnnotations($reflectionClass, $reflectionClass->getMethods());
109✔
417

418
        $interfaceConfiguration['fields'] = array_merge($fieldsFromProperties, $fieldsFromMethods);
109✔
419
        $interfaceConfiguration = self::getDescriptionConfiguration(static::getMetadatas($reflectionClass)) + $interfaceConfiguration;
109✔
420

421
        if (isset($interfaceAnnotation->resolveType)) {
109✔
422
            $interfaceConfiguration['resolveType'] = self::formatExpression($interfaceAnnotation->resolveType);
106✔
423
        } else {
424
            // Try to use default interface resolver type
425
            $interfaceConfiguration['resolveType'] = self::formatExpression(sprintf("service('overblog_graphql.interface_type_resolver').resolveType('%s', value)", $gqlName));
109✔
426
        }
427

428
        return ['type' => 'interface', 'config' => $interfaceConfiguration];
109✔
429
    }
430

431
    /**
432
     * Create a GraphQL Input type configuration from metadatas on properties.
433
     *
434
     * @return array{type: 'relay-mutation-input'|'input-object', config: array}
435
     */
436
    private static function inputMetadataToGQLConfiguration(ReflectionClass $reflectionClass, Metadata\Input $inputAnnotation): array
437
    {
438
        $inputConfiguration = array_merge([
106✔
439
            'fields' => self::getGraphQLInputFieldsFromMetadatas($reflectionClass, self::getClassProperties($reflectionClass)),
106✔
440
        ], self::getDescriptionConfiguration(static::getMetadatas($reflectionClass), true));
106✔
441

442
        return ['type' => $inputAnnotation->isRelay ? 'relay-mutation-input' : 'input-object', 'config' => $inputConfiguration];
106✔
443
    }
444

445
    /**
446
     * Get a GraphQL scalar configuration from given scalar metadata.
447
     *
448
     * @return array{type: 'custom-scalar', config: array}
449
     */
450
    private static function scalarMetadataToGQLConfiguration(ReflectionClass $reflectionClass, Metadata\Scalar $scalarAnnotation): array
451
    {
452
        $scalarConfiguration = [];
106✔
453

454
        if (isset($scalarAnnotation->scalarType)) {
106✔
455
            $scalarConfiguration['scalarType'] = self::formatExpression($scalarAnnotation->scalarType);
106✔
456
        } else {
457
            $scalarConfiguration = [
106✔
458
                'serialize' => [$reflectionClass->getName(), 'serialize'],
106✔
459
                'parseValue' => [$reflectionClass->getName(), 'parseValue'],
106✔
460
                'parseLiteral' => [$reflectionClass->getName(), 'parseLiteral'],
106✔
461
            ];
106✔
462
        }
463

464
        $scalarConfiguration = self::getDescriptionConfiguration(static::getMetadatas($reflectionClass)) + $scalarConfiguration;
106✔
465

466
        return ['type' => 'custom-scalar', 'config' => $scalarConfiguration];
106✔
467
    }
468

469
    /**
470
     * Get a GraphQL Enum configuration from given enum metadata.
471
     *
472
     * @return array{type: 'enum', config: array}
473
     */
474
    private static function enumMetadataToGQLConfiguration(ReflectionClass $reflectionClass, Metadata\Enum $enumMetadata): array
475
    {
476
        $metadatas = static::getMetadatas($reflectionClass);
106✔
477
        $enumValues = self::getMetadataMatching($metadatas, Metadata\EnumValue::class);
106✔
478
        $isPhpEnum = PHP_VERSION_ID >= 80100 && $reflectionClass->isEnum();
106✔
479
        $values = [];
106✔
480

481
        foreach ($reflectionClass->getConstants() as $name => $value) {
106✔
482
            $reflectionConstant = new ReflectionClassConstant($reflectionClass->getName(), $name);
106✔
483
            $valueConfig = self::getDescriptionConfiguration(static::getMetadatas($reflectionConstant), true);
106✔
484

485
            $enumValueAnnotation = current(array_filter($enumValues, fn ($enumValueAnnotation) => $enumValueAnnotation->name === $name));
106✔
486
            $valueConfig['value'] = $isPhpEnum ? $value->name : $value;
106✔
487

488
            if (false !== $enumValueAnnotation) {
106✔
489
                if (isset($enumValueAnnotation->description)) {
53✔
490
                    $valueConfig['description'] = $enumValueAnnotation->description;
53✔
491
                }
492

493
                if (isset($enumValueAnnotation->deprecationReason)) {
53✔
494
                    $valueConfig['deprecationReason'] = $enumValueAnnotation->deprecationReason;
53✔
495
                }
496
            }
497

498
            $values[$name] = $valueConfig;
106✔
499
        }
500

501
        $enumConfiguration = ['values' => $values];
106✔
502
        $enumConfiguration = self::getDescriptionConfiguration(static::getMetadatas($reflectionClass)) + $enumConfiguration;
106✔
503
        if ($isPhpEnum) {
106✔
504
            $enumConfiguration['enumClass'] = $reflectionClass->getName();
106✔
505
        }
506

507
        return ['type' => 'enum', 'config' => $enumConfiguration];
106✔
508
    }
509

510
    /**
511
     * Get a GraphQL Union configuration from given union metadata.
512
     *
513
     * @return array{type: 'union', config: array}
514
     */
515
    private static function unionMetadataToGQLConfiguration(ReflectionClass $reflectionClass, Metadata\Union $unionMetadata): array
516
    {
517
        $unionConfiguration = [];
106✔
518
        if (!empty($unionMetadata->types)) {
106✔
519
            $unionConfiguration['types'] = $unionMetadata->types;
106✔
520
        } else {
521
            $types = array_keys(self::$map->searchClassesMapBy(function ($gqlType, $configuration) use ($reflectionClass) {
106✔
522
                $typeClassName = $configuration['class'];
106✔
523
                $typeMetadata = self::getClassReflection($typeClassName);
106✔
524

525
                if ($reflectionClass->isInterface() && $typeMetadata->implementsInterface($reflectionClass->getName())) {
106✔
526
                    return true;
106✔
527
                }
528

529
                return $typeMetadata->isSubclassOf($reflectionClass->getName());
106✔
530
            }, self::GQL_TYPE));
106✔
531
            sort($types);
106✔
532
            $unionConfiguration['types'] = $types;
106✔
533
        }
534

535
        $unionConfiguration = self::getDescriptionConfiguration(static::getMetadatas($reflectionClass)) + $unionConfiguration;
106✔
536

537
        if (isset($unionMetadata->resolveType)) {
106✔
538
            $unionConfiguration['resolveType'] = self::formatExpression($unionMetadata->resolveType);
106✔
539
        } else {
540
            if ($reflectionClass->hasMethod('resolveType')) {
106✔
541
                $method = $reflectionClass->getMethod('resolveType');
106✔
542
                if ($method->isStatic() && $method->isPublic()) {
106✔
543
                    $unionConfiguration['resolveType'] = self::formatExpression(sprintf("@=call('%s::%s', [service('overblog_graphql.type_resolver'), value], true)", self::formatNamespaceForExpression($reflectionClass->getName()), 'resolveType'));
106✔
544
                } else {
UNCOV
545
                    throw new InvalidArgumentException(sprintf('The "resolveType()" method on class must be static and public. Or you must define a "resolveType" attribute on the %s metadata.', self::formatMetadata('Union')));
56✔
546
                }
547
            } else {
548
                throw new InvalidArgumentException(sprintf('The metadata %s has no "resolveType" attribute and the related class has no "resolveType()" public static method. You need to define of them.', self::formatMetadata('Union')));
4✔
549
            }
550
        }
551

552
        return ['type' => 'union', 'config' => $unionConfiguration];
106✔
553
    }
554

555
    /**
556
     * @phpstan-param ReflectionMethod|ReflectionProperty $reflector
557
     * @phpstan-param class-string<Metadata\Field> $fieldMetadataName
558
     *
559
     * @return array<string,array>
560
     *
561
     * @throws AnnotationException
562
     */
563
    private static function getTypeFieldConfigurationFromReflector(ReflectionClass $reflectionClass, Reflector $reflector, string $fieldMetadataName, string $currentValue = 'value'): array
564
    {
565
        /** @var ReflectionProperty|ReflectionMethod $reflector */
566
        $metadatas = static::getMetadatas($reflector);
109✔
567

568
        $fieldMetadata = self::getFirstMetadataMatching($metadatas, $fieldMetadataName);
109✔
569
        $accessMetadata = self::getFirstMetadataMatching($metadatas, Metadata\Access::class);
109✔
570
        $publicMetadata = self::getFirstMetadataMatching($metadatas, Metadata\IsPublic::class);
109✔
571

572
        if (null === $fieldMetadata) {
109✔
573
            if (null !== $accessMetadata || null !== $publicMetadata) {
106✔
574
                throw new InvalidArgumentException(sprintf('The metadatas %s and/or %s defined on "%s" are only usable in addition of metadata %s', self::formatMetadata('Access'), self::formatMetadata('Visible'), $reflector->getName(), self::formatMetadata('Field')));
4✔
575
            }
576

577
            return [];
106✔
578
        }
579

580
        if ($reflector instanceof ReflectionMethod && !$reflector->isPublic()) {
109✔
581
            throw new InvalidArgumentException(sprintf('The metadata %s can only be applied to public method. The method "%s" is not public.', self::formatMetadata('Field'), $reflector->getName()));
4✔
582
        }
583

584
        $fieldName = $reflector->getName();
109✔
585
        $fieldConfiguration = [];
109✔
586

587
        if (isset($fieldMetadata->type)) {
109✔
588
            $fieldConfiguration['type'] = $fieldMetadata->type;
109✔
589
        }
590

591
        $fieldConfiguration = self::getDescriptionConfiguration($metadatas, true) + $fieldConfiguration;
109✔
592

593
        $args = [];
109✔
594

595
        /** @var Metadata\Arg[] $argAnnotations */
596
        $argAnnotations = self::getMetadataMatching($metadatas, Metadata\Arg::class);
109✔
597

598
        $validArgNames = array_map(fn (ReflectionParameter $parameter) => $parameter->getName(), $reflector instanceof ReflectionMethod ? $reflector->getParameters() : []);
109✔
599

600
        foreach ($argAnnotations as $arg) {
109✔
601
            if ($reflector instanceof ReflectionMethod && !in_array($arg->name, $validArgNames, true)) {
106✔
602
                throw new InvalidArgumentException(sprintf('The argument "%s" defined with #[GQL\Arg] attribute/annotation on method "%s" does not match any parameter name in the method.', $arg->name, $reflector->getName()));
4✔
603
            }
604

605
            $args[$arg->name] = ['type' => $arg->type];
106✔
606

607
            if (isset($arg->description)) {
106✔
608
                $args[$arg->name]['description'] = $arg->description;
106✔
609
            }
610

611
            if (isset($arg->defaultValue)) {
106✔
612
                $args[$arg->name]['defaultValue'] = $arg->defaultValue;
106✔
613
            } elseif (isset($arg->default)) {
106✔
614
                @trigger_error(sprintf('%s %s %s', 'overblog/graphql-bundle', '1.3', 'The "default" attribute on @GQL\Arg or #GQL\Arg is deprecated, use "defaultValue" instead.'), E_USER_DEPRECATED);
×
615
                $args[$arg->name]['defaultValue'] = $arg->default;
×
616
            }
617
        }
618

619
        if ($reflector instanceof ReflectionMethod) {
109✔
620
            $args = self::guessArgs($reflectionClass, $reflector, $args);
109✔
621
        }
622

623
        $gqlArgs = array_filter($args, fn ($arg) => !isset($arg['internal']));
109✔
624
        if (!empty($gqlArgs)) {
109✔
625
            $fieldConfiguration['args'] = $gqlArgs;
106✔
626
        }
627

628
        $fieldName = $fieldMetadata->name ?? $fieldName;
109✔
629

630
        if (isset($fieldMetadata->resolve)) {
109✔
631
            $fieldConfiguration['resolve'] = self::formatExpression($fieldMetadata->resolve);
106✔
632
        } else {
633
            if ($reflector instanceof ReflectionMethod) {
109✔
634
                $fieldConfiguration['resolve'] = self::formatExpression(sprintf('call(%s.%s, %s)', $currentValue, $reflector->getName(), self::formatArgsForExpression($args)));
109✔
635
            } else {
636
                if ($fieldName !== $reflector->getName() || 'value' !== $currentValue) {
109✔
637
                    $fieldConfiguration['resolve'] = self::formatExpression(sprintf('%s.%s', $currentValue, $reflector->getName()));
3✔
638
                }
639
            }
640
        }
641

642
        $argsBuilder = self::getFirstMetadataMatching($metadatas, Metadata\ArgsBuilder::class);
109✔
643
        if ($argsBuilder) {
109✔
644
            $fieldConfiguration['argsBuilder'] = ['builder' => $argsBuilder->name, 'config' => $argsBuilder->config];
106✔
645
        }
646
        $fieldBuilder = self::getFirstMetadataMatching($metadatas, Metadata\FieldBuilder::class);
109✔
647
        if ($fieldBuilder) {
109✔
648
            $fieldConfiguration['builder'] = $fieldBuilder->name;
106✔
649
            $fieldConfiguration['builderConfig'] = $fieldBuilder->config;
106✔
650
        } else {
651
            if (!isset($fieldMetadata->type)) {
109✔
652
                try {
653
                    $fieldConfiguration['type'] = self::guessType($reflectionClass, $reflector, self::VALID_OUTPUT_TYPES);
109✔
654
                } catch (TypeGuessingException $e) {
8✔
655
                    $error = sprintf('The attribute "type" on %s is missing on %s "%s" and cannot be auto-guessed from the following type guessers:'."\n%s\n", static::formatMetadata($fieldMetadataName), $reflector instanceof ReflectionProperty ? 'property' : 'method', $reflector->getName(), $e->getMessage());
8✔
656

657
                    throw new InvalidArgumentException($error);
8✔
658
                }
659
            }
660
        }
661

662
        if ($accessMetadata) {
109✔
663
            $fieldConfiguration['access'] = self::formatExpression($accessMetadata->value);
106✔
664
        }
665

666
        if ($publicMetadata) {
109✔
667
            $fieldConfiguration['public'] = self::formatExpression($publicMetadata->value);
106✔
668
        }
669

670
        if (isset($fieldMetadata->complexity)) {
109✔
671
            $fieldConfiguration['complexity'] = self::formatExpression($fieldMetadata->complexity);
106✔
672
        }
673

674
        return [$fieldName => $fieldConfiguration];
109✔
675
    }
676

677
    /**
678
     * Create GraphQL input fields configuration based on metadatas.
679
     *
680
     * @param ReflectionProperty[] $reflectors
681
     *
682
     * @return array<string,array>
683
     *
684
     * @throws AnnotationException
685
     */
686
    private static function getGraphQLInputFieldsFromMetadatas(ReflectionClass $reflectionClass, array $reflectors): array
687
    {
688
        $fields = [];
106✔
689

690
        foreach ($reflectors as $reflector) {
106✔
691
            $metadatas = static::getMetadatas($reflector);
106✔
692

693
            /** @var Metadata\Field|null $fieldMetadata */
694
            $fieldMetadata = self::getFirstMetadataMatching($metadatas, Metadata\Field::class);
106✔
695
            $publicMetadata = self::getFirstMetadataMatching($metadatas, Metadata\IsPublic::class);
106✔
696

697
            // No field metadata found
698
            if (null === $fieldMetadata) {
106✔
699
                if (null !== $publicMetadata) {
106✔
700
                    throw new InvalidArgumentException(sprintf('The metadatas %s defined on "%s" are only usable in addition of metadata %s', self::formatMetadata('Visible'), $reflector->getName(), self::formatMetadata('Field')));
4✔
701
                }
702

703
                continue;
106✔
704
            }
705

706
            // Ignore field with resolver when the type is an Input
707
            if (isset($fieldMetadata->resolve)) {
106✔
708
                continue;
106✔
709
            }
710

711
            $fieldName = $reflector->getName();
106✔
712
            if (isset($fieldMetadata->type)) {
106✔
713
                $fieldType = $fieldMetadata->type;
106✔
714
            } else {
715
                try {
716
                    $fieldType = self::guessType($reflectionClass, $reflector, self::VALID_INPUT_TYPES);
106✔
717
                } catch (TypeGuessingException $e) {
×
718
                    throw new InvalidArgumentException(sprintf('The attribute "type" on %s is missing on property "%s" and cannot be auto-guessed from the following type guessers:'."\n%s\n", self::formatMetadata(Metadata\Field::class), $reflector->getName(), $e->getMessage()));
×
719
                }
720
            }
721
            $fieldConfiguration = [];
106✔
722
            if ($fieldType) {
106✔
723
                // Resolve a PHP class from a GraphQL type
724
                $resolvedType = self::$map->getType($fieldType);
106✔
725
                // We found a type but it is not allowed
726
                if (null !== $resolvedType && !in_array($resolvedType['type'], self::VALID_INPUT_TYPES)) {
106✔
727
                    throw new InvalidArgumentException(sprintf('The type "%s" on "%s" is a "%s" not valid on an Input %s. Only Input, Scalar and Enum are allowed.', $fieldType, $reflector->getName(), $resolvedType['type'], self::formatMetadata('Field')));
×
728
                }
729

730
                $fieldConfiguration['type'] = $fieldType;
106✔
731
            }
732

733
            if ($fieldMetadata instanceof InputField && null !== $fieldMetadata->defaultValue) {
106✔
734
                $fieldConfiguration['defaultValue'] = $fieldMetadata->defaultValue;
106✔
735
            } elseif ($reflector->hasDefaultValue() && null !== $reflector->getDefaultValue()) {
106✔
736
                $fieldConfiguration['defaultValue'] = $reflector->getDefaultValue();
106✔
737
            }
738

739
            if ($publicMetadata) {
106✔
740
                $fieldConfiguration['public'] = self::formatExpression($publicMetadata->value);
106✔
741
            }
742

743
            $fieldConfiguration = array_merge(self::getDescriptionConfiguration($metadatas, true), $fieldConfiguration);
106✔
744
            $fields[$fieldName] = $fieldConfiguration;
106✔
745
        }
746

747
        return $fields;
106✔
748
    }
749

750
    /**
751
     * Create GraphQL type fields configuration based on metadatas.
752
     *
753
     * @param ReflectionProperty[]|ReflectionMethod[] $reflectors
754
     *
755
     * @phpstan-param class-string<Metadata\Field> $fieldMetadataName
756
     *
757
     * @throws AnnotationException
758
     */
759
    private static function getGraphQLTypeFieldsFromAnnotations(ReflectionClass $reflectionClass, array $reflectors, string $fieldMetadataName = Metadata\Field::class, string $currentValue = 'value'): array
760
    {
761
        $fields = [];
111✔
762

763
        foreach ($reflectors as $reflector) {
111✔
764
            $fields = array_merge($fields, self::getTypeFieldConfigurationFromReflector($reflectionClass, $reflector, $fieldMetadataName, $currentValue));
109✔
765
        }
766

767
        return $fields;
111✔
768
    }
769

770
    /**
771
     * @phpstan-param class-string<Metadata\Query|Metadata\Mutation> $expectedMetadata
772
     *
773
     * Return fields config from Provider methods.
774
     * Loop through configured provider and extract fields targeting the targetType.
775
     *
776
     * @return array<string,array>
777
     */
778
    private static function getGraphQLFieldsFromProviders(ReflectionClass $reflectionClass, string $expectedMetadata, string $targetType, bool $isDefaultTarget = false): array
779
    {
780
        $fields = [];
111✔
781
        foreach (self::$providers as ['reflectionClass' => $providerReflection, 'metadata' => $providerMetadata]) {
111✔
782
            $defaultAccessAnnotation = self::getFirstMetadataMatching(static::getMetadatas($providerReflection), Metadata\Access::class);
109✔
783
            $defaultIsPublicAnnotation = self::getFirstMetadataMatching(static::getMetadatas($providerReflection), Metadata\IsPublic::class);
109✔
784

785
            $defaultAccess = $defaultAccessAnnotation ? self::formatExpression($defaultAccessAnnotation->value) : false;
109✔
786
            $defaultIsPublic = $defaultIsPublicAnnotation ? self::formatExpression($defaultIsPublicAnnotation->value) : false;
109✔
787

788
            $methods = [];
109✔
789
            // First found the methods matching the targeted type
790
            foreach ($providerReflection->getMethods() as $method) {
109✔
791
                $metadatas = static::getMetadatas($method);
109✔
792

793
                $metadata = self::getFirstMetadataMatching($metadatas, [Metadata\Mutation::class, Metadata\Query::class]);
109✔
794
                if (null === $metadata) {
109✔
795
                    continue;
×
796
                }
797

798
                // TODO: Remove old property check in 1.1
799
                $metadataTargets = $metadata->targetTypes ?? null;
109✔
800

801
                if (null === $metadataTargets) {
109✔
802
                    if ($metadata instanceof Metadata\Mutation && isset($providerMetadata->targetMutationTypes)) {
109✔
803
                        $metadataTargets = $providerMetadata->targetMutationTypes;
106✔
804
                    } elseif ($metadata instanceof Metadata\Query && isset($providerMetadata->targetQueryTypes)) {
109✔
805
                        $metadataTargets = $providerMetadata->targetQueryTypes;
106✔
806
                    }
807
                }
808

809
                if (null === $metadataTargets) {
109✔
810
                    if ($isDefaultTarget) {
109✔
811
                        $metadataTargets = [$targetType];
109✔
812
                        if (!$metadata instanceof $expectedMetadata) {
109✔
813
                            continue;
109✔
814
                        }
815
                    } else {
816
                        continue;
109✔
817
                    }
818
                }
819

820
                if (!in_array($targetType, $metadataTargets)) {
109✔
821
                    continue;
106✔
822
                }
823

824
                if (!$metadata instanceof $expectedMetadata) {
109✔
825
                    if (Metadata\Mutation::class === $expectedMetadata) {
8✔
826
                        $message = sprintf('The provider "%s" try to add a query field on type "%s" (through %s on method "%s") but "%s" is a mutation.', $providerReflection->getName(), $targetType, self::formatMetadata('Query'), $method->getName(), $targetType);
4✔
827
                    } else {
828
                        $message = sprintf('The provider "%s" try to add a mutation on type "%s" (through %s on method "%s") but "%s" is not a mutation.', $providerReflection->getName(), $targetType, self::formatMetadata('Mutation'), $method->getName(), $targetType);
4✔
829
                    }
830

831
                    throw new InvalidArgumentException($message);
8✔
832
                }
833
                $methods[$method->getName()] = $method;
109✔
834
            }
835

836
            $currentValue = sprintf("service('%s')", self::formatNamespaceForExpression($providerReflection->getName()));
109✔
837
            $providerFields = self::getGraphQLTypeFieldsFromAnnotations($reflectionClass, $methods, $expectedMetadata, $currentValue);
109✔
838
            foreach ($providerFields as $fieldName => $fieldConfig) {
109✔
839
                if (isset($providerMetadata->prefix)) {
109✔
840
                    $fieldName = sprintf('%s%s', $providerMetadata->prefix, $fieldName);
106✔
841
                }
842

843
                if ($defaultAccess && !isset($fieldConfig['access'])) {
109✔
844
                    $fieldConfig['access'] = $defaultAccess;
106✔
845
                }
846

847
                if ($defaultIsPublic && !isset($fieldConfig['public'])) {
109✔
848
                    $fieldConfig['public'] = $defaultIsPublic;
106✔
849
                }
850

851
                $fields[$fieldName] = $fieldConfig;
109✔
852
            }
853
        }
854

855
        return $fields;
111✔
856
    }
857

858
    /**
859
     * Get the config for description & deprecation reason.
860
     *
861
     * @return array<'description'|'deprecationReason',string>
862
     */
863
    private static function getDescriptionConfiguration(array $metadatas, bool $withDeprecation = false): array
864
    {
865
        $config = [];
111✔
866
        $descriptionAnnotation = self::getFirstMetadataMatching($metadatas, Metadata\Description::class);
111✔
867
        if (null !== $descriptionAnnotation) {
111✔
868
            $config['description'] = $descriptionAnnotation->value;
106✔
869
        }
870

871
        if ($withDeprecation) {
111✔
872
            $deprecatedAnnotation = self::getFirstMetadataMatching($metadatas, Metadata\Deprecated::class);
109✔
873
            if (null !== $deprecatedAnnotation) {
109✔
874
                $config['deprecationReason'] = $deprecatedAnnotation->value;
106✔
875
            }
876
        }
877

878
        return $config;
111✔
879
    }
880

881
    /**
882
     * Format an array of args to a list of arguments in an expression.
883
     */
884
    private static function formatArgsForExpression(array $args): string
885
    {
886
        $mapping = [];
109✔
887
        foreach ($args as $name => $config) {
109✔
888
            $mapping[] = sprintf('%s: "%s"', $name, $config['type']);
106✔
889
        }
890

891
        return sprintf('arguments({%s}, args)', implode(', ', $mapping));
109✔
892
    }
893

894
    /**
895
     * Format a namespace to be used in an expression (double escape).
896
     */
897
    private static function formatNamespaceForExpression(string $namespace): string
898
    {
899
        return str_replace('\\', '\\\\', $namespace);
109✔
900
    }
901

902
    /**
903
     * Get the first metadata matching given class.
904
     *
905
     * @phpstan-template T of object
906
     *
907
     * @phpstan-param class-string<T>|class-string<T>[] $metadataClasses
908
     *
909
     * @return object|null
910
     *
911
     * @phpstan-return T|null
912
     */
913
    private static function getFirstMetadataMatching(array $metadatas, $metadataClasses)
914
    {
915
        $metas = self::getMetadataMatching($metadatas, $metadataClasses);
111✔
916

917
        return array_shift($metas);
111✔
918
    }
919

920
    /**
921
     * Return the metadata matching given class
922
     *
923
     * @phpstan-template T of object
924
     *
925
     * @phpstan-param class-string<T>|class-string<T>[] $metadataClasses
926
     *
927
     * @return array
928
     */
929
    private static function getMetadataMatching(array $metadatas, $metadataClasses)
930
    {
931
        if (is_string($metadataClasses)) {
111✔
932
            $metadataClasses = [$metadataClasses];
111✔
933
        }
934

935
        return array_values(array_filter($metadatas, function ($metadata) use ($metadataClasses) {
111✔
936
            foreach ($metadataClasses as $metadataClass) {
111✔
937
                if ($metadata instanceof $metadataClass) {
111✔
938
                    return true;
109✔
939
                }
940
            }
941

942
            return false;
111✔
943
        }));
111✔
944
    }
945

946
    /**
947
     * Format an expression (ie. add "@=" if not set).
948
     */
949
    private static function formatExpression(string $expression): string
950
    {
951
        return '@=' === substr($expression, 0, 2) ? $expression : sprintf('@=%s', $expression);
109✔
952
    }
953

954
    /**
955
     * Suffix a name if it is not already.
956
     */
957
    private static function suffixName(string $name, string $suffix): string
958
    {
959
        return substr($name, -strlen($suffix)) === $suffix ? $name : sprintf('%s%s', $name, $suffix);
106✔
960
    }
961

962
    /**
963
     * Try to guess a GraphQL type using configured type guessers
964
     *
965
     * @throws RuntimeException
966
     */
967
    private static function guessType(ReflectionClass $reflectionClass, Reflector $reflector, array $filterGraphQLTypes = []): string
968
    {
969
        $errors = [];
109✔
970
        foreach (self::$typeGuessers as $typeGuesser) {
109✔
971
            if (!$typeGuesser->supports($reflector)) {
109✔
972
                continue;
106✔
973
            }
974
            try {
975
                $type = $typeGuesser->guessType($reflectionClass, $reflector, $filterGraphQLTypes);
109✔
976

977
                return $type;
109✔
978
            } catch (TypeGuessingException $exception) {
109✔
979
                $errors[] = sprintf('[%s] %s', $typeGuesser->getName(), $exception->getMessage());
109✔
980
            }
981
        }
982

983
        throw new TypeGuessingException(implode("\n", $errors));
12✔
984
    }
985

986
    /**
987
     * Transform a method arguments from reflection to a list of GraphQL argument.
988
     */
989
    private static function guessArgs(
990
        ReflectionClass $reflectionClass,
991
        ReflectionMethod $method,
992
        array $currentArguments,
993
    ): array {
994
        $arguments = [];
109✔
995
        foreach ($method->getParameters() as $index => $parameter) {
109✔
996
            if (array_key_exists($parameter->getName(), $currentArguments)) {
106✔
997
                $arguments[$parameter->getName()] = $currentArguments[$parameter->getName()];
106✔
998
                continue;
106✔
999
            }
1000

1001
            if ($parameter->getType() instanceof ReflectionNamedType) {
106✔
1002
                $className = $parameter->getType()->getName();
106✔
1003
                if (ResolveInfo::class === $className || is_subclass_of($className, ResolveInfo::class)) {
106✔
1004
                    $arguments[$parameter->getName()] = ['type' => ArgumentsTransformer::RESOLVE_INFO_TOKEN, 'internal' => true];
106✔
1005
                    continue;
106✔
1006
                }
1007
            }
1008

1009
            try {
1010
                $gqlType = self::guessType($reflectionClass, $parameter, self::VALID_INPUT_TYPES);
106✔
1011
            } catch (TypeGuessingException $exception) {
4✔
1012
                throw new InvalidArgumentException(sprintf('Argument n°%s "$%s" on method "%s" cannot be auto-guessed from the following type guessers:'."\n%s\n", $index + 1, $parameter->getName(), $method->getName(), $exception->getMessage()));
4✔
1013
            }
1014

1015
            $argumentConfig = [];
106✔
1016
            if ($parameter->isDefaultValueAvailable()) {
106✔
1017
                $argumentConfig['defaultValue'] = $parameter->getDefaultValue();
106✔
1018
            }
1019

1020
            $argumentConfig['type'] = $gqlType;
106✔
1021

1022
            $arguments[$parameter->getName()] = $argumentConfig;
106✔
1023
        }
1024

1025
        return $arguments;
109✔
1026
    }
1027

1028
    /**
1029
     * @return ReflectionProperty[]
1030
     */
1031
    private static function getClassProperties(ReflectionClass $reflectionClass): array
1032
    {
1033
        $properties = [];
111✔
1034
        do {
1035
            foreach ($reflectionClass->getProperties() as $property) {
111✔
1036
                if (isset($properties[$property->getName()])) {
109✔
1037
                    continue;
109✔
1038
                }
1039
                $properties[$property->getName()] = $property;
109✔
1040
            }
1041
        } while ($reflectionClass = $reflectionClass->getParentClass());
111✔
1042

1043
        return $properties;
111✔
1044
    }
1045

1046
    protected static function formatMetadata(string $className): string
1047
    {
1048
        return sprintf(static::METADATA_FORMAT, str_replace(self::ANNOTATION_NAMESPACE, '', $className));
32✔
1049
    }
1050

1051
    abstract protected static function getMetadatas(Reflector $reflector): array;
1052
}
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