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

Completed
Pull Request — master (#731)
by Vincent
18:45 queued 15:36
created

AnnotationParser::getGraphQLFieldsFromProviders()   F

Complexity

Conditions 22
Paths 1697

Size

Total Lines 78
Code Lines 45

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 42
CRAP Score 22.0455

Importance

Changes 1
Bugs 1 Features 0
Metric Value
eloc 45
c 1
b 1
f 0
dl 0
loc 78
ccs 42
cts 44
cp 0.9545
rs 0
cc 22
nc 1697
nop 4
crap 22.0455

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Overblog\GraphQLBundle\Config\Parser;
6
7
use Doctrine\ORM\Mapping\Column;
8
use Doctrine\ORM\Mapping\JoinColumn;
9
use Doctrine\ORM\Mapping\ManyToMany;
10
use Doctrine\ORM\Mapping\ManyToOne;
11
use Doctrine\ORM\Mapping\OneToMany;
12
use Doctrine\ORM\Mapping\OneToOne;
13
use Exception;
14
use Overblog\GraphQLBundle\Annotation as GQL;
15
use Overblog\GraphQLBundle\Config\Parser\Annotation\GraphClass;
16
use Overblog\GraphQLBundle\Relay\Connection\ConnectionInterface;
17
use Overblog\GraphQLBundle\Relay\Connection\EdgeInterface;
18
use ReflectionException;
19
use ReflectionMethod;
20
use ReflectionNamedType;
21
use ReflectionProperty;
22
use Reflector;
23
use RuntimeException;
24
use SplFileInfo;
25
use Symfony\Component\Config\Resource\FileResource;
26
use Symfony\Component\DependencyInjection\ContainerBuilder;
27
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
28
use function array_filter;
29
use function array_keys;
30
use function array_map;
31
use function array_unshift;
32
use function current;
33
use function file_get_contents;
34
use function get_class;
35
use function implode;
36
use function in_array;
37
use function is_array;
38
use function is_string;
39
use function preg_match;
40
use function sprintf;
41
use function str_replace;
42
use function strlen;
43
use function strpos;
44
use function substr;
45
use function trim;
46
47
class AnnotationParser implements PreParserInterface
48
{
49
    private static array $classesMap = [];
50
    private static array $providers = [];
51
    private static array $doctrineMapping = [];
52
    private static array $graphClassCache = [];
53
54
    private const GQL_SCALAR = 'scalar';
55
    private const GQL_ENUM = 'enum';
56
    private const GQL_TYPE = 'type';
57
    private const GQL_INPUT = 'input';
58
    private const GQL_UNION = 'union';
59
    private const GQL_INTERFACE = 'interface';
60
61
    /**
62
     * @see https://facebook.github.io/graphql/draft/#sec-Input-and-Output-Types
63
     */
64
    private const VALID_INPUT_TYPES = [self::GQL_SCALAR, self::GQL_ENUM, self::GQL_INPUT];
65
    private const VALID_OUTPUT_TYPES = [self::GQL_SCALAR, self::GQL_TYPE, self::GQL_INTERFACE, self::GQL_UNION, self::GQL_ENUM];
66
67
    /**
68
     * {@inheritdoc}
69
     *
70
     * @throws InvalidArgumentException
71
     */
72 25
    public static function preParse(SplFileInfo $file, ContainerBuilder $container, array $configs = []): void
73
    {
74 25
        $container->setParameter('overblog_graphql_types.classes_map', self::processFile($file, $container, $configs, true));
75 25
    }
76
77
    /**
78
     * @throws InvalidArgumentException
79
     */
80 25
    public static function parse(SplFileInfo $file, ContainerBuilder $container, array $configs = []): array
81
    {
82 25
        return self::processFile($file, $container, $configs, false);
83
    }
84
85
    /**
86
     * @internal
87
     */
88 63
    public static function reset(): void
89
    {
90 63
        self::$classesMap = [];
91 63
        self::$providers = [];
92 63
        self::$graphClassCache = [];
93 63
    }
94
95
    /**
96
     * Process a file.
97
     *
98
     * @throws InvalidArgumentException
99
     * @throws ReflectionException
100
     */
101 25
    private static function processFile(SplFileInfo $file, ContainerBuilder $container, array $configs, bool $preProcess): array
102
    {
103 25
        self::$doctrineMapping = $configs['doctrine']['types_mapping'];
104 25
        $container->addResource(new FileResource($file->getRealPath()));
105
106
        try {
107 25
            $className = $file->getBasename('.php');
108 25
            if (preg_match('#namespace (.+);#', file_get_contents($file->getRealPath()), $matches)) {
109 25
                $className = trim($matches[1]).'\\'.$className;
110
            }
111
112 25
            $gqlTypes = [];
113 25
            $graphClass = self::getGraphClass($className);
114
115 25
            foreach ($graphClass->getAnnotations() as $classAnnotation) {
116 25
                $gqlTypes = self::classAnnotationsToGQLConfiguration(
117 25
                    $graphClass,
118
                    $classAnnotation,
119
                    $configs,
120
                    $gqlTypes,
121
                    $preProcess
122
                );
123
            }
124
125 25
            return $preProcess ? self::$classesMap : $gqlTypes;
126 10
        } catch (\InvalidArgumentException $e) {
127 10
            throw new InvalidArgumentException(sprintf('Failed to parse GraphQL annotations from file "%s".', $file), $e->getCode(), $e);
128
        }
129
    }
130
131 25
    private static function classAnnotationsToGQLConfiguration(
132
        GraphClass $graphClass,
133
        object $classAnnotation,
134
        array $configs,
135
        array $gqlTypes,
136
        bool $preProcess
137
    ): array {
138 25
        $gqlConfiguration = $gqlType = $gqlName = null;
139
140
        switch (true) {
141 25
            case $classAnnotation instanceof GQL\Type:
142 25
                $gqlType = self::GQL_TYPE;
143 25
                $gqlName = $classAnnotation->name ?: $graphClass->getShortName();
144 25
                if (!$preProcess) {
145 25
                    $gqlConfiguration = self::typeAnnotationToGQLConfiguration($graphClass, $classAnnotation, $gqlName, $configs);
146
147 25
                    if ($classAnnotation instanceof GQL\Relay\Connection) {
148 24
                        if (!$graphClass->implementsInterface(ConnectionInterface::class)) {
149
                            throw new InvalidArgumentException(sprintf('The annotation @Connection on class "%s" can only be used on class implementing the ConnectionInterface.', $graphClass->getName()));
150
                        }
151
152 24
                        if (!($classAnnotation->edge xor $classAnnotation->node)) {
153
                            throw new InvalidArgumentException(sprintf('The annotation @Connection on class "%s" is invalid. You must define the "edge" OR the "node" attribute.', $graphClass->getName()));
154
                        }
155
156 24
                        $edgeType = $classAnnotation->edge;
157 24
                        if (!$edgeType) {
158 24
                            $edgeType = sprintf('%sEdge', $gqlName);
159 24
                            $gqlTypes[$edgeType] = [
160 24
                                'type' => 'object',
161
                                'config' => [
162
                                    'builders' => [
163 24
                                        ['builder' => 'relay-edge', 'builderConfig' => ['nodeType' => $classAnnotation->node]],
164
                                    ],
165
                                ],
166
                            ];
167
                        }
168 24
                        if (!isset($gqlConfiguration['config']['builders'])) {
169 24
                            $gqlConfiguration['config']['builders'] = [];
170
                        }
171 24
                        array_unshift($gqlConfiguration['config']['builders'], ['builder' => 'relay-connection', 'builderConfig' => ['edgeType' => $edgeType]]);
172
                    }
173
                }
174 25
                break;
175
176 24
            case $classAnnotation instanceof GQL\Input:
177 24
                $gqlType = self::GQL_INPUT;
178 24
                $gqlName = $classAnnotation->name ?: self::suffixName($graphClass->getShortName(), 'Input');
179 24
                if (!$preProcess) {
180 24
                    $gqlConfiguration = self::inputAnnotationToGQLConfiguration($graphClass, $classAnnotation);
181
                }
182 24
                break;
183
184 24
            case $classAnnotation instanceof GQL\Scalar:
185 24
                $gqlType = self::GQL_SCALAR;
186 24
                if (!$preProcess) {
187 24
                    $gqlConfiguration = self::scalarAnnotationToGQLConfiguration($graphClass, $classAnnotation);
188
                }
189 24
                break;
190
191 24
            case $classAnnotation instanceof GQL\Enum:
192 24
                $gqlType = self::GQL_ENUM;
193 24
                if (!$preProcess) {
194 24
                    $gqlConfiguration = self::enumAnnotationToGQLConfiguration($graphClass, $classAnnotation);
195
                }
196 24
                break;
197
198 24
            case $classAnnotation instanceof GQL\Union:
199 24
                $gqlType = self::GQL_UNION;
200 24
                if (!$preProcess) {
201 24
                    $gqlConfiguration = self::unionAnnotationToGQLConfiguration($graphClass, $classAnnotation);
202
                }
203 24
                break;
204
205 24
            case $classAnnotation instanceof GQL\TypeInterface:
206 24
                $gqlType = self::GQL_INTERFACE;
207 24
                if (!$preProcess) {
208 24
                    $gqlConfiguration = self::typeInterfaceAnnotationToGQLConfiguration($graphClass, $classAnnotation);
209
                }
210 24
                break;
211
212 24
            case $classAnnotation instanceof GQL\Provider:
213 24
                if ($preProcess) {
214 24
                    self::$providers[] = ['metadata' => $graphClass, 'annotation' => $classAnnotation];
215
                }
216
217 24
                return [];
218
        }
219
220 25
        if (null !== $gqlType) {
221 25
            if (!$gqlName) {
222 24
                $gqlName = $classAnnotation->name ?: $graphClass->getShortName();
223
            }
224
225 25
            if ($preProcess) {
226 25
                if (isset(self::$classesMap[$gqlName])) {
227 1
                    throw new InvalidArgumentException(sprintf('The GraphQL type "%s" has already been registered in class "%s"', $gqlName, self::$classesMap[$gqlName]['class']));
228
                }
229 25
                self::$classesMap[$gqlName] = ['type' => $gqlType, 'class' => $graphClass->getName()];
230
            } else {
231 25
                $gqlTypes = [$gqlName => $gqlConfiguration] + $gqlTypes;
232
            }
233
        }
234
235 25
        return $gqlTypes;
236
    }
237
238
    /**
239
     * @throws ReflectionException
240
     */
241 25
    private static function getGraphClass(string $className): GraphClass
242
    {
243 25
        self::$graphClassCache[$className] ??= new GraphClass($className);
244
245 25
        return self::$graphClassCache[$className];
246
    }
247
248 25
    private static function typeAnnotationToGQLConfiguration(
249
        GraphClass $graphClass,
250
        GQL\Type $classAnnotation,
251
        string $gqlName,
252
        array $configs
253
    ): array {
254 25
        $isMutation = $isDefault = $isRoot = false;
255 25
        if (isset($configs['definitions']['schema'])) {
256 24
            $defaultSchemaName = isset($configs['definitions']['schema']['default']) ? 'default' : array_key_first($configs['definitions']['schema']);
257 24
            foreach ($configs['definitions']['schema'] as $schemaName => $schema) {
258 24
                $schemaQuery = $schema['query'] ?? null;
259 24
                $schemaMutation = $schema['mutation'] ?? null;
260
261 24
                if ($gqlName === $schemaQuery) {
262 24
                    $isRoot = true;
263 24
                    if ($defaultSchemaName === $schemaName) {
264 24
                        $isDefault = true;
265
                    }
266 24
                } elseif ($gqlName === $schemaMutation) {
267 24
                    $isMutation = true;
268 24
                    $isRoot = true;
269 24
                    if ($defaultSchemaName === $schemaName) {
270 24
                        $isDefault = true;
271
                    }
272
                }
273
            }
274
        }
275
276 25
        $currentValue = $isRoot ? sprintf("service('%s')", self::formatNamespaceForExpression($graphClass->getName())) : 'value';
277
278 25
        $gqlConfiguration = self::graphQLTypeConfigFromAnnotation($graphClass, $classAnnotation, $currentValue);
279
280 25
        $providerFields = self::getGraphQLFieldsFromProviders($graphClass, $isMutation ? GQL\Mutation::class : GQL\Query::class, $gqlName, $isDefault);
281 25
        $gqlConfiguration['config']['fields'] = array_merge($gqlConfiguration['config']['fields'], $providerFields);
282
283 25
        if ($classAnnotation instanceof GQL\Relay\Edge) {
284 24
            if (!$graphClass->implementsInterface(EdgeInterface::class)) {
285
                throw new InvalidArgumentException(sprintf('The annotation @Edge on class "%s" can only be used on class implementing the EdgeInterface.', $graphClass->getName()));
286
            }
287 24
            if (!isset($gqlConfiguration['config']['builders'])) {
288 24
                $gqlConfiguration['config']['builders'] = [];
289
            }
290 24
            array_unshift($gqlConfiguration['config']['builders'], ['builder' => 'relay-edge', 'builderConfig' => ['nodeType' => $classAnnotation->node]]);
291
        }
292
293 25
        return $gqlConfiguration;
294
    }
295
296 25
    private static function graphQLTypeConfigFromAnnotation(GraphClass $graphClass, GQL\Type $typeAnnotation, string $currentValue): array
297
    {
298 25
        $typeConfiguration = [];
299 25
        $fieldsFromProperties = self::getGraphQLTypeFieldsFromAnnotations($graphClass, $graphClass->getPropertiesExtended(), GQL\Field::class, $currentValue);
300 25
        $fieldsFromMethods = self::getGraphQLTypeFieldsFromAnnotations($graphClass, $graphClass->getMethods(), GQL\Field::class, $currentValue);
301
302 25
        $typeConfiguration['fields'] = array_merge($fieldsFromProperties, $fieldsFromMethods);
303 25
        $typeConfiguration = self::getDescriptionConfiguration($graphClass->getAnnotations()) + $typeConfiguration;
304
305 25
        if (null !== $typeAnnotation->interfaces) {
306 24
            $typeConfiguration['interfaces'] = $typeAnnotation->interfaces;
307
        } else {
308 25
            $interfaces = array_keys(self::searchClassesMapBy(function ($gqlType, $configuration) use ($graphClass) {
309 24
                ['class' => $interfaceClassName] = $configuration;
310
311 24
                $interfaceMetadata = self::getGraphClass($interfaceClassName);
312 24
                if ($interfaceMetadata->isInterface() && $graphClass->implementsInterface($interfaceMetadata->getName())) {
313 24
                    return true;
314
                }
315
316 24
                return $graphClass->isSubclassOf($interfaceClassName);
317 25
            }, self::GQL_INTERFACE));
318
319 25
            sort($interfaces);
320 25
            $typeConfiguration['interfaces'] = $interfaces;
321
        }
322
323 25
        if ($typeAnnotation->resolveField) {
324 24
            $typeConfiguration['resolveField'] = self::formatExpression($typeAnnotation->resolveField);
325
        }
326
327 25
        if ($typeAnnotation->builders && !empty($typeAnnotation->builders)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $typeAnnotation->builders of type Overblog\GraphQLBundle\Annotation\FieldsBuilder[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
328 24
            $typeConfiguration['builders'] = array_map(function ($fieldsBuilderAnnotation) {
329 24
                return ['builder' => $fieldsBuilderAnnotation->builder, 'builderConfig' => $fieldsBuilderAnnotation->builderConfig];
330 24
            }, $typeAnnotation->builders);
331
        }
332
333 25
        if ($typeAnnotation->isTypeOf) {
334 24
            $typeConfiguration['isTypeOf'] = $typeAnnotation->isTypeOf;
335
        }
336
337 25
        $publicAnnotation = self::getFirstAnnotationMatching($graphClass->getAnnotations(), GQL\IsPublic::class);
338 25
        if ($publicAnnotation) {
339 24
            $typeConfiguration['fieldsDefaultPublic'] = self::formatExpression($publicAnnotation->value);
340
        }
341
342 25
        $accessAnnotation = self::getFirstAnnotationMatching($graphClass->getAnnotations(), GQL\Access::class);
343 25
        if ($accessAnnotation) {
344 24
            if (isset($accessAnnotation->value)) {
345 24
                $typeConfiguration['fieldsDefaultAccess'] = self::formatExpression($accessAnnotation->value);
346
            }
347 24
            if (isset($accessAnnotation->nullOnDenied)) {
348 24
                $typeConfiguration['fieldsDefaultAccessConfig'] = ['nullOnDenied' => $accessAnnotation->nullOnDenied];
349
            }
350
        }
351
352 25
        return ['type' => $typeAnnotation->isRelay ? 'relay-mutation-payload' : 'object', 'config' => $typeConfiguration];
353
    }
354
355
    /**
356
     * Create a GraphQL Interface type configuration from annotations on properties.
357
     */
358 24
    private static function typeInterfaceAnnotationToGQLConfiguration(GraphClass $graphClass, GQL\TypeInterface $interfaceAnnotation): array
359
    {
360 24
        $interfaceConfiguration = [];
361
362 24
        $fieldsFromProperties = self::getGraphQLTypeFieldsFromAnnotations($graphClass, $graphClass->getPropertiesExtended());
363 24
        $fieldsFromMethods = self::getGraphQLTypeFieldsFromAnnotations($graphClass, $graphClass->getMethods());
364
365 24
        $interfaceConfiguration['fields'] = array_merge($fieldsFromProperties, $fieldsFromMethods);
366 24
        $interfaceConfiguration = self::getDescriptionConfiguration($graphClass->getAnnotations()) + $interfaceConfiguration;
367
368 24
        $interfaceConfiguration['resolveType'] = self::formatExpression($interfaceAnnotation->resolveType);
369
370 24
        return ['type' => 'interface', 'config' => $interfaceConfiguration];
371
    }
372
373
    /**
374
     * Create a GraphQL Input type configuration from annotations on properties.
375
     */
376 24
    private static function inputAnnotationToGQLConfiguration(GraphClass $graphClass, GQL\Input $inputAnnotation): array
377
    {
378 24
        $inputConfiguration = array_merge([
379 24
            'fields' => self::getGraphQLInputFieldsFromAnnotations($graphClass, $graphClass->getPropertiesExtended()),
380 24
        ], self::getDescriptionConfiguration($graphClass->getAnnotations()));
381
382 24
        return ['type' => $inputAnnotation->isRelay ? 'relay-mutation-input' : 'input-object', 'config' => $inputConfiguration];
383
    }
384
385
    /**
386
     * Get a GraphQL scalar configuration from given scalar annotation.
387
     */
388 24
    private static function scalarAnnotationToGQLConfiguration(GraphClass $graphClass, GQL\Scalar $scalarAnnotation): array
389
    {
390 24
        $scalarConfiguration = [];
391
392 24
        if ($scalarAnnotation->scalarType) {
393 24
            $scalarConfiguration['scalarType'] = self::formatExpression($scalarAnnotation->scalarType);
394
        } else {
395
            $scalarConfiguration = [
396 24
                'serialize' => [$graphClass->getName(), 'serialize'],
397 24
                'parseValue' => [$graphClass->getName(), 'parseValue'],
398 24
                'parseLiteral' => [$graphClass->getName(), 'parseLiteral'],
399
            ];
400
        }
401
402 24
        $scalarConfiguration = self::getDescriptionConfiguration($graphClass->getAnnotations()) + $scalarConfiguration;
403
404 24
        return ['type' => 'custom-scalar', 'config' => $scalarConfiguration];
405
    }
406
407
    /**
408
     * Get a GraphQL Enum configuration from given enum annotation.
409
     */
410 24
    private static function enumAnnotationToGQLConfiguration(GraphClass $graphClass, GQL\Enum $enumAnnotation): array
411
    {
412 24
        $enumValues = $enumAnnotation->values ? $enumAnnotation->values : [];
413
414 24
        $values = [];
415
416 24
        foreach ($graphClass->getConstants() as $name => $value) {
417 24
            $valueAnnotation = current(array_filter($enumValues, function ($enumValueAnnotation) use ($name) {
418 24
                return $enumValueAnnotation->name == $name;
419 24
            }));
420 24
            $valueConfig = [];
421 24
            $valueConfig['value'] = $value;
422
423 24
            if ($valueAnnotation && $valueAnnotation->description) {
424 24
                $valueConfig['description'] = $valueAnnotation->description;
425
            }
426
427 24
            if ($valueAnnotation && $valueAnnotation->deprecationReason) {
428 24
                $valueConfig['deprecationReason'] = $valueAnnotation->deprecationReason;
429
            }
430
431 24
            $values[$name] = $valueConfig;
432
        }
433
434 24
        $enumConfiguration = ['values' => $values];
435 24
        $enumConfiguration = self::getDescriptionConfiguration($graphClass->getAnnotations()) + $enumConfiguration;
436
437 24
        return ['type' => 'enum', 'config' => $enumConfiguration];
438
    }
439
440
    /**
441
     * Get a GraphQL Union configuration from given union annotation.
442
     */
443 24
    private static function unionAnnotationToGQLConfiguration(GraphClass $graphClass, GQL\Union $unionAnnotation): array
444
    {
445 24
        $unionConfiguration = [];
446 24
        if (null !== $unionAnnotation->types) {
447 24
            $unionConfiguration['types'] = $unionAnnotation->types;
448
        } else {
449 24
            $types = array_keys(self::searchClassesMapBy(function ($gqlType, $configuration) use ($graphClass) {
450 24
                $typeClassName = $configuration['class'];
451 24
                $typeMetadata = self::getGraphClass($typeClassName);
452
453 24
                if ($graphClass->isInterface() && $typeMetadata->implementsInterface($graphClass->getName())) {
454 24
                    return true;
455
                }
456
457 24
                return $typeMetadata->isSubclassOf($graphClass->getName());
458 24
            }, self::GQL_TYPE));
459 24
            sort($types);
460 24
            $unionConfiguration['types'] = $types;
461
        }
462
463 24
        $unionConfiguration = self::getDescriptionConfiguration($graphClass->getAnnotations()) + $unionConfiguration;
464
465 24
        if ($unionAnnotation->resolveType) {
466 24
            $unionConfiguration['resolveType'] = self::formatExpression($unionAnnotation->resolveType);
467
        } else {
468 24
            if ($graphClass->hasMethod('resolveType')) {
469 24
                $method = $graphClass->getMethod('resolveType');
470 24
                if ($method->isStatic() && $method->isPublic()) {
471 24
                    $unionConfiguration['resolveType'] = self::formatExpression(sprintf("@=call('%s::%s', [service('overblog_graphql.type_resolver'), value], true)", self::formatNamespaceForExpression($graphClass->getName()), 'resolveType'));
472
                } else {
473 24
                    throw new InvalidArgumentException('The "resolveType()" method on class must be static and public. Or you must define a "resolveType" attribute on the @Union annotation.');
474
                }
475
            } else {
476 1
                throw new InvalidArgumentException('The annotation @Union has no "resolveType" attribute and the related class has no "resolveType()" public static method. You need to define of them.');
477
            }
478
        }
479
480 24
        return ['type' => 'union', 'config' => $unionConfiguration];
481
    }
482
483
    /**
484
     * @param ReflectionMethod|ReflectionProperty $reflector
485
     */
486 24
    private static function getTypeFieldConfigurationFromReflector(GraphClass $graphClass, Reflector $reflector, string $fieldAnnotationName = GQL\Field::class, string $currentValue = 'value'): array
487
    {
488 24
        $annotations = $graphClass->getAnnotations($reflector);
489
490 24
        $fieldAnnotation = self::getFirstAnnotationMatching($annotations, $fieldAnnotationName);
491 24
        $accessAnnotation = self::getFirstAnnotationMatching($annotations, GQL\Access::class);
492 24
        $publicAnnotation = self::getFirstAnnotationMatching($annotations, GQL\IsPublic::class);
493
494 24
        if (!$fieldAnnotation) {
495 24
            if ($accessAnnotation || $publicAnnotation) {
496 1
                throw new InvalidArgumentException(sprintf('The annotations "@Access" and/or "@Visible" defined on "%s" are only usable in addition of annotation "@Field"', $reflector->getName()));
497
            }
498
499 24
            return [];
500
        }
501
502 24
        if ($reflector instanceof ReflectionMethod && !$reflector->isPublic()) {
503 1
            throw new InvalidArgumentException(sprintf('The Annotation "@Field" can only be applied to public method. The method "%s" is not public.', $reflector->getName()));
504
        }
505
506 24
        $fieldName = $reflector->getName();
507 24
        $fieldType = $fieldAnnotation->type;
508 24
        $fieldConfiguration = [];
509 24
        if ($fieldType) {
510 24
            $fieldConfiguration['type'] = $fieldType;
511
        }
512
513 24
        $fieldConfiguration = self::getDescriptionConfiguration($annotations, true) + $fieldConfiguration;
514
515 24
        $args = [];
516 24
        if (!empty($fieldAnnotation->args)) {
517 24
            foreach ($fieldAnnotation->args as $arg) {
518 24
                $args[$arg->name] = ['type' => $arg->type]
519 24
                    + (null !== $arg->description ? ['description' => $arg->description] : [])
520 24
                    + (null !== $arg->default ? ['defaultValue' => $arg->default] : []);
521
            }
522 24
        } elseif ($reflector instanceof ReflectionMethod) {
523 24
            $args = self::guessArgs($reflector);
524
        }
525
526 24
        if (!empty($args)) {
527 24
            $fieldConfiguration['args'] = $args;
528
        }
529
530 24
        $fieldName = $fieldAnnotation->name ?: $fieldName;
531
532 24
        if ($fieldAnnotation->resolve) {
533 24
            $fieldConfiguration['resolve'] = self::formatExpression($fieldAnnotation->resolve);
534
        } else {
535 24
            if ($reflector instanceof ReflectionMethod) {
536 24
                $fieldConfiguration['resolve'] = self::formatExpression(sprintf('call(%s.%s, %s)', $currentValue, $reflector->getName(), self::formatArgsForExpression($args)));
537
            } else {
538 24
                if ($fieldName !== $reflector->getName() || 'value' !== $currentValue) {
539
                    $fieldConfiguration['resolve'] = self::formatExpression(sprintf('%s.%s', $currentValue, $reflector->getName()));
540
                }
541
            }
542
        }
543
544 24
        if ($fieldAnnotation->argsBuilder) {
545 24
            if (is_string($fieldAnnotation->argsBuilder)) {
546
                $fieldConfiguration['argsBuilder'] = $fieldAnnotation->argsBuilder;
547 24
            } elseif (is_array($fieldAnnotation->argsBuilder)) {
548 24
                list($builder, $builderConfig) = $fieldAnnotation->argsBuilder;
549 24
                $fieldConfiguration['argsBuilder'] = ['builder' => $builder, 'config' => $builderConfig];
550
            } else {
551
                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()));
552
            }
553
        }
554
555 24
        if ($fieldAnnotation->fieldBuilder) {
556 24
            if (is_string($fieldAnnotation->fieldBuilder)) {
557
                $fieldConfiguration['builder'] = $fieldAnnotation->fieldBuilder;
558 24
            } elseif (is_array($fieldAnnotation->fieldBuilder)) {
559 24
                list($builder, $builderConfig) = $fieldAnnotation->fieldBuilder;
560 24
                $fieldConfiguration['builder'] = $builder;
561 24
                $fieldConfiguration['builderConfig'] = $builderConfig ?: [];
562
            } else {
563 24
                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()));
564
            }
565
        } else {
566 24
            if (!$fieldType) {
567 24
                if ($reflector instanceof ReflectionMethod) {
568
                    /** @var ReflectionMethod $reflector */
569 24
                    if ($reflector->hasReturnType()) {
570
                        try {
571
                            // @phpstan-ignore-next-line
572 24
                            $fieldConfiguration['type'] = self::resolveGraphQLTypeFromReflectionType($reflector->getReturnType(), self::VALID_OUTPUT_TYPES);
573
                        } catch (Exception $e) {
574 24
                            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()));
575
                        }
576
                    } else {
577 24
                        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()));
