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 (#482)
by Maxim
15:58
created

AnnotationParser::getArgs()   B

Complexity

Conditions 7
Paths 3

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 7

Importance

Changes 0
Metric Value
eloc 9
dl 0
loc 14
ccs 10
cts 10
cp 1
rs 8.8333
c 0
b 0
f 0
cc 7
nc 3
nop 2
crap 7
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Overblog\GraphQLBundle\Config\Parser;
6
7
use Doctrine\Common\Annotations\AnnotationReader;
8
use Doctrine\Common\Annotations\AnnotationRegistry;
9
use Doctrine\ORM\Mapping\Column;
10
use Doctrine\ORM\Mapping\JoinColumn;
11
use Doctrine\ORM\Mapping\ManyToMany;
12
use Doctrine\ORM\Mapping\ManyToOne;
13
use Doctrine\ORM\Mapping\OneToMany;
14
use Doctrine\ORM\Mapping\OneToOne;
15
use Overblog\GraphQLBundle\Annotation as GQL;
16
use Overblog\GraphQLBundle\Relay\Connection\ConnectionInterface;
17
use Overblog\GraphQLBundle\Relay\Connection\EdgeInterface;
18
use Symfony\Component\Config\Resource\FileResource;
19
use Symfony\Component\DependencyInjection\ContainerBuilder;
20
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
21
22
class AnnotationParser implements PreParserInterface
23
{
24
    private static $annotationReader = null;
25
    private static $classesMap = [];
26
    private static $providers = [];
27
    private static $doctrineMapping = [];
28
    private static $classAnnotationsCache = [];
29
30
    private const GQL_SCALAR = 'scalar';
31
    private const GQL_ENUM = 'enum';
32
    private const GQL_TYPE = 'type';
33
    private const GQL_INPUT = 'input';
34
    private const GQL_UNION = 'union';
35
    private const GQL_INTERFACE = 'interface';
36
37
    /**
38
     * @see https://facebook.github.io/graphql/draft/#sec-Input-and-Output-Types
39
     */
40
    private const VALID_INPUT_TYPES = [self::GQL_SCALAR, self::GQL_ENUM, self::GQL_INPUT];
41
    private const VALID_OUTPUT_TYPES = [self::GQL_SCALAR, self::GQL_TYPE, self::GQL_INTERFACE, self::GQL_UNION, self::GQL_ENUM];
42
43
    /**
44
     * {@inheritdoc}
45
     *
46
     * @throws \ReflectionException
47
     * @throws InvalidArgumentException
48
     */
49 20
    public static function preParse(\SplFileInfo $file, ContainerBuilder $container, array $configs = []): void
50
    {
51 20
        self::processFile($file, $container, $configs, true);
52 20
    }
53
54
    /**
55
     * {@inheritdoc}
56
     *
57
     * @throws \ReflectionException
58
     * @throws InvalidArgumentException
59
     */
60 20
    public static function parse(\SplFileInfo $file, ContainerBuilder $container, array $configs = []): array
61
    {
62 20
        return self::processFile($file, $container, $configs, false);
63
    }
64
65
    /**
66
     * Clear the Annotation parser.
67
     *
68
     * @internal
69
     */
70 19
    public static function clear(): void
71
    {
72 19
        self::$classesMap = [];
73 19
        self::$providers = [];
74 19
        self::$classAnnotationsCache = [];
75 19
        self::$annotationReader = null;
76 19
    }
77
78
    /**
79
     * Process a file.
80
     *
81
     * @param \SplFileInfo     $file
82
     * @param ContainerBuilder $container
83
     * @param array            $configs
84
     * @param bool             $preProcess
85
     *
86
     * @return array
87
     *
88
     * @throws \ReflectionException
89
     * @throws InvalidArgumentException
90
     */
91 20
    private static function processFile(\SplFileInfo $file, ContainerBuilder $container, array $configs, bool $preProcess): array
92
    {
93 20
        self::$doctrineMapping = $configs['doctrine']['types_mapping'];
94 20
        $container->addResource(new FileResource($file->getRealPath()));
95
96
        try {
97 20
            $className = $file->getBasename('.php');
98 20
            if (\preg_match('#namespace (.+);#', \file_get_contents($file->getRealPath()), $matches)) {
99 20
                $className = \trim($matches[1]).'\\'.$className;
100
            }
101 20
            [$reflectionEntity, $classAnnotations, $properties, $methods] = self::extractClassAnnotations($className);
102 20
            $gqlTypes = [];
103
104 20
            foreach ($classAnnotations as $classAnnotation) {
105 20
                $gqlTypes = self::classAnnotationsToGQLConfiguration(
106 20
                    $reflectionEntity,
107 20
                    $classAnnotation,
108 20
                    $configs,
109 20
                    $classAnnotations,
110 20
                    $properties,
111 20
                    $methods,
112 20
                    $gqlTypes,
113 20
                    $preProcess
114
                );
115
            }
116
117 20
            return $preProcess ? self::$classesMap : $gqlTypes;
118 8
        } catch (\InvalidArgumentException $e) {
119 8
            throw new InvalidArgumentException(\sprintf('Failed to parse GraphQL annotations from file "%s".', $file), $e->getCode(), $e);
120
        }
121
    }
122
123
    /**
124
     * @param \ReflectionClass $reflectionEntity
125
     * @param array            $configs
126
     * @param object           $classAnnotation
127
     * @param array            $classAnnotations
128
     * @param array            $properties
129
     * @param array            $methods
130
     * @param array            $gqlTypes
131
     * @param bool             $preProcess
132
     *
133
     * @return array
134
     */
135 20
    private static function classAnnotationsToGQLConfiguration(
136
        \ReflectionClass $reflectionEntity,
137
        $classAnnotation,
138
        array $configs,
139
        array $classAnnotations,
140
        array $properties,
141
        array $methods,
142
        array $gqlTypes,
143
        bool $preProcess
144
    ): array {
145 20
        $gqlConfiguration = $gqlType = $gqlName = null;
146
147
        switch (true) {
148 20
            case $classAnnotation instanceof GQL\Type:
149 20
                $gqlType = self::GQL_TYPE;
150 20
                $gqlName = $classAnnotation->name ?: $reflectionEntity->getShortName();
151 20
                if (!$preProcess) {
152 20
                    $gqlConfiguration = self::typeAnnotationToGQLConfiguration(
153 20
                        $reflectionEntity, $classAnnotation, $gqlName, $classAnnotations, $properties, $methods, $configs
154
                    );
155
156 20
                    if ($classAnnotation instanceof GQL\Relay\Connection) {
157 19
                        if (!$reflectionEntity->implementsInterface(ConnectionInterface::class)) {
158
                            throw new InvalidArgumentException(\sprintf('The annotation @Connection on class "%s" can only be used on class implementing the ConnectionInterface.', $reflectionEntity->getName()));
159
                        }
160
161 19
                        if (!($classAnnotation->edge xor $classAnnotation->node)) {
162
                            throw new InvalidArgumentException(\sprintf('The annotation @Connection on class "%s" is invalid. You must define the "edge" OR the "node" attribute.', $reflectionEntity->getName()));
163
                        }
164
165 19
                        $edgeType = $classAnnotation->edge;
166 19
                        if (!$edgeType) {
167 19
                            $edgeType = \sprintf('%sEdge', $gqlName);
168 19
                            $gqlTypes[$edgeType] = [
169 19
                                'type' => 'object',
170
                                'config' => [
171
                                    'builders' => [
172 19
                                        ['builder' => 'relay-edge', 'builderConfig' => ['nodeType' => $classAnnotation->node]],
173
                                    ],
174
                                ],
175
                            ];
176
                        }
177 19
                        if (!isset($gqlConfiguration['config']['builders'])) {
178 19
                            $gqlConfiguration['config']['builders'] = [];
179
                        }
180 19
                        \array_unshift($gqlConfiguration['config']['builders'], ['builder' => 'relay-connection', 'builderConfig' => ['edgeType' => $edgeType]]);
181
                    }
182
                }
183 20
                break;
184
185 19
            case $classAnnotation instanceof GQL\Input:
186 19
                $gqlType = self::GQL_INPUT;
187 19
                $gqlName = $classAnnotation->name ?: self::suffixName($reflectionEntity->getShortName(), 'Input');
188 19
                if (!$preProcess) {
189 19
                    $gqlConfiguration = self::inputAnnotationToGQLConfiguration(
190 19
                        $classAnnotation, $classAnnotations, $properties, $reflectionEntity->getNamespaceName()
191
                    );
192
                }
193 19
                break;
194
195 19
            case $classAnnotation instanceof GQL\Scalar:
196 19
                $gqlType = self::GQL_SCALAR;
197 19
                if (!$preProcess) {
198 19
                    $gqlConfiguration = self::scalarAnnotationToGQLConfiguration(
199 19
                        $reflectionEntity->getName(), $classAnnotation, $classAnnotations
200
                    );
201
                }
202 19
                break;
203
204 19
            case $classAnnotation instanceof GQL\Enum:
205 19
                $gqlType = self::GQL_ENUM;
206 19
                if (!$preProcess) {
207 19
                    $gqlConfiguration = self::enumAnnotationToGQLConfiguration(
208 19
                        $classAnnotation, $classAnnotations, $reflectionEntity->getConstants()
209
                    );
210
                }
211 19
                break;
212
213 19
            case $classAnnotation instanceof GQL\Union:
214 19
                $gqlType = self::GQL_UNION;
215 19
                if (!$preProcess) {
216 19
                    $gqlConfiguration = self::unionAnnotationToGQLConfiguration(
217 19
                        $reflectionEntity->getName(), $classAnnotation, $classAnnotations, $methods
218
                    );
219
                }
220 19
                break;
221
222 19
            case $classAnnotation instanceof GQL\TypeInterface:
223 19
                $gqlType = self::GQL_INTERFACE;
224 19
                if (!$preProcess) {
225 19
                    $gqlConfiguration = self::typeInterfaceAnnotationToGQLConfiguration(
226 19
                        $classAnnotation, $classAnnotations, $properties, $methods, $reflectionEntity->getNamespaceName()
227
                    );
228
                }
229 19
                break;
230
231 19
            case $classAnnotation instanceof GQL\Provider:
232 19
                if ($preProcess) {
233 19
                    self::$providers[$reflectionEntity->getName()] = ['annotation' => $classAnnotation, 'methods' => $methods];
234
                }
235 19
                break;
236
        }
237
238 20
        if (null !== $gqlType) {
239 20
            if (!$gqlName) {
240 19
                $gqlName = $classAnnotation->name ?: $reflectionEntity->getShortName();
0 ignored issues
show
Bug introduced by
The property name does not seem to exist on Overblog\GraphQLBundle\Annotation\Provider.
Loading history...
241
            }
242
243 20
            if ($preProcess) {
244 20
                if (isset(self::$classesMap[$gqlName])) {
245 1
                    throw new InvalidArgumentException(\sprintf('The GraphQL type "%s" has already been registered in class "%s"', $gqlName, self::$classesMap[$gqlName]['class']));
246
                }
247 20
                self::$classesMap[$gqlName] = ['type' => $gqlType, 'class' => $reflectionEntity->getName()];
248
            } else {
249 20
                $gqlTypes = [$gqlName => $gqlConfiguration] + $gqlTypes;
250
            }
251
        }
252
253 20
        return $gqlTypes;
254
    }
255
256 20
    private static function extractClassAnnotations(string $className): array
257
    {
258 20
        if (!isset(self::$classAnnotationsCache[$className])) {
259 20
            $annotationReader = self::getAnnotationReader();
260 20
            $reflectionEntity = new \ReflectionClass($className);
261 20
            $classAnnotations = $annotationReader->getClassAnnotations($reflectionEntity);
262
263 20
            $properties = [];
264 20
            foreach ($reflectionEntity->getProperties() as $property) {
265 19
                $properties[$property->getName()] = ['property' => $property, 'annotations' => $annotationReader->getPropertyAnnotations($property)];
266
            }
267
268 20
            $methods = [];
269 20
            foreach ($reflectionEntity->getMethods() as $method) {
270 19
                $methods[$method->getName()] = ['method' => $method, 'annotations' => $annotationReader->getMethodAnnotations($method)];
271
            }
272
273 20
            self::$classAnnotationsCache[$className] = [$reflectionEntity, $classAnnotations, $properties, $methods];
274
        }
275
276 20
        return self::$classAnnotationsCache[$className];
277
    }
278
279 20
    private static function typeAnnotationToGQLConfiguration(
280
        \ReflectionClass $reflectionEntity,
281
        GQL\Type $classAnnotation,
282
        string $gqlName,
283
        array $classAnnotations,
284
        array $properties,
285
        array $methods,
286
        array $configs
287
    ): array {
288 20
        $rootQueryType = $configs['definitions']['schema']['default']['query'] ?? null;
289 20
        $rootMutationType = $configs['definitions']['schema']['default']['mutation'] ?? null;
290 20
        $isRootQuery = ($rootQueryType && $gqlName === $rootQueryType);
291 20
        $isRootMutation = ($rootMutationType && $gqlName === $rootMutationType);
292 20
        $currentValue = ($isRootQuery || $isRootMutation) ? \sprintf("service('%s')", self::formatNamespaceForExpression($reflectionEntity->getName())) : 'value';
293
294 20
        $gqlConfiguration = self::graphQLTypeConfigFromAnnotation($classAnnotation, $classAnnotations, $properties, $methods, $reflectionEntity->getNamespaceName(), $currentValue);
295 20
        $providerFields = self::getGraphQLFieldsFromProviders($reflectionEntity->getNamespaceName(), $isRootMutation ? 'Mutation' : 'Query', $gqlName, ($isRootQuery || $isRootMutation));
296 20
        $gqlConfiguration['config']['fields'] = $providerFields + $gqlConfiguration['config']['fields'];
297
298 20
        if ($classAnnotation instanceof GQL\Relay\Edge) {
299 19
            if (!$reflectionEntity->implementsInterface(EdgeInterface::class)) {
300
                throw new InvalidArgumentException(\sprintf('The annotation @Edge on class "%s" can only be used on class implementing the EdgeInterface.', $reflectionEntity->getName()));
301
            }
302 19
            if (!isset($gqlConfiguration['config']['builders'])) {
303 19
                $gqlConfiguration['config']['builders'] = [];
304
            }
305 19
            \array_unshift($gqlConfiguration['config']['builders'], ['builder' => 'relay-edge', 'builderConfig' => ['nodeType' => $classAnnotation->node]]);
306
        }
307
308 20
        return $gqlConfiguration;
309
    }
310
311 20
    private static function getAnnotationReader()
312
    {
313 20
        if (null === self::$annotationReader) {
314 19
            if (!\class_exists('\\Doctrine\\Common\\Annotations\\AnnotationReader') ||
315 19
                !\class_exists('\\Doctrine\\Common\\Annotations\\AnnotationRegistry')) {
316
                throw new \RuntimeException('In order to use graphql annotation, you need to require doctrine annotations');
317
            }
318
319 19
            AnnotationRegistry::registerLoader('class_exists');
0 ignored issues
show
Deprecated Code introduced by
The function Doctrine\Common\Annotati...istry::registerLoader() has been deprecated: this method is deprecated and will be removed in doctrine/annotations 2.0 autoloading should be deferred to the globally registered autoloader by then. For now, use @example AnnotationRegistry::registerLoader('class_exists') ( Ignorable by Annotation )

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

319
            /** @scrutinizer ignore-deprecated */ AnnotationRegistry::registerLoader('class_exists');

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
320 19
            self::$annotationReader = new AnnotationReader();
321
        }
322
323 20
        return self::$annotationReader;
324
    }
325
326 20
    private static function graphQLTypeConfigFromAnnotation(GQL\Type $typeAnnotation, array $classAnnotations, array $properties, array $methods, string $namespace, string $currentValue): array
327
    {
328 20
        $typeConfiguration = [];
329
330 20
        $fields = self::getGraphQLFieldsFromAnnotations($namespace, $properties, false, false, $currentValue);
331 20
        $fields = self::getGraphQLFieldsFromAnnotations($namespace, $methods, false, true, $currentValue) + $fields;
332
333 20
        $typeConfiguration['fields'] = $fields;
334 20
        $typeConfiguration = self::getDescriptionConfiguration($classAnnotations) + $typeConfiguration;
335
336 20
        if ($typeAnnotation->interfaces) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $typeAnnotation->interfaces of type string[] 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...
337 19
            $typeConfiguration['interfaces'] = $typeAnnotation->interfaces;
338
        }
339
340 20
        if ($typeAnnotation->resolveField) {
341 19
            $typeConfiguration['resolveField'] = self::formatExpression($typeAnnotation->resolveField);
342
        }
343
344 20
        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...
345
            $typeConfiguration['builders'] = \array_map(function ($fieldsBuilderAnnotation) {
346 19
                return ['builder' => $fieldsBuilderAnnotation->builder, 'builderConfig' => $fieldsBuilderAnnotation->builderConfig];
347 19
            }, $typeAnnotation->builders);
348
        }
349
350 20
        $publicAnnotation = self::getFirstAnnotationMatching($classAnnotations, GQL\IsPublic::class);
351 20
        if ($publicAnnotation) {
352 19
            $typeConfiguration['fieldsDefaultPublic'] = self::formatExpression($publicAnnotation->value);
353
        }
354
355 20
        $accessAnnotation = self::getFirstAnnotationMatching($classAnnotations, GQL\Access::class);
356 20
        if ($accessAnnotation) {
357 19
            $typeConfiguration['fieldsDefaultAccess'] = self::formatExpression($accessAnnotation->value);
358
        }
359
360 20
        return ['type' => $typeAnnotation->isRelay ? 'relay-mutation-payload' : 'object', 'config' => $typeConfiguration];
361
    }
