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

Passed
Pull Request — master (#685)
by
unknown
12:00
created

AnnotationParser::resolveTypeFromDoctrineType()   B

Complexity

Conditions 11
Paths 11

Size

Total Lines 22
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 11.1967

Importance

Changes 0
Metric Value
eloc 18
dl 0
loc 22
ccs 15
cts 17
cp 0.8824
rs 7.3166
c 0
b 0
f 0
cc 11
nc 11
nop 1
crap 11.1967

How to fix   Complexity   

Long Method

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

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

Commonly applied refactorings include:

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

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

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

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