578
                    }
579
                } else {
580
                    try {
581 24
                        $fieldConfiguration['type'] = self::guessType($graphClass, $annotations);
582 2
                    } catch (Exception $e) {
583 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()));
584
                    }
585
                }
586
            }
587
        }
588
589 24
        if (null !== $accessAnnotation) {
590 24
            if (isset($accessAnnotation->value)) {
591 24
                $fieldConfiguration['access'] = self::formatExpression($accessAnnotation->value);
592
            }
593 24
            if (isset($accessAnnotation->nullOnDenied)) {
594 24
                $fieldConfiguration['accessConfig'] = ['nullOnDenied' => $accessAnnotation->nullOnDenied];
595
            }
596
        }
597
598 24
        if ($publicAnnotation) {
599 24
            $fieldConfiguration['public'] = self::formatExpression($publicAnnotation->value);
600
        }
601
602 24
        if ($fieldAnnotation->complexity) {
603 24
            $fieldConfiguration['complexity'] = self::formatExpression($fieldAnnotation->complexity);
604
        }
605
606 24
        return [$fieldName => $fieldConfiguration];
607
    }
608
609
    /**
610
     * Create GraphQL input fields configuration based on annotations.
611
     *
612
     * @param ReflectionProperty[] $reflectors
613
     */
