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
Push — master ( 7e5601...2a112f )
by Jérémiah
16:11 queued 15s
created

AnnotationParser::getArgs()   A

Complexity

Conditions 6
Paths 3

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 6

Importance

Changes 0
Metric Value
eloc 7
dl 0
loc 12
ccs 8
cts 8
cp 1
rs 9.2222
c 0
b 0
f 0
cc 6
nc 3
nop 2
crap 6
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] + ($arg->description ? ['description' => $arg->description] : []);
736
            }
737 19
        } elseif ($method) {
738 19
            $config = self::guessArgs($method);
739
        }
740
741 19
        return $config;
742
    }
743
744
    /**
745
     * Format an array of args to a list of arguments in an expression.
746
     *
747
     * @param array $args
748
     *
749
     * @return string
750
     */
751 19
    private static function formatArgsForExpression(array $args): string
752
    {
753 19
        $mapping = [];
754 19
        foreach ($args as $name => $config) {
755 19
            $mapping[] = \sprintf('%s: "%s"', $name, $config['type']);
756
        }
757
758 19
        return \sprintf('arguments({%s}, args)', \implode(', ', $mapping));
759
    }
760
761
    /**
762
     * Format a namespace to be used in an expression (double escape).
763
     *
764
     * @param string $namespace
765
     *
766
     * @return string
767
     */
768 20
    private static function formatNamespaceForExpression(string $namespace): string
769
    {
770 20
        return \str_replace('\\', '\\\\', $namespace);
771
    }
772
773
    /**
774
     * Get the first annotation matching given class.
775
     *
776
     * @param array        $annotations
777
     * @param string|array $annotationClass
778
     *
779
     * @return mixed
780
     */
781 20
    private static function getFirstAnnotationMatching(array $annotations, $annotationClass)
782
    {
783 20
        if (\is_string($annotationClass)) {
784 20
            $annotationClass = [$annotationClass];
785
        }
786
787 20
        foreach ($annotations as $annotation) {
788 20
            foreach ($annotationClass as $class) {
789 20
                if ($annotation instanceof $class) {
790 20
                    return $annotation;
791
                }
792
            }
793
        }
794
795 20
        return false;
796
    }
797
798
    /**
799
     * Format an expression (ie. add "@=" if not set).
800
     *
801
     * @param string $expression
802
     *
803
     * @return string
804
     */
805 19
    private static function formatExpression(string $expression)
806
    {
807 19
        return '@=' === \substr($expression, 0, 2) ? $expression : \sprintf('@=%s', $expression);
808
    }
809
810
    /**
811
     * Suffix a name if it is not already.
812
     *
813
     * @param string $name
814
     * @param string $suffix
815
     *
816
     * @return string
817
     */
818 19
    private static function suffixName(string $name, string $suffix)
819
    {
820 19
        return \substr($name, -\strlen($suffix)) === $suffix ? $name : \sprintf('%s%s', $name, $suffix);
821
    }
822
823
    /**
824
     * Try to guess a field type base on is annotations.
825
     *
826
     * @param string $namespace
827
     * @param array  $annotations
828
     *
829
     * @return string
830
     *
831
     * @throws \RuntimeException
832
     */
833 19
    private static function guessType(string $namespace, array $annotations): string
834
    {
835 19
        $columnAnnotation = self::getFirstAnnotationMatching($annotations, Column::class);
836 19
        if ($columnAnnotation) {
837 19
            $type = self::resolveTypeFromDoctrineType($columnAnnotation->type);
838 19
            $nullable = $columnAnnotation->nullable;
839 19
            if ($type) {
840 19
                return $nullable ? $type : \sprintf('%s!', $type);
841
            } else {
842 1
                throw new \RuntimeException(\sprintf('Unable to auto-guess GraphQL type from Doctrine type "%s"', $columnAnnotation->type));
843
            }
844
        }
845
846
        $associationAnnotations = [
847 19
            OneToMany::class => true,
848
            OneToOne::class => false,
849
            ManyToMany::class => true,
850
            ManyToOne::class => false,
851
        ];
852
853 19
        $associationAnnotation = self::getFirstAnnotationMatching($annotations, \array_keys($associationAnnotations));
854 19
        if ($associationAnnotation) {
855 19
            $target = self::fullyQualifiedClassName($associationAnnotation->targetEntity, $namespace);
856 19
            $type = self::resolveTypeFromClass($target, ['type']);
857
858 19
            if ($type) {
859 19
                $isMultiple = $associationAnnotations[\get_class($associationAnnotation)];
860 19
                if ($isMultiple) {
861 19
                    return \sprintf('[%s]!', $type);
862
                } else {
863 19
                    $isNullable = false;
864 19
                    $joinColumn = self::getFirstAnnotationMatching($annotations, JoinColumn::class);
865 19
                    if ($joinColumn) {
866 19
                        $isNullable = $joinColumn->nullable;
867
                    }
868
869 19
                    return \sprintf('%s%s', $type, $isNullable ? '' : '!');
870
                }
871
            } else {
872 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));
873
            }
874
        }
875
876
        throw new InvalidArgumentException(\sprintf('No Doctrine ORM annotation found.'));
877
    }
878
879
    /**
880
     * Resolve a FQN from classname and namespace.
881
     *
882
     * @param string $className
883
     * @param string $namespace
884
     *
885
     * @return string
886
     *
887
     * @internal
888
     */
