Scrutinizer GitHub App not installed

We could not synchronize checks via GitHub's checks API since Scrutinizer's GitHub App is not installed for this repository.

Install GitHub App

Passed
Pull Request — master (#751)
by Bart
08:07
created

AnnotationParser::preParse()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 3
crap 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Overblog\GraphQLBundle\Config\Parser;
6
7
use Doctrine\Common\Annotations\AnnotationException;
8
use Doctrine\ORM\Mapping\Column;
9
use Doctrine\ORM\Mapping\JoinColumn;
10
use Doctrine\ORM\Mapping\ManyToMany;
11
use Doctrine\ORM\Mapping\ManyToOne;
12
use Doctrine\ORM\Mapping\OneToMany;
13
use Doctrine\ORM\Mapping\OneToOne;
14
use Exception;
15
use Overblog\GraphQLBundle\Annotation as GQL;
16
use Overblog\GraphQLBundle\Config\Parser\Annotation\GraphClass;
17
use Overblog\GraphQLBundle\Relay\Connection\ConnectionInterface;
18
use Overblog\GraphQLBundle\Relay\Connection\EdgeInterface;
19
use ReflectionException;
20
use ReflectionMethod;
21
use ReflectionNamedType;
22
use ReflectionProperty;
23
use Reflector;
24
use RuntimeException;
25
use SplFileInfo;
26
use Symfony\Component\Config\Resource\FileResource;
27
use Symfony\Component\DependencyInjection\ContainerBuilder;
28
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
29
use function array_filter;
30
use function array_keys;
31
use function array_map;
32
use function array_unshift;
33
use function current;
34
use function file_get_contents;
35
use function get_class;
36
use function implode;
37
use function in_array;
38
use function is_array;
39
use function is_string;
40
use function preg_match;
41
use function sprintf;
42
use function str_replace;
43
use function strlen;
44
use function strpos;
45
use function substr;
46
use function trim;
47
48
class AnnotationParser implements PreParserInterface
49
{
50
    private static array $classesMap = [];
51
    private static array $providers = [];
52
    private static array $doctrineMapping = [];
53
    private static array $graphClassCache = [];
54
55
    private const GQL_SCALAR = 'scalar';
56
    private const GQL_ENUM = 'enum';
57
    private const GQL_TYPE = 'type';
58
    private const GQL_INPUT = 'input';
59
    private const GQL_UNION = 'union';
60
    private const GQL_INTERFACE = 'interface';
61
62
    /**
63
     * @see https://facebook.github.io/graphql/draft/#sec-Input-and-Output-Types
64
     */
65
    private const VALID_INPUT_TYPES = [self::GQL_SCALAR, self::GQL_ENUM, self::GQL_INPUT];
66
    private const VALID_OUTPUT_TYPES = [self::GQL_SCALAR, self::GQL_TYPE, self::GQL_INTERFACE, self::GQL_UNION, self::GQL_ENUM];
67
68
    /**
69
     * {@inheritdoc}
70
     *
71
     * @throws InvalidArgumentException
72
     * @throws ReflectionException
73
     */
74 26
    public static function preParse(SplFileInfo $file, ContainerBuilder $container, array $configs = []): void
75
    {
76 26
        $container->setParameter('overblog_graphql_types.classes_map', self::processFile($file, $container, $configs, true));
77 26
    }
78
79
    /**
80
     * @throws InvalidArgumentException
81
     * @throws ReflectionException
82
     */
83 26
    public static function parse(SplFileInfo $file, ContainerBuilder $container, array $configs = []): array
84
    {
85 26
        return self::processFile($file, $container, $configs, false);
86
    }
87
88
    /**
89
     * @internal
90
     */
91 64
    public static function reset(): void
92
    {
93 64
        self::$classesMap = [];
94 64
        self::$providers = [];
95 64
        self::$graphClassCache = [];
96 64
        self::$doctrineMapping = [];
97 64
    }
98
99
    /**
100
     * Process a file.
101
     *
102
     * @throws InvalidArgumentException|ReflectionException|AnnotationException
103
     */
104 26
    private static function processFile(SplFileInfo $file, ContainerBuilder $container, array $configs, bool $preProcess): array
105
    {
106 26
        self::$doctrineMapping += $configs['doctrine']['types_mapping'];
107 26
        $container->addResource(new FileResource($file->getRealPath()));
108
109
        try {
110 26
            $className = $file->getBasename('.php');
111 26
            if (preg_match('#namespace (.+);#', file_get_contents($file->getRealPath()), $matches)) {
112 26
                $className = trim($matches[1]).'\\'.$className;
113
            }
114
115 26
            $gqlTypes = [];
116 26
            $graphClass = self::getGraphClass($className);
117
118 26
            foreach ($graphClass->getAnnotations() as $classAnnotation) {
119 26
                $gqlTypes = self::classAnnotationsToGQLConfiguration(
120 26
                    $graphClass,
121
                    $classAnnotation,
122
                    $configs,
123
                    $gqlTypes,
124
                    $preProcess
125
                );
126
            }
127
128 26
            return $preProcess ? self::$classesMap : $gqlTypes;
129 10
        } catch (\InvalidArgumentException $e) {
130 10
            throw new InvalidArgumentException(sprintf('Failed to parse GraphQL annotations from file "%s".', $file), $e->getCode(), $e);
131
        }
132
    }
133
134 26
    private static function classAnnotationsToGQLConfiguration(
135
        GraphClass $graphClass,
136
        object $classAnnotation,
137
        array $configs,
138
        array $gqlTypes,
139
        bool $preProcess
140
    ): array {
141 26
        $gqlConfiguration = $gqlType = $gqlName = null;
142
143
        switch (true) {
144 26
            case $classAnnotation instanceof GQL\Type:
145 26
                $gqlType = self::GQL_TYPE;
146 26
                $gqlName = $classAnnotation->name ?? $graphClass->getShortName();
147 26
                if (!$preProcess) {
148 26
                    $gqlConfiguration = self::typeAnnotationToGQLConfiguration($graphClass, $classAnnotation, $gqlName, $configs);
149
150 26
                    if ($classAnnotation instanceof GQL\Relay\Connection) {
151 25
                        if (!$graphClass->implementsInterface(ConnectionInterface::class)) {
152
                            throw new InvalidArgumentException(sprintf('The annotation @Connection on class "%s" can only be used on class implementing the ConnectionInterface.', $graphClass->getName()));
153
                        }
154
155 25
                        if (!(isset($classAnnotation->edge) xor isset($classAnnotation->node))) {
156
                            throw new InvalidArgumentException(sprintf('The annotation @Connection on class "%s" is invalid. You must define either the "edge" OR the "node" attribute, but not both.', $graphClass->getName()));
157
                        }
158
159 25
                        $edgeType = $classAnnotation->edge ?? false;
160 25
                        if (!$edgeType) {
161 25
                            $edgeType = $gqlName.'Edge';
162 25
                            $gqlTypes[$edgeType] = [
163 25
                                'type' => 'object',
164
                                'config' => [
165
                                    'builders' => [
166 25
                                        ['builder' => 'relay-edge', 'builderConfig' => ['nodeType' => $classAnnotation->node]],
167
                                    ],
168
                                ],
169
                            ];
170
                        }
171
172 25
                        if (!isset($gqlConfiguration['config']['builders'])) {
173 25
                            $gqlConfiguration['config']['builders'] = [];
174
                        }
175
176 25
                        array_unshift($gqlConfiguration['config']['builders'], ['builder' => 'relay-connection', 'builderConfig' => ['edgeType' => $edgeType]]);
177
                    }
178
                }
179 26
                break;
180
181 25
            case $classAnnotation instanceof GQL\Input:
182 25
                $gqlType = self::GQL_INPUT;
183 25
                $gqlName = $classAnnotation->name ?? self::suffixName($graphClass->getShortName(), 'Input');
184 25
                if (!$preProcess) {
185 25
                    $gqlConfiguration = self::inputAnnotationToGQLConfiguration($graphClass, $classAnnotation);
186
                }
187 25
                break;
188
189 25
            case $classAnnotation instanceof GQL\Scalar:
190 25
                $gqlType = self::GQL_SCALAR;
191 25
                if (!$preProcess) {
192 25
                    $gqlConfiguration = self::scalarAnnotationToGQLConfiguration($graphClass, $classAnnotation);
193
                } else {
194 25
                    if (isset($classAnnotation->doctrineType)) {
195 25
                        $gqlName = $classAnnotation->name ?? $graphClass->getShortName();
196 25
                        self::$doctrineMapping[$classAnnotation->doctrineType] = $gqlName;
197
                    }
198 25
                    if (isset($classAnnotation->phpType)) {
199 25
                        self::$classesMap[$gqlName] = ['type' => $gqlType, 'class' => $classAnnotation->phpType];
200
201 25
                        return $gqlTypes;
202
                    }
203
                }
204 25
                break;
205
206 25
            case $classAnnotation instanceof GQL\Enum:
207 25
                $gqlType = self::GQL_ENUM;
208 25
                if (!$preProcess) {
209 25
                    $gqlConfiguration = self::enumAnnotationToGQLConfiguration($graphClass, $classAnnotation);
210
                }
211 25
                break;
212
213 25
            case $classAnnotation instanceof GQL\Union:
214 25
                $gqlType = self::GQL_UNION;
215 25
                if (!$preProcess) {
216 25
                    $gqlConfiguration = self::unionAnnotationToGQLConfiguration($graphClass, $classAnnotation);
217
                }
218 25
                break;
219
220 25
            case $classAnnotation instanceof GQL\TypeInterface:
221 25
                $gqlType = self::GQL_INTERFACE;
222 25
                if (!$preProcess) {
223 25
                    $gqlConfiguration = self::typeInterfaceAnnotationToGQLConfiguration($graphClass, $classAnnotation);
224
                }
225 25
                break;
226
227 25
            case $classAnnotation instanceof GQL\Provider:
228 25
                if ($preProcess) {
229 25
                    self::$providers[] = ['metadata' => $graphClass, 'annotation' => $classAnnotation];
230
                }
231
232 25
                return [];
233
        }
234
235 26
        if (null !== $gqlType) {
236 26
            if (!$gqlName) {
237 25
                $gqlName = isset($classAnnotation->name) ? $classAnnotation->name : $graphClass->getShortName();
238
            }
239
240 26
            if ($preProcess) {
241 26
                if (isset(self::$classesMap[$gqlName])) {
242 1
                    throw new InvalidArgumentException(sprintf('The GraphQL type "%s" has already been registered in class "%s"', $gqlName, self::$classesMap[$gqlName]['class']));
243
                }
244 26
                self::$classesMap[$gqlName] = ['type' => $gqlType, 'class' => $graphClass->getName()];
245
            } else {
246 26
                $gqlTypes = [$gqlName => $gqlConfiguration] + $gqlTypes;
247
            }
248
        }
249
250 26
        return $gqlTypes;
251
    }
252
253
    /**
254
     * @throws ReflectionException
255
     */
256 26
    private static function getGraphClass(string $className): GraphClass
257
    {
258 26
        self::$graphClassCache[$className] ??= new GraphClass($className);
259
260 26
        return self::$graphClassCache[$className];
261
    }
262
263 26
    private static function typeAnnotationToGQLConfiguration(
264
        GraphClass $graphClass,
265
        GQL\Type $classAnnotation,
266
        string $gqlName,
267
        array $configs
268
    ): array {
269 26
        $isMutation = $isDefault = $isRoot = false;
270 26
        if (isset($configs['definitions']['schema'])) {
271 25
            $defaultSchemaName = isset($configs['definitions']['schema']['default']) ? 'default' : array_key_first($configs['definitions']['schema']);
272 25
            foreach ($configs['definitions']['schema'] as $schemaName => $schema) {
273 25
                $schemaQuery = $schema['query'] ?? null;
274 25
                $schemaMutation = $schema['mutation'] ?? null;
275
276 25
                if ($gqlName === $schemaQuery) {
277 25
                    $isRoot = true;
278 25
                    if ($defaultSchemaName === $schemaName) {
279 25
                        $isDefault = true;
280
                    }
281 25
                } elseif ($gqlName === $schemaMutation) {
282 25
                    $isMutation = true;
283 25
                    $isRoot = true;
284 25
                    if ($defaultSchemaName === $schemaName) {
285 25
                        $isDefault = true;
286
                    }
287
                }
288
            }
289
        }
290
291 26
        $currentValue = $isRoot ? sprintf("service('%s')", self::formatNamespaceForExpression($graphClass->getName())) : 'value';
292
293 26
        $gqlConfiguration = self::graphQLTypeConfigFromAnnotation($graphClass, $classAnnotation, $currentValue);
294
295 26
        $providerFields = self::getGraphQLFieldsFromProviders($graphClass, $isMutation ? GQL\Mutation::class : GQL\Query::class, $gqlName, $isDefault);
296 26
        $gqlConfiguration['config']['fields'] = array_merge($gqlConfiguration['config']['fields'], $providerFields);
297
298 26
        if ($classAnnotation instanceof GQL\Relay\Edge) {
299 25
            if (!$graphClass->implementsInterface(EdgeInterface::class)) {
300
                throw new InvalidArgumentException(sprintf('The annotation @Edge on class "%s" can only be used on class implementing the EdgeInterface.', $graphClass->getName()));
301
            }
302 25
            if (!isset($gqlConfiguration['config']['builders'])) {
303 25
                $gqlConfiguration['config']['builders'] = [];
304
            }
305 25
            array_unshift($gqlConfiguration['config']['builders'], ['builder' => 'relay-edge', 'builderConfig' => ['nodeType' => $classAnnotation->node]]);
306
        }
307
308 26
        return $gqlConfiguration;
309
    }
310
311 26
    private static function graphQLTypeConfigFromAnnotation(GraphClass $graphClass, GQL\Type $typeAnnotation, string $currentValue): array
312
    {
313 26
        $typeConfiguration = [];
314 26
        $fieldsFromProperties = self::getGraphQLTypeFieldsFromAnnotations($graphClass, $graphClass->getPropertiesExtended(), GQL\Field::class, $currentValue);
315 26
        $fieldsFromMethods = self::getGraphQLTypeFieldsFromAnnotations($graphClass, $graphClass->getMethods(), GQL\Field::class, $currentValue);
316
317 26
        $typeConfiguration['fields'] = array_merge($fieldsFromProperties, $fieldsFromMethods);
318 26
        $typeConfiguration = self::getDescriptionConfiguration($graphClass->getAnnotations()) + $typeConfiguration;
319
320 26
        if (isset($typeAnnotation->interfaces)) {
321 25
            $typeConfiguration['interfaces'] = $typeAnnotation->interfaces;
322
        } else {
323 26
            $interfaces = array_keys(self::searchClassesMapBy(function ($gqlType, $configuration) use ($graphClass) {
324 25
                ['class' => $interfaceClassName] = $configuration;
325
326 25
                $interfaceMetadata = self::getGraphClass($interfaceClassName);
327 25
                if ($interfaceMetadata->isInterface() && $graphClass->implementsInterface($interfaceMetadata->getName())) {
328 25
                    return true;
329
                }
330
331 25
                return $graphClass->isSubclassOf($interfaceClassName);
332 26
            }, self::GQL_INTERFACE));
333
334 26
            sort($interfaces);
335 26
            $typeConfiguration['interfaces'] = $interfaces;
336
        }
337
338 26
        if (isset($typeAnnotation->resolveField)) {
339 25
            $typeConfiguration['resolveField'] = self::formatExpression($typeAnnotation->resolveField);
340
        }
341
342 26
        if (isset($typeAnnotation->builders) && !empty($typeAnnotation->builders)) {
343 25
            $typeConfiguration['builders'] = array_map(function ($fieldsBuilderAnnotation) {
344 25
                return ['builder' => $fieldsBuilderAnnotation->builder, 'builderConfig' => $fieldsBuilderAnnotation->builderConfig];
345 25
            }, $typeAnnotation->builders);
346
        }
347
348 26
        if (isset($typeAnnotation->isTypeOf)) {
349 25
            $typeConfiguration['isTypeOf'] = $typeAnnotation->isTypeOf;
350
        }
351
352 26
        $publicAnnotation = self::getFirstAnnotationMatching($graphClass->getAnnotations(), GQL\IsPublic::class);
353 26
        if (null !== $publicAnnotation) {
354 25
            $typeConfiguration['fieldsDefaultPublic'] = self::formatExpression($publicAnnotation->value);
355
        }
356
357 26
        $accessAnnotation = self::getFirstAnnotationMatching($graphClass->getAnnotations(), GQL\Access::class);
358 26
        if (null !== $accessAnnotation) {
359 25
            $typeConfiguration['fieldsDefaultAccess'] = self::formatExpression($accessAnnotation->value);
360
        }
361
362 26
        return ['type' => $typeAnnotation->isRelay ? 'relay-mutation-payload' : 'object', 'config' => $typeConfiguration];
363
    }
364
365
    /**
366
     * Create a GraphQL Interface type configuration from annotations on properties.
367
     */
368 25
    private static function typeInterfaceAnnotationToGQLConfiguration(GraphClass $graphClass, GQL\TypeInterface $interfaceAnnotation): array
369
    {
370 25
        $interfaceConfiguration = [];
371
372 25
        $fieldsFromProperties = self::getGraphQLTypeFieldsFromAnnotations($graphClass, $graphClass->getPropertiesExtended());
373 25
        $fieldsFromMethods = self::getGraphQLTypeFieldsFromAnnotations($graphClass, $graphClass->getMethods());
374
375 25
        $interfaceConfiguration['fields'] = array_merge($fieldsFromProperties, $fieldsFromMethods);
376 25
        $interfaceConfiguration = self::getDescriptionConfiguration($graphClass->getAnnotations()) + $interfaceConfiguration;
377
378 25
        $interfaceConfiguration['resolveType'] = self::formatExpression($interfaceAnnotation->resolveType);
379
380 25
        return ['type' => 'interface', 'config' => $interfaceConfiguration];
381
    }
382
383
    /**
384
     * Create a GraphQL Input type configuration from annotations on properties.
385
     */
386 25
    private static function inputAnnotationToGQLConfiguration(GraphClass $graphClass, GQL\Input $inputAnnotation): array
387
    {
388 25
        $inputConfiguration = array_merge([
389 25
            'fields' => self::getGraphQLInputFieldsFromAnnotations($graphClass, $graphClass->getPropertiesExtended()),
390 25
        ], self::getDescriptionConfiguration($graphClass->getAnnotations()));
391
392 25
        return ['type' => $inputAnnotation->isRelay ? 'relay-mutation-input' : 'input-object', 'config' => $inputConfiguration];
393
    }
394
395
    /**
396
     * Get a GraphQL scalar configuration from given scalar annotation.
397
     */
398 25
    private static function scalarAnnotationToGQLConfiguration(GraphClass $graphClass, GQL\Scalar $scalarAnnotation): array
399
    {
400 25
        $scalarConfiguration = [];
401
402 25
        if (isset($scalarAnnotation->scalarType)) {
403 25
            $scalarConfiguration['scalarType'] = self::formatExpression($scalarAnnotation->scalarType);
404
        } else {
405
            $scalarConfiguration = [
406 25
                'serialize' => [$graphClass->getName(), 'serialize'],
407 25
                'parseValue' => [$graphClass->getName(), 'parseValue'],
408 25
                'parseLiteral' => [$graphClass->getName(), 'parseLiteral'],
409
            ];
410
        }
411
412 25
        $scalarConfiguration = self::getDescriptionConfiguration($graphClass->getAnnotations()) + $scalarConfiguration;
413
414 25
        return ['type' => 'custom-scalar', 'config' => $scalarConfiguration];
415
    }
416
417
    /**
418
     * Get a GraphQL Enum configuration from given enum annotation.
419
     */
420 25
    private static function enumAnnotationToGQLConfiguration(GraphClass $graphClass, GQL\Enum $enumAnnotation): array
421
    {
422 25
        $enumValues = $enumAnnotation->values ?? [];
423
424 25
        $values = [];
425
426 25
        foreach ($graphClass->getConstants() as $name => $value) {
427 25
            $valueAnnotation = current(array_filter($enumValues, fn ($enumValueAnnotation) => $enumValueAnnotation->name == $name));
428 25
            $valueConfig = [];
429 25
            $valueConfig['value'] = $value;
430
431 25
            if ($valueAnnotation && isset($valueAnnotation->description)) {
432 25
                $valueConfig['description'] = $valueAnnotation->description;
433
            }
434
435 25
            if ($valueAnnotation && isset($valueAnnotation->deprecationReason)) {
436 25
                $valueConfig['deprecationReason'] = $valueAnnotation->deprecationReason;
437
            }
438
439 25
            $values[$name] = $valueConfig;
440
        }
441
442 25
        $enumConfiguration = ['values' => $values];
443 25
        $enumConfiguration = self::getDescriptionConfiguration($graphClass->getAnnotations()) + $enumConfiguration;
444
445 25
        return ['type' => 'enum', 'config' => $enumConfiguration];
446
    }
447
448
    /**
449
     * Get a GraphQL Union configuration from given union annotation.
450
     */
451 25
    private static function unionAnnotationToGQLConfiguration(GraphClass $graphClass, GQL\Union $unionAnnotation): array
452
    {
453 25
        $unionConfiguration = [];
454 25
        if (isset($unionAnnotation->types)) {
455 25
            $unionConfiguration['types'] = $unionAnnotation->types;
456
        } else {
457 25
            $types = array_keys(self::searchClassesMapBy(function ($gqlType, $configuration) use ($graphClass) {
458 25
                $typeClassName = $configuration['class'];
459 25
                $typeMetadata = self::getGraphClass($typeClassName);
460
461 25
                if ($graphClass->isInterface() && $typeMetadata->implementsInterface($graphClass->getName())) {
462 25
                    return true;
463
                }
464
465 25
                return $typeMetadata->isSubclassOf($graphClass->getName());
466 25
            }, self::GQL_TYPE));
467 25
            sort($types);
468 25
            $unionConfiguration['types'] = $types;
469
        }
470
471 25
        $unionConfiguration = self::getDescriptionConfiguration($graphClass->getAnnotations()) + $unionConfiguration;
472
473 25
        if (isset($unionAnnotation->resolveType)) {
474 25
            $unionConfiguration['resolveType'] = self::formatExpression($unionAnnotation->resolveType);
475
        } else {
476 25
            if ($graphClass->hasMethod('resolveType')) {
477 25
                $method = $graphClass->getMethod('resolveType');
478 25
                if ($method->isStatic() && $method->isPublic()) {
479 25
                    $unionConfiguration['resolveType'] = self::formatExpression(sprintf("@=call('%s::%s', [service('overblog_graphql.type_resolver'), value], true)", self::formatNamespaceForExpression($graphClass->getName()), 'resolveType'));
480
                } else {
481 25
                    throw new InvalidArgumentException(sprintf('The "resolveType()" method on class must be static and public. Or you must define a "resolveType" attribute on the @Union annotation.'));
482
                }
483
            } else {
484 1
                throw new InvalidArgumentException(sprintf('The annotation @Union has no "resolveType" attribute and the related class has no "resolveType()" public static method. You need to define of them.'));
485
            }
486
        }
487
488 25
        return ['type' => 'union', 'config' => $unionConfiguration];
489
    }
490
491
    /**
492
     * @phpstan-param ReflectionMethod|ReflectionProperty $reflector
493
     * @phpstan-param class-string<GQL\Field> $fieldAnnotationName
494
     *
495
     * @throws AnnotationException
496
     */
497 25
    private static function getTypeFieldConfigurationFromReflector(GraphClass $graphClass, Reflector $reflector, string $fieldAnnotationName, string $currentValue = 'value'): array
498
    {
499 25
        $annotations = $graphClass->getAnnotations($reflector);
500
501 25
        $fieldAnnotation = self::getFirstAnnotationMatching($annotations, $fieldAnnotationName);
502 25
        $accessAnnotation = self::getFirstAnnotationMatching($annotations, GQL\Access::class);
503 25
        $publicAnnotation = self::getFirstAnnotationMatching($annotations, GQL\IsPublic::class);
504
505 25
        if (null === $fieldAnnotation) {
506 25
            if (null !== $accessAnnotation || null !== $publicAnnotation) {
507 1
                throw new InvalidArgumentException(sprintf('The annotations "@Access" and/or "@Visible" defined on "%s" are only usable in addition of annotation "@Field"', $reflector->getName()));
508
            }
509
510 25
            return [];
511
        }
512
513 25
        if ($reflector instanceof ReflectionMethod && !$reflector->isPublic()) {
514 1
            throw new InvalidArgumentException(sprintf('The Annotation "@Field" can only be applied to public method. The method "%s" is not public.', $reflector->getName()));
515
        }
516
517 25
        $fieldName = $reflector->getName();
518 25
        $fieldConfiguration = [];
519
520 25
        if (isset($fieldAnnotation->type)) {
521 25
            $fieldConfiguration['type'] = $fieldAnnotation->type;
522
        }
523
524 25
        $fieldConfiguration = self::getDescriptionConfiguration($annotations, true) + $fieldConfiguration;
525
526 25
        $args = [];
527
528 25
        foreach ($fieldAnnotation->args as $arg) {
529 25
            $args[$arg->name] = ['type' => $arg->type];
530
531 25
            if (isset($arg->description)) {
532 25
                $args[$arg->name]['description'] = $arg->description;
533
            }
534
535 25
            if (isset($arg->default)) {
536 25
                $args[$arg->name]['defaultValue'] = $arg->default;
537
            }
538
        }
539
540 25
        if (empty($fieldAnnotation->args) && $reflector instanceof ReflectionMethod) {
541 25
            $args = self::guessArgs($reflector);
542
        }
543
544 25
        if (!empty($args)) {
545 25
            $fieldConfiguration['args'] = $args;
546
        }
547
548 25
        $fieldName = $fieldAnnotation->name ?? $fieldName;
549
550 25
        if (isset($fieldAnnotation->resolve)) {
551 25
            $fieldConfiguration['resolve'] = self::formatExpression($fieldAnnotation->resolve);
552
        } else {
553 25
            if ($reflector instanceof ReflectionMethod) {
554 25
                $fieldConfiguration['resolve'] = self::formatExpression(sprintf('call(%s.%s, %s)', $currentValue, $reflector->getName(), self::formatArgsForExpression($args)));
555
            } else {
556 25
                if ($fieldName !== $reflector->getName() || 'value' !== $currentValue) {
557
                    $fieldConfiguration['resolve'] = self::formatExpression(sprintf('%s.%s', $currentValue, $reflector->getName()));
558
                }
559
            }
560
        }
561
562 25
        if ($fieldAnnotation->argsBuilder) {
563 25
            if (is_string($fieldAnnotation->argsBuilder)) {
564
                $fieldConfiguration['argsBuilder'] = $fieldAnnotation->argsBuilder;
565 25
            } elseif (is_array($fieldAnnotation->argsBuilder)) {
566 25
                list($builder, $builderConfig) = $fieldAnnotation->argsBuilder;
567 25
                $fieldConfiguration['argsBuilder'] = ['builder' => $builder, 'config' => $builderConfig];
568
            } else {
569
                throw new InvalidArgumentException(sprintf('The attribute "argsBuilder" on GraphQL annotation "@%s" defined on "%s" must be a string or an array where first index is the builder name and the second is the config.', $fieldAnnotationName, $reflector->getName()));
570
            }
571
        }
572
573 25
        if ($fieldAnnotation->fieldBuilder) {
574 25
            if (is_string($fieldAnnotation->fieldBuilder)) {
575
                $fieldConfiguration['builder'] = $fieldAnnotation->fieldBuilder;
576 25
            } elseif (is_array($fieldAnnotation->fieldBuilder)) {
577 25
                list($builder, $builderConfig) = $fieldAnnotation->fieldBuilder;
578 25
                $fieldConfiguration['builder'] = $builder;
579 25
                $fieldConfiguration['builderConfig'] = $builderConfig ?: [];
580
            } else {
581 25
                throw new InvalidArgumentException(sprintf('The attribute "fieldBuilder" on GraphQL annotation "@%s" defined on "%s" must be a string or an array where first index is the builder name and the second is the config.', $fieldAnnotationName, $reflector->getName()));
582
            }
583
        } else {
584 25
            if (!isset($fieldAnnotation->type)) {
585 25
                if ($reflector instanceof ReflectionMethod) {
586
                    /** @var ReflectionMethod $reflector */
587 25
                    if ($reflector->hasReturnType()) {
588
                        try {
589
                            // @phpstan-ignore-next-line
590 25
                            $fieldConfiguration['type'] = self::resolveGraphQLTypeFromReflectionType($reflector->getReturnType(), self::VALID_OUTPUT_TYPES);
591
                        } catch (Exception $e) {
592 25
                            throw new InvalidArgumentException(sprintf('The attribute "type" on GraphQL annotation "@%s" is missing on method "%s" and cannot be auto-guessed from type hint "%s"', $fieldAnnotationName, $reflector->getName(), (string) $reflector->getReturnType()));
593
                        }
594
                    } else {
595 25
                        throw new InvalidArgumentException(sprintf('The attribute "type" on GraphQL annotation "@%s" is missing on method "%s" and cannot be auto-guessed as there is not return type hint.', $fieldAnnotationName, $reflector->getName()));
596
                    }
597
                } else {
598
                    try {
599 25
                        $fieldConfiguration['type'] = self::guessType($graphClass, $reflector, self::VALID_OUTPUT_TYPES);
600 2
                    } catch (Exception $e) {
601 2
                        throw new InvalidArgumentException(sprintf('The attribute "type" on "@%s" defined on "%s" is required and cannot be auto-guessed : %s.', $fieldAnnotationName, $reflector->getName(), $e->getMessage()));
602
                    }
603
                }
604
            }
605
        }
606
607 25
        if ($accessAnnotation) {
608 25
            $fieldConfiguration['access'] = self::formatExpression($accessAnnotation->value);
609
        }
610
611 25
        if ($publicAnnotation) {
612 25
            $fieldConfiguration['public'] = self::formatExpression($publicAnnotation->value);
613
        }
614
615 25
        if ($fieldAnnotation->complexity) {
616 25
            $fieldConfiguration['complexity'] = self::formatExpression($fieldAnnotation->complexity);
617
        }
618
619 25
        return [$fieldName => $fieldConfiguration];
620
    }
621
622
    /**
623
     * Create GraphQL input fields configuration based on annotations.
624
     *
625
     * @param ReflectionProperty[] $reflectors
626
     *
627
     * @throws AnnotationException
628
     */
629 25
    private static function getGraphQLInputFieldsFromAnnotations(GraphClass $graphClass, array $reflectors): array
630
    {
631 25
        $fields = [];
632
633 25
        foreach ($reflectors as $reflector) {
634 25
            $annotations = $graphClass->getAnnotations($reflector);
635
636
            /** @var GQL\Field|null $fieldAnnotation */
637 25
            $fieldAnnotation = self::getFirstAnnotationMatching($annotations, GQL\Field::class);
638
639
            // No field annotation found
640 25
            if (null === $fieldAnnotation) {
641 25
                continue;
642
            }
643
644
            // Ignore field with resolver when the type is an Input
645 25
            if (isset($fieldAnnotation->resolve)) {
646 25
                continue;
647
            }
648
649 25
            $fieldName = $reflector->getName();
650 25
            if (isset($fieldAnnotation->type)) {
651 25
                $fieldType = $fieldAnnotation->type;
652
            } else {
653
                try {
654 25
                    $fieldType = self::guessType($graphClass, $reflector, self::VALID_INPUT_TYPES);
655
                } catch (Exception $e) {
656
                    throw new InvalidArgumentException(sprintf('The attribute "type" on GraphQL annotation "@%s" is missing on property "%s" and cannot be auto-guessed as there is no type hint or Doctrine annotation.', GQL\Field::class, $reflector->getName()));
657
                }
658
            }
659 25
            $fieldConfiguration = [];
660 25
            if ($fieldType) {
661
                // Resolve a PHP class from a GraphQL type
662 25
                $resolvedType = self::$classesMap[$fieldType] ?? null;
663
                // We found a type but it is not allowed
664 25
                if (null !== $resolvedType && !in_array($resolvedType['type'], self::VALID_INPUT_TYPES)) {
665
                    throw new InvalidArgumentException(sprintf('The type "%s" on "%s" is a "%s" not valid on an Input @Field. Only Input, Scalar and Enum are allowed.', $fieldType, $reflector->getName(), $resolvedType['type']));
666
                }
667
668 25
                $fieldConfiguration['type'] = $fieldType;
669
            }
670
671 25
            $fieldConfiguration = array_merge(self::getDescriptionConfiguration($annotations, true), $fieldConfiguration);
672 25
            $fields[$fieldName] = $fieldConfiguration;
673
        }
674
675 25
        return $fields;
676
    }
677
678
    /**
679
     * Create GraphQL type fields configuration based on annotations.
680
     *
681
     * @phpstan-param class-string<GQL\Field> $fieldAnnotationName
682
     *
683
     * @param ReflectionProperty[]|ReflectionMethod[] $reflectors
684
     *
685
     * @throws AnnotationException
686
     */
687 26
    private static function getGraphQLTypeFieldsFromAnnotations(GraphClass $graphClass, array $reflectors, string $fieldAnnotationName = GQL\Field::class, string $currentValue = 'value'): array
688
    {
689 26
        $fields = [];
690
691 26
        foreach ($reflectors as $reflector) {
692 25
            $fields = array_merge($fields, self::getTypeFieldConfigurationFromReflector($graphClass, $reflector, $fieldAnnotationName, $currentValue));
693
        }
694
695 26
        return $fields;
696
    }
697
698
    /**
699
     * @phpstan-param class-string<GQL\Query|GQL\Mutation> $expectedAnnotation
700
     *
701
     * Return fields config from Provider methods.
702
     * Loop through configured provider and extract fields targeting the targetType.
703
     */
704 26
    private static function getGraphQLFieldsFromProviders(GraphClass $graphClass, string $expectedAnnotation, string $targetType, bool $isDefaultTarget = false): array
705
    {
706 26
        $fields = [];
707 26
        foreach (self::$providers as ['metadata' => $providerMetadata, 'annotation' => $providerAnnotation]) {
708 25
            $defaultAccessAnnotation = self::getFirstAnnotationMatching($providerMetadata->getAnnotations(), GQL\Access::class);
709 25
            $defaultIsPublicAnnotation = self::getFirstAnnotationMatching($providerMetadata->getAnnotations(), GQL\IsPublic::class);
710
711 25
            $defaultAccess = $defaultAccessAnnotation ? self::formatExpression($defaultAccessAnnotation->value) : false;
712 25
            $defaultIsPublic = $defaultIsPublicAnnotation ? self::formatExpression($defaultIsPublicAnnotation->value) : false;
713
714 25
            $methods = [];
715
            // First found the methods matching the targeted type
716 25
            foreach ($providerMetadata->getMethods() as $method) {
717 25
                $annotations = $providerMetadata->getAnnotations($method);
718
719 25
                $annotation = self::getFirstAnnotationMatching($annotations, [GQL\Mutation::class, GQL\Query::class]);
720 25
                if (null === $annotation) {
721
                    continue;
722
                }
723
724
                // TODO: Remove old property check in 1.1
725 25
                $annotationTargets = $annotation->targetTypes ?? $annotation->targetType ?? null;
726
727 25
                if (null === $annotationTargets) {
728 25
                    if ($annotation instanceof GQL\Mutation && isset($providerAnnotation->targetMutationTypes)) {
729 25
                        $annotationTargets = $providerAnnotation->targetMutationTypes;
730 25
                    } elseif ($annotation instanceof GQL\Query && isset($providerAnnotation->targetQueryTypes)) {
731 25
                        $annotationTargets = $providerAnnotation->targetQueryTypes;
732
                    }
733
                }
734
735 25
                if (null === $annotationTargets) {
736 25
                    if ($isDefaultTarget) {
737 25
                        $annotationTargets = [$targetType];
738 25
                        if (!$annotation instanceof $expectedAnnotation) {
739 25
                            continue;
740
                        }
741
                    } else {
742 25
                        continue;
743
                    }
744
                }
745
746 25
                if (!in_array($targetType, $annotationTargets)) {
747 25
                    continue;
748
                }
749
750 25
                if (!$annotation instanceof $expectedAnnotation) {
751 2
                    if (GQL\Mutation::class === $expectedAnnotation) {
752 1
                        $message = sprintf('The provider "%s" try to add a query field on type "%s" (through @Query on method "%s") but "%s" is a mutation.', $providerMetadata->getName(), $targetType, $method->getName(), $targetType);
753
                    } else {
754 1
                        $message = sprintf('The provider "%s" try to add a mutation on type "%s" (through @Mutation on method "%s") but "%s" is not a mutation.', $providerMetadata->getName(), $targetType, $method->getName(), $targetType);
755
                    }
756
757 2
                    throw new InvalidArgumentException($message);
758
                }
759 25
                $methods[$method->getName()] = $method;
760
            }
761
762 25
            $currentValue = sprintf("service('%s')", self::formatNamespaceForExpression($providerMetadata->getName()));
763 25
            $providerFields = self::getGraphQLTypeFieldsFromAnnotations($graphClass, $methods, $expectedAnnotation, $currentValue);
764 25
            foreach ($providerFields as $fieldName => $fieldConfig) {
765 25
                if (isset($providerAnnotation->prefix)) {
766 25
                    $fieldName = sprintf('%s%s', $providerAnnotation->prefix, $fieldName);
767
                }
768
769 25
                if ($defaultAccess && !isset($fieldConfig['access'])) {
770 25
                    $fieldConfig['access'] = $defaultAccess;
771
                }
772
773 25
                if ($defaultIsPublic && !isset($fieldConfig['public'])) {
774 25
                    $fieldConfig['public'] = $defaultIsPublic;
775
                }
776
777 25
                $fields[$fieldName] = $fieldConfig;
778
            }
779
        }
780
781 26
        return $fields;
782
    }
783
784
    /**
785
     * Get the config for description & deprecation reason.
786
     */
787 26
    private static function getDescriptionConfiguration(array $annotations, bool $withDeprecation = false): array
788
    {
789 26
        $config = [];
790 26
        $descriptionAnnotation = self::getFirstAnnotationMatching($annotations, GQL\Description::class);
791 26
        if (null !== $descriptionAnnotation) {
792 25
            $config['description'] = $descriptionAnnotation->value;
793
        }
794
795 26
        if ($withDeprecation) {
796 25
            $deprecatedAnnotation = self::getFirstAnnotationMatching($annotations, GQL\Deprecated::class);
797 25
            if (null !== $deprecatedAnnotation) {
798 25
                $config['deprecationReason'] = $deprecatedAnnotation->value;
799
            }
800
        }
801
802 26
        return $config;
803
    }
804
805
    /**
806
     * Format an array of args to a list of arguments in an expression.
807
     */
808 25
    private static function formatArgsForExpression(array $args): string
809
    {
810 25
        $mapping = [];
811 25
        foreach ($args as $name => $config) {
812 25
            $mapping[] = sprintf('%s: "%s"', $name, $config['type']);
813
        }
814
815 25
        return sprintf('arguments({%s}, args)', implode(', ', $mapping));
816
    }
817
818
    /**
819
     * Format a namespace to be used in an expression (double escape).
820
     */
821 25
    private static function formatNamespaceForExpression(string $namespace): string
822
    {
823 25
        return str_replace('\\', '\\\\', $namespace);
824
    }
825
826
    /**
827
     * Get the first annotation matching given class.
828
     *
829
     * @phpstan-template T of object
830
     * @phpstan-param class-string<T>|class-string<T>[] $annotationClass
831
     * @phpstan-return T|null
832
     *
833
     * @param string|array $annotationClass
834
     *
835
     * @return object|null
836
     */
837 26
    private static function getFirstAnnotationMatching(array $annotations, $annotationClass)
838
    {
839 26
        if (is_string($annotationClass)) {
840 26
            $annotationClass = [$annotationClass];
841
        }
842
843 26
        foreach ($annotations as $annotation) {
844 26
            foreach ($annotationClass as $class) {
845 26
                if ($annotation instanceof $class) {
846 25
                    return $annotation;
847
                }
848
            }
849
        }
850
851 26
        return null;
852
    }
853
854
    /**
855
     * Format an expression (ie. add "@=" if not set).
856
     */
857 25
    private static function formatExpression(string $expression): string
858
    {
859 25
        return '@=' === substr($expression, 0, 2) ? $expression : sprintf('@=%s', $expression);
860
    }
861
862
    /**
863
     * Suffix a name if it is not already.
864
     */
865 25
    private static function suffixName(string $name, string $suffix): string
866
    {
867 25
        return substr($name, -strlen($suffix)) === $suffix ? $name : sprintf('%s%s', $name, $suffix);
868
    }
869
870
    /**
871
     * Try to guess a field type base on his annotations.
872
     *
873
     * @throws RuntimeException
874
     */
875 25
    private static function guessType(GraphClass $graphClass, ReflectionProperty $reflector, array $filterGraphQLTypes = []): string
876
    {
877 25
        if ($reflector->hasType()) {
0 ignored issues
show
Bug introduced by
The method hasType() does not exist on ReflectionProperty. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

877
        if ($reflector->/** @scrutinizer ignore-call */ hasType()) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
878
            try {
879
                // @phpstan-ignore-next-line
880 25
                return self::resolveGraphQLTypeFromReflectionType($reflector->getType(), $filterGraphQLTypes);
0 ignored issues
show
Bug introduced by
The method getType() does not exist on ReflectionProperty. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

880
                return self::resolveGraphQLTypeFromReflectionType($reflector->/** @scrutinizer ignore-call */ getType(), $filterGraphQLTypes);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
881 25
            } catch (Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
882
            }
883
        }
884
885 25
        $annotations = $graphClass->getAnnotations($reflector);
886 25
        $columnAnnotation = self::getFirstAnnotationMatching($annotations, Column::class);
887 25
        if (null !== $columnAnnotation) {
888 25
            $type = self::resolveTypeFromDoctrineType($columnAnnotation->type);
889 25
            $nullable = $columnAnnotation->nullable;
890 25
            if ($type) {
891 25
                return $nullable ? $type : sprintf('%s!', $type);
892
            } else {
893 1
                throw new RuntimeException(sprintf('Unable to auto-guess GraphQL type from Doctrine type "%s"', $columnAnnotation->type));
894
            }
895
        }
896
897
        $associationAnnotations = [
898 25
            OneToMany::class => true,
899
            OneToOne::class => false,
900
            ManyToMany::class => true,
901
            ManyToOne::class => false,
902
        ];
903
904 25
        $associationAnnotation = self::getFirstAnnotationMatching($annotations, array_keys($associationAnnotations));
905 25
        if (null !== $associationAnnotation) {
906 25
            $target = self::fullyQualifiedClassName($associationAnnotation->targetEntity, $graphClass->getNamespaceName());
907 25
            $type = self::resolveTypeFromClass($target, ['type']);
908
909 25
            if ($type) {
910 25
                $isMultiple = $associationAnnotations[get_class($associationAnnotation)];
911 25
                if ($isMultiple) {
912 25
                    return sprintf('[%s]!', $type);
913
                } else {
914 25
                    $isNullable = false;
915 25
                    $joinColumn = self::getFirstAnnotationMatching($annotations, JoinColumn::class);
916 25
                    if (null !== $joinColumn) {
917 25
                        $isNullable = $joinColumn->nullable;
918
                    }
919
920 25
                    return sprintf('%s%s', $type, $isNullable ? '' : '!');
921
                }
922
            } else {
923 1
                throw new RuntimeException(sprintf('Unable to auto-guess GraphQL type from Doctrine target class "%s" (check if the target class is a GraphQL type itself (with a @GQL\Type annotation).', $target));
924
            }
925
        }
926
927
        throw new InvalidArgumentException(sprintf('No Doctrine ORM annotation found.'));
928
    }
929
930
    /**
931
     * Resolve a FQN from classname and namespace.
932
     *
933
     * @internal
934
     */
935 25
    public static function fullyQualifiedClassName(string $className, string $namespace): string
936
    {
937 25
        if (false === strpos($className, '\\') && $namespace) {
938 25
            return $namespace.'\\'.$className;
939
        }
940
941 1
        return $className;
942
    }
943
944
    /**
945
     * Resolve a GraphQLType from a doctrine type.
946
     */
947 25
    private static function resolveTypeFromDoctrineType(string $doctrineType): ?string
948
    {
949 25
        if (isset(self::$doctrineMapping[$doctrineType])) {
950 25
            return self::$doctrineMapping[$doctrineType];
951
        }
952
953
        switch ($doctrineType) {
954 25
            case 'integer':
955 1
            case 'smallint':
956 1
            case 'bigint':
957 25
                return 'Int';
958 1
            case 'string':
959 1
            case 'text':
960
                return 'String';
961 1
            case 'bool':
962 1
            case 'boolean':
963
                return 'Boolean';
964 1
            case 'float':
965 1
            case 'decimal':
966
                return 'Float';
967
            default:
968 1
                return null;
969
        }
970
    }
971
972
    /**
973
     * Transform a method arguments from reflection to a list of GraphQL argument.
974
     */
975 25
    private static function guessArgs(ReflectionMethod $method): array
976
    {
977 25
        $arguments = [];
978 25
        foreach ($method->getParameters() as $index => $parameter) {
979 25
            if (!$parameter->hasType()) {
980 1
                throw new InvalidArgumentException(sprintf('Argument n°%s "$%s" on method "%s" cannot be auto-guessed as there is not type hint.', $index + 1, $parameter->getName(), $method->getName()));
981
            }
982
983
            try {
984
                // @phpstan-ignore-next-line
985 25
                $gqlType = self::resolveGraphQLTypeFromReflectionType($parameter->getType(), self::VALID_INPUT_TYPES, $parameter->isDefaultValueAvailable());
986
            } catch (Exception $e) {
987
                throw new InvalidArgumentException(sprintf('Argument n°%s "$%s" on method "%s" cannot be auto-guessed : %s".', $index + 1, $parameter->getName(), $method->getName(), $e->getMessage()));
988
            }
989
990 25
            $argumentConfig = [];
991 25
            if ($parameter->isDefaultValueAvailable()) {
992 25
                $argumentConfig['defaultValue'] = $parameter->getDefaultValue();
993
            }
994
995 25
            $argumentConfig['type'] = $gqlType;
996
997 25
            $arguments[$parameter->getName()] = $argumentConfig;
998
        }
999
1000 25
        return $arguments;
1001
    }
1002
1003 25
    private static function resolveGraphQLTypeFromReflectionType(ReflectionNamedType $type, array $filterGraphQLTypes = [], bool $isOptional = false): string
1004
    {
1005 25
        $sType = $type->getName();
1006 25
        if ($type->isBuiltin()) {
1007 25
            $gqlType = self::resolveTypeFromPhpType($sType);
1008 25
            if (null === $gqlType) {
1009 25
                throw new RuntimeException(sprintf('No corresponding GraphQL type found for builtin type "%s"', $sType));
1010
            }
1011
        } else {
1012 25
            $gqlType = self::resolveTypeFromClass($sType, $filterGraphQLTypes);
1013 25
            if (null === $gqlType) {
1014
                throw new RuntimeException(sprintf('No corresponding GraphQL %s found for class "%s"', $filterGraphQLTypes ? implode(',', $filterGraphQLTypes) : 'object', $sType));
1015
            }
1016
        }
1017
1018 25
        return sprintf('%s%s', $gqlType, ($type->allowsNull() || $isOptional) ? '' : '!');
1019
    }
1020
1021
    /**
1022
     * Resolve a GraphQL Type from a class name.
1023
     */
1024 25
    private static function resolveTypeFromClass(string $className, array $wantedTypes = []): ?string
1025
    {
1026 25
        foreach (self::$classesMap as $gqlType => $config) {
1027 25
            if ($config['class'] === $className) {
1028 25
                if (in_array($config['type'], $wantedTypes)) {
1029 25
                    return $gqlType;
1030
                }
1031
            }
1032
        }
1033
1034 1
        return null;
1035
    }
1036
1037
    /**
1038
     * Search the classes map for class by predicate.
1039
     *
1040
     * @return array
1041
     */
1042 26
    private static function searchClassesMapBy(callable $predicate, string $type)
1043
    {
1044 26
        $classNames = [];
1045 26
        foreach (self::$classesMap as $gqlType => $config) {
1046 26
            if ($config['type'] !== $type) {
1047 26
                continue;
1048
            }
1049
1050 25
            if ($predicate($gqlType, $config)) {
1051 25
                $classNames[$gqlType] = $config;
1052
            }
1053
        }
1054
1055 26
        return $classNames;
1056
    }
1057
1058
    /**
1059
     * Convert a PHP Builtin type to a GraphQL type.
1060
     */
1061 25
    private static function resolveTypeFromPhpType(string $phpType): ?string
1062
    {
1063
        switch ($phpType) {
1064 25
            case 'boolean':
1065 25
            case 'bool':
1066 25
                return 'Boolean';
1067 25
            case 'integer':
1068 25
            case 'int':
1069 25
                return 'Int';
1070 25
            case 'float':
1071 25
            case 'double':
1072 25
                return 'Float';
1073 25
            case 'string':
1074 25
                return 'String';
1075
            default:
1076 25
                return null;
1077
        }
1078
    }
1079
}
1080