614 24
    private static function getGraphQLInputFieldsFromAnnotations(GraphClass $graphClass, array $reflectors): array
615
    {
616 24
        $fields = [];
617
618 24
        foreach ($reflectors as $reflector) {
619 24
            $annotations = $graphClass->getAnnotations($reflector);
620 24
            $fieldAnnotation = self::getFirstAnnotationMatching($annotations, GQL\Field::class);
621
622
            // Ignore field with resolver when the type is an Input
623 24
            if ($fieldAnnotation->resolve) {
624
                return [];
625
            }
626
627 24
            $fieldName = $reflector->getName();
628 24
            $fieldType = $fieldAnnotation->type;
629 24
            $fieldConfiguration = [];
630 24
            if ($fieldType) {
631 24
                $resolvedType = self::resolveClassFromType($fieldType);
632
                // We found a type but it is not allowed
633 24
                if (null !== $resolvedType && !in_array($resolvedType['type'], self::VALID_INPUT_TYPES)) {
634
                    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']));
635
                }
636
637 24
                $fieldConfiguration['type'] = $fieldType;
638
            }
639
640 24
            $fieldConfiguration = array_merge(self::getDescriptionConfiguration($annotations, true), $fieldConfiguration);
641 24
            $fields[$fieldName] = $fieldConfiguration;
642
        }
643
644 24
        return $fields;
645
    }