362
363
    /**
364
     * Create a GraphQL Interface type configuration from annotations on properties.
365
     *
366
     * @param GQL\TypeInterface $interfaceAnnotation
367
     * @param array             $classAnnotations
368
     * @param array             $properties
369
     * @param array             $methods
370
     * @param string            $namespace
371
     *
372
     * @return array
373
     */
374 19
    private static function typeInterfaceAnnotationToGQLConfiguration(GQL\TypeInterface $interfaceAnnotation, array $classAnnotations, array $properties, array $methods, string $namespace)
375
    {
376 19
        $interfaceConfiguration = [];
377
378 19
        $fields = self::getGraphQLFieldsFromAnnotations($namespace, $properties);
379 19
        $fields = self::getGraphQLFieldsFromAnnotations($namespace, $methods, false, true) + $fields;
380
381 19
        $interfaceConfiguration['fields'] = $fields;
382 19
        $interfaceConfiguration = self::getDescriptionConfiguration($classAnnotations) + $interfaceConfiguration;
383
384 19
        $interfaceConfiguration['resolveType'] = self::formatExpression($interfaceAnnotation->resolveType);
385
386 19
        return ['type' => 'interface', 'config' => $interfaceConfiguration];
387
    }
388
389
    /**
390
     * Create a GraphQL Input type configuration from annotations on properties.
391
     *
392
     * @param GQL\Input $inputAnnotation
393
     * @param array     $classAnnotations
394
     * @param array     $properties
395
     * @param string    $namespace
396
     *
397
     * @return array
398
     */
