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 (#640)
by Moritz
27:34 queued 24:09
created

AnnotationParser::getAnnotationReader()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 4.0312

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 7
c 1
b 0
f 0
dl 0
loc 13
rs 10
ccs 7
cts 8
cp 0.875
cc 4
nc 3
nop 0
crap 4.0312
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
        $container->setParameter('overblog_graphql_types.classes_map', 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
     * @internal
67
     */
68 58
    public static function reset(): void
69
    {
70 58
        self::$classesMap = [];
71 58
        self::$providers = [];
72 58
        self::$classAnnotationsCache = [];
73 58
        self::$annotationReader = null;
74 58
    }
75
76
    /**
77
     * Process a file.
78
     *
79
     * @param \SplFileInfo     $file
80
     * @param ContainerBuilder $container
81
     * @param array            $configs
82
     * @param bool             $preProcess
83
     *
84
     * @return array
85
     *
86
     * @throws \ReflectionException
87
     * @throws InvalidArgumentException
88
     */
89 20
    private static function processFile(\SplFileInfo $file, ContainerBuilder $container, array $configs, bool $preProcess): array
90
    {
91 20
        self::$doctrineMapping = $configs['doctrine']['types_mapping'];
92 20
        $container->addResource(new FileResource($file->getRealPath()));
93
94
        try {
95 20
            $className = $file->getBasename('.php');
96 20
            if (\preg_match('#namespace (.+);#', \file_get_contents($file->getRealPath()), $matches)) {
97 20
                $className = \trim($matches[1]).'\\'.$className;
98
            }
99 20
            [$reflectionEntity, $classAnnotations, $properties, $methods] = self::extractClassAnnotations($className);
100 20
            $gqlTypes = [];
101
102 20
            foreach ($classAnnotations as $classAnnotation) {
103 20
                $gqlTypes = self::classAnnotationsToGQLConfiguration(
104 20
                    $reflectionEntity,
105 20
                    $classAnnotation,
106 20
                    $configs,
107 20
                    $classAnnotations,
108 20
                    $properties,
109 20
                    $methods,
110 20
                    $gqlTypes,
111 20
                    $preProcess
112
                );
113
            }
114
115 20
            return $preProcess ? self::$classesMap : $gqlTypes;
116 8
        } catch (\InvalidArgumentException $e) {
117 8
            throw new InvalidArgumentException(\sprintf('Failed to parse GraphQL annotations from file "%s".', $file), $e->getCode(), $e);
118
        }
119
    }
120
121
    /**
122
     * @param \ReflectionClass $reflectionEntity
123
     * @param array            $configs
124
     * @param object           $classAnnotation
125
     * @param array            $classAnnotations
126
     * @param array            $properties
127
     * @param array            $methods
128
     * @param array            $gqlTypes
129
     * @param bool             $preProcess
130
     *
131
     * @return array
132
     */
133 20
    private static function classAnnotationsToGQLConfiguration(
134
        \ReflectionClass $reflectionEntity,
135
        $classAnnotation,
136
        array $configs,
137
        array $classAnnotations,
138
        array $properties,
139
        array $methods,
140
        array $gqlTypes,
141
        bool $preProcess
142
    ): array {
143 20
        $gqlConfiguration = $gqlType = $gqlName = null;
144
145
        switch (true) {
146 20
            case $classAnnotation instanceof GQL\Type:
147 20
                $gqlType = self::GQL_TYPE;
148 20
                $gqlName = $classAnnotation->name ?: $reflectionEntity->getShortName();
149 20
                if (!$preProcess) {
150 20
                    $gqlConfiguration = self::typeAnnotationToGQLConfiguration(
151 20
                        $reflectionEntity, $classAnnotation, $gqlName, $classAnnotations, $properties, $methods, $configs
152
                    );
153
154 20
                    if ($classAnnotation instanceof GQL\Relay\Connection) {
155 19
                        if (!$reflectionEntity->implementsInterface(ConnectionInterface::class)) {
156
                            throw new InvalidArgumentException(\sprintf('The annotation @Connection on class "%s" can only be used on class implementing the ConnectionInterface.', $reflectionEntity->getName()));
157
                        }
158
159 19
                        if (!($classAnnotation->edge xor $classAnnotation->node)) {
160
                            throw new InvalidArgumentException(\sprintf('The annotation @Connection on class "%s" is invalid. You must define the "edge" OR the "node" attribute.', $reflectionEntity->getName()));
161
                        }
162
163 19
                        $edgeType = $classAnnotation->edge;
164 19
                        if (!$edgeType) {
165 19
                            $edgeType = \sprintf('%sEdge', $gqlName);
166 19
                            $gqlTypes[$edgeType] = [
167 19
                                'type' => 'object',
168
                                'config' => [
169
                                    'builders' => [
170 19
                                        ['builder' => 'relay-edge', 'builderConfig' => ['nodeType' => $classAnnotation->node]],
171
                                    ],
172
                                ],
173
                            ];
174
                        }
175 19
                        if (!isset($gqlConfiguration['config']['builders'])) {
176 19
                            $gqlConfiguration['config']['builders'] = [];
177
                        }
178 19
                        \array_unshift($gqlConfiguration['config']['builders'], ['builder' => 'relay-connection', 'builderConfig' => ['edgeType' => $edgeType]]);
179
                    }
180
                }
181 20
                break;
182
183 19
            case $classAnnotation instanceof GQL\Input:
184 19
                $gqlType = self::GQL_INPUT;
185 19
                $gqlName = $classAnnotation->name ?: self::suffixName($reflectionEntity->getShortName(), 'Input');
186 19
                if (!$preProcess) {
187 19
                    $gqlConfiguration = self::inputAnnotationToGQLConfiguration(
188 19
                        $classAnnotation, $classAnnotations, $properties, $reflectionEntity->getNamespaceName()
189
                    );
190
                }
191 19
                break;
192
193 19
            case $classAnnotation instanceof GQL\Scalar:
194 19
                $gqlType = self::GQL_SCALAR;
195 19
                if (!$preProcess) {
196 19
                    $gqlConfiguration = self::scalarAnnotationToGQLConfiguration(
197 19
                        $reflectionEntity->getName(), $classAnnotation, $classAnnotations
198
                    );
199
                }
200 19
                break;
201
202 19
            case $classAnnotation instanceof GQL\Enum:
203 19
                $gqlType = self::GQL_ENUM;
204 19
                if (!$preProcess) {
205 19
                    $gqlConfiguration = self::enumAnnotationToGQLConfiguration(
206 19
                        $classAnnotation, $classAnnotations, $reflectionEntity->getConstants()
207
                    );
208
                }
209 19
                break;
210
211 19
            case $classAnnotation instanceof GQL\Union:
212 19
                $gqlType = self::GQL_UNION;
213 19
                if (!$preProcess) {
214 19
                    $gqlConfiguration = self::unionAnnotationToGQLConfiguration(
215 19
                        $reflectionEntity->getName(), $classAnnotation, $classAnnotations, $methods
216
                    );
217
                }
218 19
                break;
219
220 19
            case $classAnnotation instanceof GQL\TypeInterface:
221 19
                $gqlType = self::GQL_INTERFACE;
222 19
                if (!$preProcess) {
223 19
                    $gqlConfiguration = self::typeInterfaceAnnotationToGQLConfiguration(
224 19
                        $classAnnotation, $classAnnotations, $properties, $methods, $reflectionEntity->getNamespaceName()
225
                    );
226
                }
227 19
                break;
228
229 19
            case $classAnnotation instanceof GQL\Provider:
230 19
                if ($preProcess) {
231 19
                    self::$providers[$reflectionEntity->getName()] = ['annotation' => $classAnnotation, 'methods' => $methods, 'annotations' => $classAnnotations];
232
                }
233 19
                break;
234
        }
235
236 20
        if (null !== $gqlType) {
237 20
            if (!$gqlName) {
238 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...
239
            }
240
241 20
            if ($preProcess) {
242 20
                if (isset(self::$classesMap[$gqlName])) {
243 1
                    throw new InvalidArgumentException(\sprintf('The GraphQL type "%s" has already been registered in class "%s"', $gqlName, self::$classesMap[$gqlName]['class']));
244
                }
245 20
                self::$classesMap[$gqlName] = ['type' => $gqlType, 'class' => $reflectionEntity->getName()];
246
            } else {
247 20
                $gqlTypes = [$gqlName => $gqlConfiguration] + $gqlTypes;
248
            }
249
        }
250
251 20
        return $gqlTypes;
252
    }
253
254 20
    private static function extractClassAnnotations(string $className): array
255
    {
256 20
        if (!isset(self::$classAnnotationsCache[$className])) {
257 20
            $annotationReader = self::getAnnotationReader();
258 20
            $reflectionEntity = new \ReflectionClass($className);
259 20
            $classAnnotations = $annotationReader->getClassAnnotations($reflectionEntity);
260
261 20
            $properties = [];
262 20
            foreach ($reflectionEntity->getProperties() as $property) {
263 19
                $properties[$property->getName()] = ['property' => $property, 'annotations' => $annotationReader->getPropertyAnnotations($property)];
264
            }
265
266 20
            $methods = [];
267 20
            foreach ($reflectionEntity->getMethods() as $method) {
268 19
                $methods[$method->getName()] = ['method' => $method, 'annotations' => $annotationReader->getMethodAnnotations($method)];
269
            }
270
271 20
            self::$classAnnotationsCache[$className] = [$reflectionEntity, $classAnnotations, $properties, $methods];
272
        }
273
274 20
        return self::$classAnnotationsCache[$className];
275
    }
276
277 20
    private static function typeAnnotationToGQLConfiguration(
278
        \ReflectionClass $reflectionEntity,
279
        GQL\Type $classAnnotation,
280
        string $gqlName,
281
        array $classAnnotations,
282
        array $properties,
283
        array $methods,
284
        array $configs
285
    ): array {
286 20
        $rootQueryType = $configs['definitions']['schema']['default']['query'] ?? null;
287 20
        $rootMutationType = $configs['definitions']['schema']['default']['mutation'] ?? null;
288 20
        $isRootQuery = ($rootQueryType && $gqlName === $rootQueryType);
289 20
        $isRootMutation = ($rootMutationType && $gqlName === $rootMutationType);
290 20
        $currentValue = ($isRootQuery || $isRootMutation) ? \sprintf("service('%s')", self::formatNamespaceForExpression($reflectionEntity->getName())) : 'value';
291
292 20
        $gqlConfiguration = self::graphQLTypeConfigFromAnnotation($classAnnotation, $classAnnotations, $properties, $methods, $reflectionEntity->getNamespaceName(), $currentValue);
293 20
        $providerFields = self::getGraphQLFieldsFromProviders($reflectionEntity->getNamespaceName(), $isRootMutation ? 'Mutation' : 'Query', $gqlName, ($isRootQuery || $isRootMutation));
294 20
        $gqlConfiguration['config']['fields'] = $providerFields + $gqlConfiguration['config']['fields'];
295
296 20
        if ($classAnnotation instanceof GQL\Relay\Edge) {
297 19
            if (!$reflectionEntity->implementsInterface(EdgeInterface::class)) {
298
                throw new InvalidArgumentException(\sprintf('The annotation @Edge on class "%s" can only be used on class implementing the EdgeInterface.', $reflectionEntity->getName()));
299
            }
300 19
            if (!isset($gqlConfiguration['config']['builders'])) {
301 19
                $gqlConfiguration['config']['builders'] = [];
302
            }
303 19
            \array_unshift($gqlConfiguration['config']['builders'], ['builder' => 'relay-edge', 'builderConfig' => ['nodeType' => $classAnnotation->node]]);
304
        }
305
306 20
        return $gqlConfiguration;
307
    }
308
309 20
    private static function getAnnotationReader()
310
    {
311 20
        if (null === self::$annotationReader) {
312 20
            if (!\class_exists('\\Doctrine\\Common\\Annotations\\AnnotationReader') ||
313 20
                !\class_exists('\\Doctrine\\Common\\Annotations\\AnnotationRegistry')) {
314
                throw new \RuntimeException('In order to use graphql annotation, you need to require doctrine annotations');
315
            }
316
317 20
            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

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

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