646
647
    /**
648
     * Create GraphQL type fields configuration based on annotations.
649
     *
650
     * @param ReflectionProperty[]|ReflectionMethod[] $reflectors
651
     */
652 25
    private static function getGraphQLTypeFieldsFromAnnotations(GraphClass $graphClass, array $reflectors, string $fieldAnnotationName = GQL\Field::class, string $currentValue = 'value'): array
653
    {
654 25
        $fields = [];
655
656 25
        foreach ($reflectors as $reflector) {
657 24
            $fields = array_merge($fields, self::getTypeFieldConfigurationFromReflector($graphClass, $reflector, $fieldAnnotationName, $currentValue));
658
        }
659
660 25
        return $fields;
661
    }
662
663
    /**
664
     * Return fields config from Provider methods.
665
     * Loop through configured provider and extract fields targeting the targetType.
666
     */
667 25
    private static function getGraphQLFieldsFromProviders(GraphClass $graphClass, string $expectedAnnotation, string $targetType, bool $isDefaultTarget = false): array
668
    {
669 25
        $fields = [];
670 25
        foreach (self::$providers as ['metadata' => $providerMetadata, 'annotation' => $providerAnnotation]) {
671 24
            $defaultAccessAnnotation = self::getFirstAnnotationMatching($providerMetadata->getAnnotations(), GQL\Access::class);
672 24
            $defaultIsPublicAnnotation = self::getFirstAnnotationMatching($providerMetadata->getAnnotations(), GQL\IsPublic::class);
673
674 24
            $defaultAccess = isset($defaultAccessAnnotation->value) ? self::formatExpression($defaultAccessAnnotation->value) : false;
675 24
            $defaultAccessConfig = isset($defaultAccessAnnotation->nullOnDenied) ? ['nullOnDenied' => $defaultAccessAnnotation->nullOnDenied] : false;
676 24
            $defaultIsPublic = $defaultIsPublicAnnotation ? self::formatExpression($defaultIsPublicAnnotation->value) : false;
677
678 24
            $methods = [];
679
            // First found the methods matching the targeted type
680 24
            foreach ($providerMetadata->getMethods() as $method) {
681 24
                $annotations = $providerMetadata->getAnnotations($method);
682
683 24
                $annotation = self::getFirstAnnotationMatching($annotations, [GQL\Mutation::class, GQL\Query::class]);
684 24
                if (!$annotation) {
685
                    continue;
686
                }
687
688 24
                $annotationTargets = $annotation->targetType;
689
690 24
                if (null === $annotationTargets) {
691 24
                    if ($isDefaultTarget) {
692 24
                        $annotationTargets = [$targetType];
693 24
                        if (!$annotation instanceof $expectedAnnotation) {
694 24
                            continue;
695
                        }
696
                    } else {
697 24
                        continue;
698
                    }
699
                }
700
701 24
                if (is_string($annotationTargets)) {
702
                    $annotationTargets = [$annotationTargets];
703
                }
704
705 24
                if (!in_array($targetType, $annotationTargets)) {
706 24
                    continue;
707
                }
708
709 24
                if (!$annotation instanceof $expectedAnnotation) {
710 2
                    if (GQL\Mutation::class == $expectedAnnotation) {
711 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);
712
                    } else {
713 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);
714
                    }
715
716 2
                    throw new InvalidArgumentException($message);
717
                }
718 24
                $methods[$method->getName()] = $method;
719
            }