399 19
    private static function inputAnnotationToGQLConfiguration(GQL\Input $inputAnnotation, array $classAnnotations, array $properties, string $namespace): array
400
    {
401 19
        $inputConfiguration = [];
402 19
        $fields = self::getGraphQLFieldsFromAnnotations($namespace, $properties, true);
403
404 19
        $inputConfiguration['fields'] = $fields;
405 19
        $inputConfiguration = self::getDescriptionConfiguration($classAnnotations) + $inputConfiguration;
406
407 19
        return ['type' => $inputAnnotation->isRelay ? 'relay-mutation-input' : 'input-object', 'config' => $inputConfiguration];
408
    }
409
410
    /**
411
     * Get a GraphQL scalar configuration from given scalar annotation.
412
     *
413
     * @param string     $className
414
     * @param GQL\Scalar $scalarAnnotation
415
     * @param array      $classAnnotations
416
     *
417
     * @return array
418
     */
419 19
    private static function scalarAnnotationToGQLConfiguration(string $className, GQL\Scalar $scalarAnnotation, array $classAnnotations): array
420
    {
421 19
        $scalarConfiguration = [];
422
423 19
        if ($scalarAnnotation->scalarType) {
424 19
            $scalarConfiguration['scalarType'] = self::formatExpression($scalarAnnotation->scalarType);
425
        } else {
426
            $scalarConfiguration = [
427 19
                'serialize' => [$className, 'serialize'],
428 19
                'parseValue' => [$className, 'parseValue'],
429 19
                'parseLiteral' => [$className, 'parseLiteral'],
430
            ];
431
        }
432
433 19
        $scalarConfiguration = self::getDescriptionConfiguration($classAnnotations) + $scalarConfiguration;
434
435 19
        return ['type' => 'custom-scalar', 'config' => $scalarConfiguration];
436
    }
