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 (#507)
by Vincent
18:24
created

resolveGraphQLTypeFromReflectionType()   B

Complexity

Conditions 7
Paths 4

Size

Total Lines 16
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 7.049

Importance

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

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