720
721 24
            $currentValue = sprintf("service('%s')", self::formatNamespaceForExpression($providerMetadata->getName()));
722 24
            $providerFields = self::getGraphQLTypeFieldsFromAnnotations($graphClass, $methods, $expectedAnnotation, $currentValue);
723 24
            foreach ($providerFields as $fieldName => $fieldConfig) {
724 24
                if ($providerAnnotation->prefix) {
725 24
                    $fieldName = sprintf('%s%s', $providerAnnotation->prefix, $fieldName);
726
                }
727
728 24
                if ($defaultAccess && !isset($fieldConfig['access'])) {
729 24
                    $fieldConfig['access'] = $defaultAccess;
730
                }
731
732 24
                if ($defaultAccessConfig && !isset($fieldConfig['accessConfig'])) {
733 24
                    $fieldConfig['accessConfig'] = $defaultAccessConfig;
734
                }
735
736 24
                if ($defaultIsPublic && !isset($fieldConfig['public'])) {
737 24
                    $fieldConfig['public'] = $defaultIsPublic;
738
                }
739
740 24
                $fields[$fieldName] = $fieldConfig;
741
            }
742
        }
743
744 25
        return $fields;
745
    }
746
747
    /**
748
     * Get the config for description & deprecation reason.
749
     */