437
438
    /**
439
     * Get a GraphQL Enum configuration from given enum annotation.
440
     *
441
     * @param GQL\Enum $enumAnnotation
442
     * @param array    $classAnnotations
443
     * @param array    $constants
444
     *
445
     * @return array
446
     */
447 19
    private static function enumAnnotationToGQLConfiguration(GQL\Enum $enumAnnotation, array $classAnnotations, array $constants): array
448
    {
449 19
        $enumValues = $enumAnnotation->values ? $enumAnnotation->values : [];
450
451 19
        $values = [];
452
453 19
        foreach ($constants as $name => $value) {
454
            $valueAnnotation = \current(\array_filter($enumValues, function ($enumValueAnnotation) use ($name) {
455 19
                return $enumValueAnnotation->name == $name;
456 19
            }));
457 19
            $valueConfig = [];
458 19
            $valueConfig['value'] = $value;
459
460 19
            if ($valueAnnotation && $valueAnnotation->description) {
461 19
                $valueConfig['description'] = $valueAnnotation->description;
462
            }
463
464 19
            if ($valueAnnotation && $valueAnnotation->deprecationReason) {
465 19
                $valueConfig['deprecationReason'] = $valueAnnotation->deprecationReason;
466
            }
467
468 19
            $values[$name] = $valueConfig;
469
        }
470
471 19
        $enumConfiguration = ['values' => $values];
472 19
        $enumConfiguration = self::getDescriptionConfiguration($classAnnotations) + $enumConfiguration;
473
474 19
        return ['type' => 'enum', 'config' => $enumConfiguration];
475
    }