889 19
    public static function fullyQualifiedClassName(string $className, string $namespace): string
890
    {
891 19
        if (false === \strpos($className, '\\') && $namespace) {
892 19
            return $namespace.'\\'.$className;
893
        }
894
895 1
        return $className;
896
    }
897
898
    /**
899
     * Resolve a GraphQLType from a doctrine type.
900
     *
901
     * @param string $doctrineType
902
     *
903
     * @return string|null
904
     */
905 19
    private static function resolveTypeFromDoctrineType(string $doctrineType): ?string
906
    {
907 19
        if (isset(self::$doctrineMapping[$doctrineType])) {
908 19
            return self::$doctrineMapping[$doctrineType];
909
        }
910
911 19
        switch ($doctrineType) {
912 19
            case 'integer':
913 19
            case 'smallint':
914 19
            case 'bigint':
915 19
                return 'Int';
916 19
            case 'string':
917 1
            case 'text':
918 19
                return 'String';
919 1
            case 'bool':
920 1
            case 'boolean':
921
                return 'Boolean';
922 1
            case 'float':
923 1
            case 'decimal':
924
                return 'Float';
925
            default:
926 1
                return null;
927
        }
928
    }
929
930
    /**
931
     * Transform a method arguments from reflection to a list of GraphQL argument.
932
     */
933 19
    private static function guessArgs(\ReflectionMethod $method): array
934
    {
935 19
        $arguments = [];
936 19
        foreach ($method->getParameters() as $index => $parameter) {
937 19
            if (!$parameter->hasType()) {
938 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()));
939
            }
940
941
            try {
942 19
                $gqlType = self::resolveGraphQLTypeFromReflectionType($parameter->getType(), self::VALID_INPUT_TYPES, $parameter->isDefaultValueAvailable());
943
            } catch (\Exception $e) {
944
                throw new InvalidArgumentException(\sprintf('Argument n°%s "$%s" on method "%s" cannot be auto-guessed : %s".', $index + 1, $parameter->getName(), $method->getName(), $e->getMessage()));
945
            }
946
947 19
            $argumentConfig = [];
948 19
            if ($parameter->isDefaultValueAvailable()) {
949 19
                $argumentConfig['defaultValue'] = $parameter->getDefaultValue();
950
            }
951
952 19
            $argumentConfig['type'] = $gqlType;
953
954 19
            $arguments[$parameter->getName()] = $argumentConfig;
955
        }
956
957 19
        return $arguments;
958
    }
959
960 19
    private static function resolveGraphQLTypeFromReflectionType(\ReflectionType $type, array $filterGraphQLTypes = null, bool $isOptional = false): string
961
    {
962 19
        $sType = (string) $type;
963 19
        if ($type->isBuiltin()) {
964 19
            $gqlType = self::resolveTypeFromPhpType($sType);
965 19
            if (null === $gqlType) {
966 19
                throw new \RuntimeException(\sprintf('No corresponding GraphQL type found for builtin type "%s"', $sType));
967
            }
968
        } else {
969 19
            $gqlType = self::resolveTypeFromClass($sType, $filterGraphQLTypes);
970 19
            if (null === $gqlType) {
971
                throw new \RuntimeException(\sprintf('No corresponding GraphQL %s found for class "%s"', $filterGraphQLTypes ? \implode(',', $filterGraphQLTypes) : 'object', $sType));
972
            }
973
        }
974
975 19
        return \sprintf('%s%s', $gqlType, ($type->allowsNull() || $isOptional) ? '' : '!');
976
    }
977
978
    /**
979
     * Resolve a GraphQL Type from a class name.
980
     *
981
     * @param string $className
982
     * @param array  $wantedTypes
983
     *
984
     * @return string|null
985
     */
986 19
    private static function resolveTypeFromClass(string $className, array $wantedTypes = null): ?string
987
    {
988 19
        foreach (self::$classesMap as $gqlType => $config) {
989 19
            if ($config['class'] === $className) {
990 19
                if (!$wantedTypes || \in_array($config['type'], $wantedTypes)) {
991 19
                    return $gqlType;
992
                }
993
            }
994
        }
995
996 1
        return null;
997
    }
998
999
    /**
1000
     * Resolve a PHP class from a GraphQL type.
1001
     *
1002
     * @param string $type
1003
     *
1004
     * @return string|array|null
1005
     */
1006 19
    private static function resolveClassFromType(string $type)
1007
    {
1008 19
        return self::$classesMap[$type] ?? null;
1009
    }
1010
1011
    /**
1012
     * Convert a PHP Builtin type to a GraphQL type.
1013
     *
1014
     * @param string $phpType
1015
     *
1016
     * @return string|null
1017
     */
1018 19
    private static function resolveTypeFromPhpType(string $phpType): ?string
1019
    {
1020 19
        switch ($phpType) {
1021 19
            case 'boolean':
1022 19
            case 'bool':
1023 19
                return 'Boolean';
1024 19
            case 'integer':
1025 19
            case 'int':
1026 19
                return 'Int';
1027 19
            case 'float':
1028 19
            case 'double':
1029 19
                return 'Float';
1030 19
            case 'string':
1031 19
                return 'String';
1032
            default:
1033
                return null;
1034
        }
1035
    }
1036
}
1037