750 25
    private static function getDescriptionConfiguration(array $annotations, bool $withDeprecation = false): array
751
    {
752 25
        $config = [];
753 25
        $descriptionAnnotation = self::getFirstAnnotationMatching($annotations, GQL\Description::class);
754 25
        if ($descriptionAnnotation) {
755 24
            $config['description'] = $descriptionAnnotation->value;
756
        }
757
758 25
        if ($withDeprecation) {
759 24
            $deprecatedAnnotation = self::getFirstAnnotationMatching($annotations, GQL\Deprecated::class);
760 24
            if ($deprecatedAnnotation) {
761 24
                $config['deprecationReason'] = $deprecatedAnnotation->value;
762
            }
763
        }
764
765 25
        return $config;
766
    }
767
768
    /**
769
     * Format an array of args to a list of arguments in an expression.
770
     */
771 24
    private static function formatArgsForExpression(array $args): string
772
    {
773 24
        $mapping = [];
774 24
        foreach ($args as $name => $config) {
775 24
            $mapping[] = sprintf('%s: "%s"', $name, $config['type']);
776
        }
777
778 24
        return sprintf('arguments({%s}, args)', implode(', ', $mapping));
779
    }
780
781
    /**
782
     * Format a namespace to be used in an expression (double escape).
783
     */