476
477
    /**
478
     * Get a GraphQL Union configuration from given union annotation.
479
     *
480
     * @param string    $className
481
     * @param GQL\Union $unionAnnotation
482
     * @param array     $classAnnotations
483
     * @param array     $methods
484
     *
485
     * @return array
486
     */
487 19
    private static function unionAnnotationToGQLConfiguration(string $className, GQL\Union $unionAnnotation, array $classAnnotations, array $methods): array
488
    {
489 19
        $unionConfiguration = ['types' => $unionAnnotation->types];
490 19
        $unionConfiguration = self::getDescriptionConfiguration($classAnnotations) + $unionConfiguration;
491
492 19
        if ($unionAnnotation->resolveType) {
493 19
            $unionConfiguration['resolveType'] = self::formatExpression($unionAnnotation->resolveType);
494
        } else {
495 19
            if (isset($methods['resolveType'])) {
496 19
                $method = $methods['resolveType']['method'];
497 19
                if ($method->isStatic() && $method->isPublic()) {
498 19
                    $unionConfiguration['resolveType'] = self::formatExpression(\sprintf("@=call('%s::%s', [service('overblog_graphql.type_resolver'), value], true)", self::formatNamespaceForExpression($className), 'resolveType'));
499
                } else {
500 19
                    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.'));
501
                }
502
            } else {
503 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.'));
504
            }
505
        }
506
507 19
        return ['type' => 'union', 'config' => $unionConfiguration];
508
    }
509
510
    /**
511
     * Create GraphQL fields configuration based on annotations.
512
     *
513
     * @param string $namespace
514
     * @param array  $propertiesOrMethods
515
     * @param bool   $isInput
516
     * @param bool   $isMethod
517
     * @param string $currentValue
518
     *
519
     * @return array
520
     */
521 20
    private static function getGraphQLFieldsFromAnnotations(string $namespace, array $propertiesOrMethods, bool $isInput = false, bool $isMethod = false, string $currentValue = 'value', string $fieldAnnotationName = 'Field'): array
522
    {
523 20
        $fields = [];
524 20
        foreach ($propertiesOrMethods as $target => $config) {
525 19
            $annotations = $config['annotations'];
526 19
            $method = $isMethod ? $config['method'] : false;
527
528 19
            $fieldAnnotation = self::getFirstAnnotationMatching($annotations, \sprintf('Overblog\GraphQLBundle\Annotation\%s', $fieldAnnotationName));
529 19
            $accessAnnotation = self::getFirstAnnotationMatching($annotations, GQL\Access::class);
530 19
            $publicAnnotation = self::getFirstAnnotationMatching($annotations, GQL\IsPublic::class);
531
532 19
            if (!$fieldAnnotation) {
533 19
                if ($accessAnnotation || $publicAnnotation) {
534 1
                    throw new InvalidArgumentException(\sprintf('The annotations "@Access" and/or "@Visible" defined on "%s" are only usable in addition of annotation "@Field"', $target));
535
                }
536 19
                continue;
537
            }
538
539 19
            if ($isMethod && !$method->isPublic()) {
540 1
                throw new InvalidArgumentException(\sprintf('The Annotation "@Field" can only be applied to public method. The method "%s" is not public.', $target));
541
            }
542
543
            // Ignore field with resolver when the type is an Input
544 19
            if ($fieldAnnotation->resolve && $isInput) {
545
                continue;
546
            }
547
548 19
            $fieldName = $target;
549 19
            $fieldType = $fieldAnnotation->type;
550 19
            $fieldConfiguration = [];
551 19
            if ($fieldType) {
552 19
                $resolvedType = self::resolveClassFromType($fieldType);
553 19
                if (null !== $resolvedType && $isInput && !\in_array($resolvedType['type'], self::VALID_INPUT_TYPES)) {
554
                    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, $target, $resolvedType['type']));
555
                }
556
557 19
                $fieldConfiguration['type'] = $fieldType;
558
            }
559
560 19
            $fieldConfiguration = self::getDescriptionConfiguration($annotations, true) + $fieldConfiguration;
561
562 19
            if (!$isInput) {
563 19
                $args = self::getArgs($fieldAnnotation->args, $isMethod && !$fieldAnnotation->argsBuilder ? $method : null);
0 ignored issues
show
Bug introduced by
It seems like $isMethod && ! $fieldAnn...uilder ? $method : null can also be of type false; however, parameter $method of Overblog\GraphQLBundle\C...tationParser::getArgs() does only seem to accept ReflectionMethod|null, maybe add an additional type check? ( Ignorable by Annotation )

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

563
                $args = self::getArgs($fieldAnnotation->args, /** @scrutinizer ignore-type */ $isMethod && !$fieldAnnotation->argsBuilder ? $method : null);
Loading history...
564
565 19
                if (!empty($args)) {
566 19
                    $fieldConfiguration['args'] = $args;
567
                }
568
569 19
                $fieldName = $fieldAnnotation->name ?: $fieldName;
570
571 19
                if ($fieldAnnotation->resolve) {
572 19
                    $fieldConfiguration['resolve'] = self::formatExpression($fieldAnnotation->resolve);
573
                } else {
574 19
                    if ($isMethod) {
575 19
                        $fieldConfiguration['resolve'] = self::formatExpression(\sprintf('call(%s.%s, %s)', $currentValue, $target, self::formatArgsForExpression($args)));
576
                    } else {
577 19
                        if ($fieldName !== $target || 'value' !== $currentValue) {
578
                            $fieldConfiguration['resolve'] = self::formatExpression(\sprintf('%s.%s', $currentValue, $target));
579
                        }
580
                    }
581
                }
582
583 19
                if ($fieldAnnotation->argsBuilder) {
584 19
                    if (\is_string($fieldAnnotation->argsBuilder)) {
585
                        $fieldConfiguration['argsBuilder'] = $fieldAnnotation->argsBuilder;
586 19
                    } elseif (\is_array($fieldAnnotation->argsBuilder)) {
587 19
                        list($builder, $builderConfig) = $fieldAnnotation->argsBuilder;
588 19
                        $fieldConfiguration['argsBuilder'] = ['builder' => $builder, 'config' => $builderConfig];
589
                    } else {
590
                        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, $target));
591
                    }