784 24
    private static function formatNamespaceForExpression(string $namespace): string
785
    {
786 24
        return str_replace('\\', '\\\\', $namespace);
787
    }
788
789
    /**
790
     * Get the first annotation matching given class.
791
     *
792
     * @param string|array $annotationClass
793
     *
794
     * @return mixed
795
     */
796 25
    private static function getFirstAnnotationMatching(array $annotations, $annotationClass)
797
    {
798 25
        if (is_string($annotationClass)) {
799 25
            $annotationClass = [$annotationClass];
800
        }
801
802 25
        foreach ($annotations as $annotation) {
803 25
            foreach ($annotationClass as $class) {
804 25
                if ($annotation instanceof $class) {
805 24
                    return $annotation;
806
                }
807
            }
808
        }
809
810 25
        return false;
811
    }
812
813
    /**
814
     * Format an expression (ie. add "@=" if not set).
815
     */
816 24
    private static function formatExpression(string $expression): string
817
    {
818 24
        return '@=' === substr($expression, 0, 2) ? $expression : sprintf('@=%s', $expression);
819
    }
820
821
    /**
822
     * Suffix a name if it is not already.
823
     */
824 24
    private static function suffixName(string $name, string $suffix): string
825
    {
826 24
        return substr($name, -strlen($suffix)) === $suffix ? $name : sprintf('%s%s', $name, $suffix);
827
    }
828
829
    /**
830
     * Try to guess a field type base on his annotations.
831
     *
832
     * @throws RuntimeException
833
     */
834 24
    private static function guessType(GraphClass $graphClass, array $annotations): string
835
    {
836 24
        $columnAnnotation = self::getFirstAnnotationMatching($annotations, Column::class);
837 24
        if ($columnAnnotation) {
838 24
            $type = self::resolveTypeFromDoctrineType($columnAnnotation->type);
839 24
            $nullable = $columnAnnotation->nullable;
840 24
            if ($type) {
841 24
                return $nullable ? $type : sprintf('%s!', $type);
842
            } else {
843 1
                throw new RuntimeException(sprintf('Unable to auto-guess GraphQL type from Doctrine type "%s"', $columnAnnotation->type));
844
            }
845
        }
846
847
        $associationAnnotations = [
848 24
            OneToMany::class => true,
849
            OneToOne::class => false,
850
            ManyToMany::class => true,
851
            ManyToOne::class => false,
852
        ];
853
854 24
        $associationAnnotation = self::getFirstAnnotationMatching($annotations, array_keys($associationAnnotations));
855 24
        if ($associationAnnotation) {
856 24
            $target = self::fullyQualifiedClassName($associationAnnotation->targetEntity, $graphClass->getNamespaceName());
857 24
            $type = self::resolveTypeFromClass($target, ['type']);
858
859 24
            if ($type) {
860 24
                $isMultiple = $associationAnnotations[get_class($associationAnnotation)];
861 24
                if ($isMultiple) {
862 24
                    return sprintf('[%s]!', $type);
863
                } else {
864 24
                    $isNullable = false;
865 24
                    $joinColumn = self::getFirstAnnotationMatching($annotations, JoinColumn::class);
866 24
                    if ($joinColumn) {
867 24
                        $isNullable = $joinColumn->nullable;
868
                    }
869
870 24
                    return sprintf('%s%s', $type, $isNullable ? '' : '!');
871
                }
872
            } else {
873 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));
874
            }