592
                }
593
594 19
                if ($fieldAnnotation->fieldBuilder) {
595 19
                    if (\is_string($fieldAnnotation->fieldBuilder)) {
596
                        $fieldConfiguration['builder'] = $fieldAnnotation->fieldBuilder;
597 19
                    } elseif (\is_array($fieldAnnotation->fieldBuilder)) {
598 19
                        list($builder, $builderConfig) = $fieldAnnotation->fieldBuilder;
599 19
                        $fieldConfiguration['builder'] = $builder;
600 19
                        $fieldConfiguration['builderConfig'] = $builderConfig ?: [];
601
                    } else {
602 19
                        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, $target));
603
                    }
604
                } else {
605 19
                    if (!$fieldType) {
606 19
                        if ($isMethod) {
607 19
                            if ($method->hasReturnType()) {
608
                                try {
609 19
                                    $fieldConfiguration['type'] = self::resolveGraphQLTypeFromReflectionType($method->getReturnType(), self::VALID_OUTPUT_TYPES);
610
                                } catch (\Exception $e) {
611 19
                                    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, $target, (string) $method->getReturnType()));
612
                                }
613
                            } else {
614 19
                                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, $target));
615
                            }
616
                        } else {
617
                            try {
618 19
                                $fieldConfiguration['type'] = self::guessType($namespace, $annotations);
619 2
                            } catch (\Exception $e) {
620 2
                                throw new InvalidArgumentException(\sprintf('The attribute "type" on "@%s" defined on "%s" is required and cannot be auto-guessed : %s.', $fieldAnnotationName, $target, $e->getMessage()));
621
                            }
622
                        }
623
                    }
624
                }
625
626 19
                if ($accessAnnotation) {
627 19
                    $fieldConfiguration['access'] = self::formatExpression($accessAnnotation->value);
628
                }
629
630 19
                if ($publicAnnotation) {
631 19
                    $fieldConfiguration['public'] = self::formatExpression($publicAnnotation->value);
632
                }
633
634 19
                if ($fieldAnnotation->complexity) {
635 19
                    $fieldConfiguration['complexity'] = self::formatExpression($fieldAnnotation->complexity);
636
                }
637
            }
638
639 19
            $fields[$fieldName] = $fieldConfiguration;
640
        }
641
642 20
        return $fields;
643
    }
644
645
    /**
646
     * Return fields config from Provider methods.
647
     *
648
     * @param string $namespace
649
     * @param string $annotationName
650
     * @param string $targetType
651
     * @param bool   $isRoot
652
     *
653
     * @return array
654
     */
655 20
    private static function getGraphQLFieldsFromProviders(string $namespace, string $annotationName, string $targetType, bool $isRoot = false)
656
    {
657 20
        $fields = [];
658 20
        foreach (self::$providers as $className => $configuration) {
659 20
            $providerMethods = $configuration['methods'];
660 20
            $providerAnnotation = $configuration['annotation'];
661
662 20
            $filteredMethods = [];
663 20
            foreach ($providerMethods as $methodName => $config) {
664 20
                $annotations = $config['annotations'];
665
666 20
                $annotation = self::getFirstAnnotationMatching($annotations, \sprintf('Overblog\\GraphQLBundle\\Annotation\\%s', $annotationName));
667 20
                if (!$annotation) {
668 20
                    continue;
669
                }
670
671 20
                $annotationTarget = 'Query' === $annotationName ? $annotation->targetType : null;
672 20
                if (!$annotationTarget && $isRoot) {
673 19
                    $annotationTarget = $targetType;
674
                }
675
676 20
                if ($annotationTarget !== $targetType) {
677 20
                    continue;
678
                }
679
680 19
                $filteredMethods[$methodName] = $config;
681
            }
682
683 20
            $currentValue = \sprintf("service('%s')", self::formatNamespaceForExpression($className));
684 20
            $providerFields = self::getGraphQLFieldsFromAnnotations($namespace, $filteredMethods, false, true, $currentValue, $annotationName);
685 20
            foreach ($providerFields as $fieldName => $fieldConfig) {
686 19
                if ($providerAnnotation->prefix) {
687 19
                    $fieldName = \sprintf('%s%s', $providerAnnotation->prefix, $fieldName);
688
                }
689 19
                $fields[$fieldName] = $fieldConfig;
690
            }
691
        }
692
693 20
        return $fields;
694
    }
695
696
    /**
697
     * Get the config for description & deprecation reason.
698
     *
699
     * @param array $annotations
700
     * @param bool  $withDeprecation
701
     *
702
     * @return array
703
     */
704 20
    private static function getDescriptionConfiguration(array $annotations, bool $withDeprecation = false): array