875
        }
876
877
        throw new InvalidArgumentException('No Doctrine ORM annotation found.');
878
    }
879
880
    /**
881
     * Resolve a FQN from classname and namespace.
882
     *
883
     * @internal
884
     */
885 24
    public static function fullyQualifiedClassName(string $className, string $namespace): string
886
    {
887 24
        if (false === strpos($className, '\\') && $namespace) {
888 24
            return $namespace.'\\'.$className;
889
        }
890
891 1
        return $className;
892
    }
893
894
    /**
895
     * Resolve a GraphQLType from a doctrine type.
896
     */
897 24
    private static function resolveTypeFromDoctrineType(string $doctrineType): ?string
898
    {
899 24
        if (isset(self::$doctrineMapping[$doctrineType])) {
900 24
            return self::$doctrineMapping[$doctrineType];
901
        }
902
903
        switch ($doctrineType) {
904 24
            case 'integer':
905 24
            case 'smallint':
906 24
            case 'bigint':
907 24
                return 'Int';
908 24
            case 'string':
909 1
            case 'text':
910 24
                return 'String';
911 1
            case 'bool':
912 1
            case 'boolean':
913
                return 'Boolean';
914 1
            case 'float':
915 1
            case 'decimal':
916
                return 'Float';
917
            default:
918 1
                return null;
919
        }
920
    }
921
922
    /**
923
     * Transform a method arguments from reflection to a list of GraphQL argument.
924
     */
925 24
    private static function guessArgs(ReflectionMethod $method): array
926
    {
927 24
        $arguments = [];
928 24
        foreach ($method->getParameters() as $index => $parameter) {
929 24
            if (!$parameter->hasType()) {
930 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()));
931
            }
932
933
            try {
934
                // @phpstan-ignore-next-line
935 24
                $gqlType = self::resolveGraphQLTypeFromReflectionType($parameter->getType(), self::VALID_INPUT_TYPES, $parameter->isDefaultValueAvailable());
936
            } catch (Exception $e) {
937
                throw new InvalidArgumentException(sprintf('Argument n°%s "$%s" on method "%s" cannot be auto-guessed : %s".', $index + 1, $parameter->getName(), $method->getName(), $e->getMessage()));
938
            }
939
940 24
            $argumentConfig = [];
941 24
            if ($parameter->isDefaultValueAvailable()) {
942 24
                $argumentConfig['defaultValue'] = $parameter->getDefaultValue();
943
            }
944
945 24
            $argumentConfig['type'] = $gqlType;
946
947 24
            $arguments[$parameter->getName()] = $argumentConfig;
948
        }
949
950 24
        return $arguments;
951
    }
952
953 24
    private static function resolveGraphQLTypeFromReflectionType(ReflectionNamedType $type, array $filterGraphQLTypes = [], bool $isOptional = false): string
954
    {
955 24
        $sType = $type->getName();
956 24
        if ($type->isBuiltin()) {
957 24
            $gqlType = self::resolveTypeFromPhpType($sType);
958 24
            if (null === $gqlType) {
959 24
                throw new RuntimeException(sprintf('No corresponding GraphQL type found for builtin type "%s"', $sType));
960
            }
961
        } else {
962 24
            $gqlType = self::resolveTypeFromClass($sType, $filterGraphQLTypes);
963 24
            if (null === $gqlType) {
964
                throw new RuntimeException(sprintf('No corresponding GraphQL %s found for class "%s"', $filterGraphQLTypes ? implode(',', $filterGraphQLTypes) : 'object', $sType));
965
            }
966
        }
967
968 24
        return sprintf('%s%s', $gqlType, ($type->allowsNull() || $isOptional) ? '' : '!');
969
    }
970
971
    /**
972
     * Resolve a GraphQL Type from a class name.
973
     */
974 24
    private static function resolveTypeFromClass(string $className, array $wantedTypes = []): ?string
975
    {
976 24
        foreach (self::$classesMap as $gqlType => $config) {
977 24
            if ($config['class'] === $className) {
978 24
                if (in_array($config['type'], $wantedTypes)) {
979 24
                    return $gqlType;
980
                }
981
            }
982
        }
983
984 1
        return null;
985
    }
986
987
    /**
988
     * Resolve a PHP class from a GraphQL type.
989
     *
990
     * @return string|array|null
991
     */
992 24
    private static function resolveClassFromType(string $type)
993
    {
994 24
        return self::$classesMap[$type] ?? null;
995
    }
996
997
    /**
998
     * Search the classes map for class by predicate.
999
     *
1000
     * @return array
1001
     */
1002 25
    private static function searchClassesMapBy(callable $predicate, string $type = null)
1003
    {
1004 25
        $classNames = [];
1005 25
        foreach (self::$classesMap as $gqlType => $config) {
1006 25
            if ($type && $config['type'] !== $type) {
1007 25
                continue;
1008
            }
1009
1010 24
            if ($predicate($gqlType, $config)) {
1011 24
                $classNames[$gqlType] = $config;
1012
            }
1013
        }
1014
1015 25
        return $classNames;
1016
    }
1017
1018
    /**
1019
     * Convert a PHP Builtin type to a GraphQL type.
1020
     */
1021 24
    private static function resolveTypeFromPhpType(string $phpType): ?string
1022
    {
1023
        switch ($phpType) {
1024 24
            case 'boolean':
1025 24
            case 'bool':
1026 24
                return 'Boolean';
1027 24
            case 'integer':
1028 24
            case 'int':
1029 24
                return 'Int';
1030 24
            case 'float':
1031 24
            case 'double':
1032 24
                return 'Float';
1033 24
            case 'string':
1034 24
                return 'String';
1035
            default:
1036
                return null;
1037
        }
1038
    }
1039
}
1040