705
    {
706 20
        $config = [];
707 20
        $descriptionAnnotation = self::getFirstAnnotationMatching($annotations, GQL\Description::class);
708 20
        if ($descriptionAnnotation) {
709 19
            $config['description'] = $descriptionAnnotation->value;
710
        }
711
712 20
        if ($withDeprecation) {
713 19
            $deprecatedAnnotation = self::getFirstAnnotationMatching($annotations, GQL\Deprecated::class);
714 19
            if ($deprecatedAnnotation) {
715 19
                $config['deprecationReason'] = $deprecatedAnnotation->value;
716
            }
717
        }
718
719 20
        return $config;
720
    }
721
722
    /**
723
     * Get args config from an array of @Arg annotation or by auto-guessing if a method is provided.
724
     *
725
     * @param array             $args
726
     * @param \ReflectionMethod $method
727
     *
728
     * @return array
729
     */
730 19
    private static function getArgs(array $args = null, \ReflectionMethod $method = null): array
731
    {
732 19
        $config = [];
733 19
        if ($args && !empty($args)) {
734 19
            foreach ($args as $arg) {
735 19
                $config[$arg->name] = ['type' => $arg->type] 
736 19
                    + ($arg->description ? ['description' => $arg->description] : [])
737 19
                    + ($arg->default ? ['defaultValue' => $arg->default] : []);
738
            }
739 19
        } elseif ($method) {
740 19
            $config = self::guessArgs($method);
741
        }
742
743 19
        return $config;
744
    }
745
746
    /**
747
     * Format an array of args to a list of arguments in an expression.
748
     *
749
     * @param array $args
750
     *
751
     * @return string
752
     */
753 19
    private static function formatArgsForExpression(array $args): string
754
    {
755 19
        $mapping = [];
756 19
        foreach ($args as $name => $config) {
757 19
            $mapping[] = \sprintf('%s: "%s"', $name, $config['type']);
758
        }
759
760 19
        return \sprintf('arguments({%s}, args)', \implode(', ', $mapping));
761
    }
762
763
    /**
764
     * Format a namespace to be used in an expression (double escape).
765
     *
766
     * @param string $namespace
767
     *
768
     * @return string
769
     */
770 20
    private static function formatNamespaceForExpression(string $namespace): string
771
    {
772 20
        return \str_replace('\\', '\\\\', $namespace);
773
    }
774
775
    /**
776
     * Get the first annotation matching given class.
777
     *
778
     * @param array        $annotations
779
     * @param string|array $annotationClass
780
     *
781
     * @return mixed
782
     */
783 20
    private static function getFirstAnnotationMatching(array $annotations, $annotationClass)
784
    {
785 20
        if (\is_string($annotationClass)) {
786 20
            $annotationClass = [$annotationClass];
787
        }
788
789 20
        foreach ($annotations as $annotation) {
790 20
            foreach ($annotationClass as $class) {
791 20
                if ($annotation instanceof $class) {
792 20
                    return $annotation;
793
                }
794
            }
795
        }
796
797 20
        return false;
798
    }
799
800
    /**
801
     * Format an expression (ie. add "@=" if not set).
802
     *
803
     * @param string $expression
804
     *
805
     * @return string
806
     */
807 19
    private static function formatExpression(string $expression)
808
    {
809 19
        return '@=' === \substr($expression, 0, 2) ? $expression : \sprintf('@=%s', $expression);
810
    }
811
812
    /**
813
     * Suffix a name if it is not already.
814
     *
815
     * @param string $name
816
     * @param string $suffix
817
     *
818
     * @return string
819
     */
820 19
    private static function suffixName(string $name, string $suffix)
821
    {
822 19
        return \substr($name, -\strlen($suffix)) === $suffix ? $name : \sprintf('%s%s', $name, $suffix);
823
    }
824
825
    /**
826
     * Try to guess a field type base on is annotations.
827
     *
828
     * @param string $namespace
829
     * @param array  $annotations
830
     *
831
     * @return string
832
     *
833
     * @throws \RuntimeException
834
     */
835 19
    private static function guessType(string $namespace, array $annotations): string
836
    {
837 19
        $columnAnnotation = self::getFirstAnnotationMatching($annotations, Column::class);
838 19
        if ($columnAnnotation) {
839 19
            $type = self::resolveTypeFromDoctrineType($columnAnnotation->type);
840 19
            $nullable = $columnAnnotation->nullable;
841 19
            if ($type) {
842 19
                return $nullable ? $type : \sprintf('%s!', $type);
843
            } else {
844 1
                throw new \RuntimeException(\sprintf('Unable to auto-guess GraphQL type from Doctrine type "%s"', $columnAnnotation->type));
845
            }
846
        }
847
848
        $associationAnnotations = [
849 19
            OneToMany::class => true,
850
            OneToOne::class => false,
851
            ManyToMany::class => true,
852
            ManyToOne::class => false,
853
        ];
854
855 19
        $associationAnnotation = self::getFirstAnnotationMatching($annotations, \array_keys($associationAnnotations));
856 19
        if ($associationAnnotation) {
857 19
            $target = self::fullyQualifiedClassName($associationAnnotation->targetEntity, $namespace);
858 19
            $type = self::resolveTypeFromClass($target, ['type']);
859
860 19
            if ($type) {
861 19
                $isMultiple = $associationAnnotations[\get_class($associationAnnotation)];
862 19
                if ($isMultiple) {
863 19
                    return \sprintf('[%s]!', $type);
864
                } else {
865 19
                    $isNullable = false;
866 19
                    $joinColumn = self::getFirstAnnotationMatching($annotations, JoinColumn::class);
867 19
                    if ($joinColumn) {
868 19
                        $isNullable = $joinColumn->nullable;
869
                    }
870
871 19
                    return \sprintf('%s%s', $type, $isNullable ? '' : '!');
872
                }
873
            } else {
874 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));
875
            }
876
        }
877
878
        throw new InvalidArgumentException(\sprintf('No Doctrine ORM annotation found.'));
879
    }
880
881
    /**
882
     * Resolve a FQN from classname and namespace.
883
     *
884
     * @param string $className
885
     * @param string $namespace
886
     *
887
     * @return string
888
     *
889
     * @internal
890
     */
891 19
    public static function fullyQualifiedClassName(string $className, string $namespace): string
892
    {
893 19
        if (false === \strpos($className, '\\') && $namespace) {
894 19
            return $namespace.'\\'.$className;
895
        }
896
897 1
        return $className;
898
    }
899
900
    /**
901
     * Resolve a GraphQLType from a doctrine type.
902
     *
903
     * @param string $doctrineType
904
     *
905
     * @return string|null
906
     */
907 19
    private static function resolveTypeFromDoctrineType(string $doctrineType): ?string
908
    {
909 19
        if (isset(self::$doctrineMapping[$doctrineType])) {
910 19
            return self::$doctrineMapping[$doctrineType];
911
        }
912
913 19
        switch ($doctrineType) {
914 19
            case 'integer':
915 19
            case 'smallint':
916 19
            case 'bigint':
917 19
                return 'Int';
918 19
            case 'string':
919 1
            case 'text':
920 19
                return 'String';
921 1
            case 'bool':
922 1
            case 'boolean':
923
                return 'Boolean';
924 1
            case 'float':
925 1
            case 'decimal':
926
                return 'Float';
927
            default:
928 1
                return null;
929
        }
930
    }
931
932
    /**
933
     * Transform a method arguments from reflection to a list of GraphQL argument.
934
     */
935 19
    private static function guessArgs(\ReflectionMethod $method): array
936
    {
937 19
        $arguments = [];
938 19
        foreach ($method->getParameters() as $index => $parameter) {
939 19
            if (!$parameter->hasType()) {
940 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()));
941
            }
942
943
            try {
944 19
                $gqlType = self::resolveGraphQLTypeFromReflectionType($parameter->getType(), self::VALID_INPUT_TYPES, $parameter->isDefaultValueAvailable());
945
            } catch (\Exception $e) {
946
                throw new InvalidArgumentException(\sprintf('Argument n°%s "$%s" on method "%s" cannot be auto-guessed : %s".', $index + 1, $parameter->getName(), $method->getName(), $e->getMessage()));
947
            }
948
949 19
            $argumentConfig = [];
950 19
            if ($parameter->isDefaultValueAvailable()) {
951 19
                $argumentConfig['defaultValue'] = $parameter->getDefaultValue();
952
            }
953
954 19
            $argumentConfig['type'] = $gqlType;
955
956 19
            $arguments[$parameter->getName()] = $argumentConfig;
957
        }
958
959 19
        return $arguments;
960
    }
961
962 19
    private static function resolveGraphQLTypeFromReflectionType(\ReflectionType $type, array $filterGraphQLTypes = null, bool $isOptional = false): string
963
    {
964 19
        $sType = (string) $type;
965 19
        if ($type->isBuiltin()) {
966 19
            $gqlType = self::resolveTypeFromPhpType($sType);
967 19
            if (null === $gqlType) {
968 19
                throw new \RuntimeException(\sprintf('No corresponding GraphQL type found for builtin type "%s"', $sType));
969
            }
970
        } else {
971 19
            $gqlType = self::resolveTypeFromClass($sType, $filterGraphQLTypes);
972 19
            if (null === $gqlType) {
973
                throw new \RuntimeException(\sprintf('No corresponding GraphQL %s found for class "%s"', $filterGraphQLTypes ? \implode(',', $filterGraphQLTypes) : 'object', $sType));
974
            }
975
        }
976
977 19
        return \sprintf('%s%s', $gqlType, ($type->allowsNull() || $isOptional) ? '' : '!');
978
    }
979
980
    /**
981
     * Resolve a GraphQL Type from a class name.
982
     *
983
     * @param string $className
984
     * @param array  $wantedTypes
985
     *
986
     * @return string|null
987
     */
988 19
    private static function resolveTypeFromClass(string $className, array $wantedTypes = null): ?string
989
    {
990 19
        foreach (self::$classesMap as $gqlType => $config) {
991 19
            if ($config['class'] === $className) {
992 19
                if (!$wantedTypes || \in_array($config['type'], $wantedTypes)) {
993 19
                    return $gqlType;
994
                }
995
            }
996
        }
997
998 1
        return null;
999
    }
1000
1001
    /**
1002
     * Resolve a PHP class from a GraphQL type.
1003
     *
1004
     * @param string $type
1005
     *
1006
     * @return string|array|null
1007
     */
1008 19
    private static function resolveClassFromType(string $type)
1009
    {
1010 19
        return self::$classesMap[$type] ?? null;
1011
    }
1012
1013
    /**
1014
     * Convert a PHP Builtin type to a GraphQL type.
1015
     *
1016
     * @param string $phpType
1017
     *
1018
     * @return string|null
1019
     */
1020 19
    private static function resolveTypeFromPhpType(string $phpType): ?string
1021
    {
1022 19
        switch ($phpType) {
1023 19
            case 'boolean':
1024 19
            case 'bool':
1025 19
                return 'Boolean';
1026 19
            case 'integer':
1027 19
            case 'int':
1028 19
                return 'Int';
1029 19
            case 'float':
1030 19
            case 'double':
1031 19
                return 'Float';
1032 19
            case 'string':
1033 19
                return 'String';
1034
            default:
1035
                return null;
1036
        }
1037
    }
1038
}
1039