Scrutinizer GitHub App not installed

We could not synchronize checks via GitHub's checks API since Scrutinizer's GitHub App is not installed for this repository.

Install GitHub App

Completed
Push — annotations ( 42a852...be29b8 )
by Jérémiah
15s queued 11s
created

getGraphqlFieldsFromAnnotations()   F

Complexity

Conditions 39
Paths 12801

Size

Total Lines 126
Code Lines 80

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 65
CRAP Score 40.3967

Importance

Changes 0
Metric Value
eloc 80
dl 0
loc 126
ccs 65
cts 72
cp 0.9028
rs 0
c 0
b 0
f 0
cc 39
nc 12801
nop 6
crap 40.3967

How to fix   Long Method    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 Overblog\GraphQLBundle\Annotation as GQL;
10
use Overblog\GraphQLBundle\Relay\Connection\ConnectionInterface;
11
use Overblog\GraphQLBundle\Relay\Connection\EdgeInterface;
12
use Symfony\Component\Config\Resource\FileResource;
13
use Symfony\Component\DependencyInjection\ContainerBuilder;
14
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
15
16
class AnnotationParser implements PreParserInterface
17
{
18
    public const CLASSESMAP_CONTAINER_PARAMETER = 'overblog_graphql_types.classes_map';
19
20
    private static $annotationReader = null;
21
    private static $classesMap = [];
22
    private static $providers = [];
23
    private static $doctrineMapping = [];
24
25
    public const GQL_SCALAR = 'scalar';
26
    public const GQL_ENUM = 'enum';
27
    public const GQL_TYPE = 'type';
28
    public const GQL_INPUT = 'input';
29
    public const GQL_UNION = 'union';
30
    public const GQL_INTERFACE = 'interface';
31
32
    /**
33
     * @see https://facebook.github.io/graphql/draft/#sec-Input-and-Output-Types
34
     */
35
    protected static $validInputTypes = [self::GQL_SCALAR, self::GQL_ENUM, self::GQL_INPUT];
36
    protected static $validOutputTypes = [self::GQL_SCALAR, self::GQL_TYPE, self::GQL_INTERFACE, self::GQL_UNION, self::GQL_ENUM];
37
38
    /**
39
     * {@inheritdoc}
40
     *
41
     * @throws \ReflectionException
42
     * @throws InvalidArgumentException
43
     */
44 20
    public static function preParse(\SplFileInfo $file, ContainerBuilder $container, array $configs = []): void
45
    {
46 20
        self::proccessFile($file, $container, $configs, true);
47 20
    }
48
49
    /**
50
     * {@inheritdoc}
51
     *
52
     * @throws \ReflectionException
53
     * @throws InvalidArgumentException
54
     */
55 20
    public static function parse(\SplFileInfo $file, ContainerBuilder $container, array $configs = []): array
56
    {
57 20
        return self::proccessFile($file, $container, $configs);
58
    }
59
60
    /**
61
     * Clear the Annotation parser.
62
     */
63 19
    public static function clear(): void
64
    {
65 19
        self::$classesMap = [];
66 19
        self::$providers = [];
67 19
        self::$annotationReader = null;
68 19
    }
69
70
    /**
71
     * Process a file.
72
     *
73
     * @param \SplFileInfo     $file
74
     * @param ContainerBuilder $container
75
     * @param bool             $resolveClassMap
76
     *
77
     * @throws \ReflectionException
78
     * @throws InvalidArgumentException
79
     */
80 20
    public static function proccessFile(\SplFileInfo $file, ContainerBuilder $container, array $configs, bool $resolveClassMap = false): array
81
    {
82 20
        self::$doctrineMapping = $configs['doctrine']['types_mapping'];
83
84 20
        $rootQueryType = $configs['definitions']['schema']['default']['query'] ?? false;
85 20
        $rootMutationType = $configs['definitions']['schema']['default']['mutation'] ?? false;
86
87 20
        $container->addResource(new FileResource($file->getRealPath()));
88
89 20
        if (!$resolveClassMap) {
90 20
            $container->setParameter(self::CLASSESMAP_CONTAINER_PARAMETER, self::$classesMap);
91
        }
92
93
        try {
94 20
            $fileContent = \file_get_contents($file->getRealPath());
95
96 20
            $shortClassName = \substr($file->getFilename(), 0, -4);
97 20
            if (\preg_match('#namespace (.+);#', $fileContent, $namespace)) {
98 20
                $className = $namespace[1].'\\'.$shortClassName;
99 20
                $namespace = $namespace[1];
100
            } else {
101
                $className = $shortClassName;
102
            }
103
104 20
            $reflexionEntity = new \ReflectionClass($className);
105
106 20
            $classAnnotations = self::getAnnotationReader()->getClassAnnotations($reflexionEntity);
107
108 20
            $properties = [];
109 20
            foreach ($reflexionEntity->getProperties() as $property) {
110 19
                $properties[$property->getName()] = ['property' => $property, 'annotations' => self::getAnnotationReader()->getPropertyAnnotations($property)];
111
            }
112
113 20
            $methods = [];
114 20
            foreach ($reflexionEntity->getMethods() as $method) {
115 19
                $methods[$method->getName()] = ['method' => $method, 'annotations' => self::getAnnotationReader()->getMethodAnnotations($method)];
116
            }
117
118 20
            $gqlTypes = [];
119
120 20
            foreach ($classAnnotations as $classAnnotation) {
121 20
                $gqlConfiguration = $gqlType = $gqlName = false;
122
123
                switch (true) {
124 20
                    case $classAnnotation instanceof GQL\Type:
125 20
                        $gqlType = self::GQL_TYPE;
126 20
                        $gqlName = $classAnnotation->name ?: $shortClassName;
127
128 20
                        if (!$resolveClassMap) {
129 20
                            $isRootQuery = ($rootQueryType && $gqlName === $rootQueryType);
130 20
                            $isRootMutation = ($rootMutationType && $gqlName === $rootMutationType);
131 20
                            $currentValue = ($isRootQuery || $isRootMutation) ? \sprintf("service('%s')", self::formatNamespaceForExpression($className)) : 'value';
132
133 20
                            $gqlConfiguration = self::getGraphqlType($classAnnotation, $classAnnotations, $properties, $methods, $namespace, $currentValue);
134 20
                            $providerFields = self::getGraphqlFieldsFromProviders($namespace, $className, $isRootMutation ? 'Mutation' : 'Query', $gqlName, ($isRootQuery || $isRootMutation));
135 20
                            $gqlConfiguration['config']['fields'] = $providerFields + $gqlConfiguration['config']['fields'];
136
137 20
                            if ($classAnnotation instanceof GQL\Relay\Edge) {
138 19
                                if (!$reflexionEntity->implementsInterface(EdgeInterface::class)) {
139
                                    throw new InvalidArgumentException(\sprintf('The annotation @Edge on class "%s" can only be used on class implementing the EdgeInterface.', $className));
140
                                }
141 19
                                if (!isset($gqlConfiguration['config']['builders'])) {
142 19
                                    $gqlConfiguration['config']['builders'] = [];
143
                                }
144 19
                                \array_unshift($gqlConfiguration['config']['builders'], ['builder' => 'relay-edge', 'builderConfig' => ['nodeType' => $classAnnotation->node]]);
145
                            }
146
147 20
                            if ($classAnnotation instanceof GQL\Relay\Connection) {
148 19
                                if (!$reflexionEntity->implementsInterface(ConnectionInterface::class)) {
149
                                    throw new InvalidArgumentException(\sprintf('The annotation @Connection on class "%s" can only be used on class implementing the ConnectionInterface.', $className));
150
                                }
151
152 19
                                if (!($classAnnotation->edge xor $classAnnotation->node)) {
153
                                    throw new InvalidArgumentException(\sprintf('The annotation @Connection on class "%s" is invalid. You must define the "edge" OR the "node" attribute.', $className));
154
                                }
155
156 19
                                $edgeType = $classAnnotation->edge;
157 19
                                if (!$edgeType) {
158 19
                                    $edgeType = \sprintf('%sEdge', $gqlName);
159 19
                                    $gqlTypes[$edgeType] = [
160 19
                                        'type' => 'object',
161
                                        'config' => [
162
                                            'builders' => [
163 19
                                                ['builder' => 'relay-edge', 'builderConfig' => ['nodeType' => $classAnnotation->node]],
164
                                            ],
165
                                        ],
166
                                    ];
167
                                }
168 19
                                if (!isset($gqlConfiguration['config']['builders'])) {
169 19
                                    $gqlConfiguration['config']['builders'] = [];
170
                                }
171 19
                                \array_unshift($gqlConfiguration['config']['builders'], ['builder' => 'relay-connection', 'builderConfig' => ['edgeType' => $edgeType]]);
172
                            }
173
                        }
174 20
                        break;
175 19
                    case $classAnnotation instanceof GQL\Input:
176 19
                        $gqlType = self::GQL_INPUT;
177 19
                        $gqlName = $classAnnotation->name ?: self::suffixName($shortClassName, 'Input');
178 19
                        if (!$resolveClassMap) {
179 19
                            $gqlConfiguration = self::getGraphqlInput($classAnnotation, $classAnnotations, $properties, $namespace);
180
                        }
181 19
                        break;
182 19
                    case $classAnnotation instanceof GQL\Scalar:
183 19
                        $gqlType = self::GQL_SCALAR;
184 19
                        if (!$resolveClassMap) {
185 19
                            $gqlConfiguration = self::getGraphqlScalar($className, $classAnnotation, $classAnnotations);
186
                        }
187 19
                        break;
188 19
                    case $classAnnotation instanceof GQL\Enum:
189 19
                        $gqlType = self::GQL_ENUM;
190 19
                        if (!$resolveClassMap) {
191 19
                            $gqlConfiguration = self::getGraphqlEnum($classAnnotation, $classAnnotations, $reflexionEntity->getConstants());
192
                        }
193 19
                        break;
194 19
                    case $classAnnotation instanceof GQL\Union:
195 19
                        $gqlType = self::GQL_UNION;
196 19
                        if (!$resolveClassMap) {
197 19
                            $gqlConfiguration = self::getGraphqlUnion($className, $classAnnotation, $classAnnotations, $methods);
198
                        }
199 19
                        break;
200 19
                    case $classAnnotation instanceof GQL\TypeInterface:
201 19
                        $gqlType = self::GQL_INTERFACE;
202 19
                        if (!$resolveClassMap) {
203 19
                            $gqlConfiguration = self::getGraphqlInterface($classAnnotation, $classAnnotations, $properties, $methods, $namespace);
204
                        }
205 19
                        break;
206 19
                    case $classAnnotation instanceof GQL\Provider:
207 19
                        if ($resolveClassMap) {
208 19
                            self::$providers[$className] = ['annotation' => $classAnnotation, 'methods' => $methods];
209
                        }
210 19
                        break;
211
                    default:
212 19
                        continue 2;
213
                }
214
215 20
                if ($gqlType) {
216 20
                    if (!$gqlName) {
217 19
                        $gqlName = $classAnnotation->name ?: $shortClassName;
218
                    }
219
220 20
                    if ($resolveClassMap) {
221 20
                        if (isset(self::$classesMap[$gqlName])) {
222 1
                            throw new InvalidArgumentException(\sprintf('The GraphQL type "%s" has already been registered in class "%s"', $gqlName, self::$classesMap[$gqlName]['class']));
223
                        }
224 20
                        self::$classesMap[$gqlName] = ['type' => $gqlType, 'class' => $className];
225
                    } else {
226 20
                        $gqlTypes = [$gqlName => $gqlConfiguration] + $gqlTypes;
227
                    }
228
                }
229
            }
230
231 20
            return $resolveClassMap ? self::$classesMap : $gqlTypes;
232 8
        } catch (\InvalidArgumentException $e) {
233 8
            throw new InvalidArgumentException(\sprintf('Failed to parse GraphQL annotations from file "%s".', $file), $e->getCode(), $e);
234
        }
235
    }
236
237
    /**
238
     * Retrieve annotation reader.
239
     *
240
     * @return AnnotationReader
241
     */
242 20
    private static function getAnnotationReader()
243
    {
244 20
        if (null === self::$annotationReader) {
245 19
            if (!\class_exists('\\Doctrine\\Common\\Annotations\\AnnotationReader') ||
246 19
                !\class_exists('\\Doctrine\\Common\\Annotations\\AnnotationRegistry')) {
247
                throw new \Exception('In order to use graphql annotation, you need to require doctrine annotations');
248
            }
249
250 19
            AnnotationRegistry::registerLoader('class_exists');
0 ignored issues
show
Deprecated Code introduced by
The function Doctrine\Common\Annotati...istry::registerLoader() has been deprecated: this method is deprecated and will be removed in doctrine/annotations 2.0 autoloading should be deferred to the globally registered autoloader by then. For now, use @example AnnotationRegistry::registerLoader('class_exists') ( Ignorable by Annotation )

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

250
            /** @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...
251 19
            self::$annotationReader = new AnnotationReader();
252
        }
253
254 20
        return self::$annotationReader;
255
    }
256
257
    /**
258
     * Create a GraphQL Type configuration from annotations on class, properties and methods.
259
     *
260
     * @param GQL\Type $typeAnnotation
261
     * @param array    $classAnnotations
262
     * @param array    $properties
263
     * @param array    $methods
264
     * @param string   $namespace
265
     * @param string   $currentValue
266
     *
267
     * @return array
268
     */
269 20
    private static function getGraphqlType(GQL\Type $typeAnnotation, array $classAnnotations, array $properties, array $methods, string $namespace, string $currentValue)
270
    {
271 20
        $typeConfiguration = [];
272
273 20
        $fields = self::getGraphqlFieldsFromAnnotations($namespace, $properties, false, false, $currentValue);
274 20
        $fields = self::getGraphqlFieldsFromAnnotations($namespace, $methods, false, true, $currentValue) + $fields;
275
276 20
        $typeConfiguration['fields'] = $fields;
277 20
        $typeConfiguration = self::getDescriptionConfiguration($classAnnotations) + $typeConfiguration;
278
279 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...
280 19
            $typeConfiguration['interfaces'] = $typeAnnotation->interfaces;
281
        }
282
283 20
        if ($typeAnnotation->resolveField) {
284 19
            $typeConfiguration['resolveField'] = self::formatExpression($typeAnnotation->resolveField);
285
        }
286
287 20
        if ($typeAnnotation->builders && \count($typeAnnotation->builders) > 0) {
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...
288
            $typeConfiguration['builders'] = \array_map(function ($fieldsBuilderAnnotation) {
289 19
                return ['builder' => $fieldsBuilderAnnotation->builder, 'builderConfig' => $fieldsBuilderAnnotation->builderConfig];
290 19
            }, $typeAnnotation->builders);
291
        }
292
293 20
        $publicAnnotation = self::getFirstAnnotationMatching($classAnnotations, 'Overblog\GraphQLBundle\Annotation\IsPublic');
294 20
        if ($publicAnnotation) {
295 19
            $typeConfiguration['fieldsDefaultPublic'] = self::formatExpression($publicAnnotation->value);
296
        }
297
298 20
        $accessAnnotation = self::getFirstAnnotationMatching($classAnnotations, 'Overblog\GraphQLBundle\Annotation\Access');
299 20
        if ($accessAnnotation) {
300 19
            $typeConfiguration['fieldsDefaultAccess'] = self::formatExpression($accessAnnotation->value);
301
        }
302
303 20
        return ['type' => $typeAnnotation->isRelay ? 'relay-mutation-payload' : 'object', 'config' => $typeConfiguration];
304
    }
305
306
    /**
307
     * Create a GraphQL Interface type configuration from annotations on properties.
308
     *
309
     * @param string        $shortClassName
310
     * @param GQL\Interface $interfaceAnnotation
0 ignored issues
show
Bug introduced by
The type Overblog\GraphQLBundle\Annotation\Interface was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

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

512
                $args = self::getArgs($fieldAnnotation->args, /** @scrutinizer ignore-type */ $isMethod && !$fieldAnnotation->argsBuilder ? $method : null);
Loading history...
513
514 19
                if (!empty($args)) {
515 19
                    $fieldConfiguration['args'] = $args;
516
                }
517
518 19
                $fieldName = $fieldAnnotation->name ?: $fieldName;
519
520 19
                if ($fieldAnnotation->resolve) {
521 19
                    $fieldConfiguration['resolve'] = self::formatExpression($fieldAnnotation->resolve);
522
                } else {
523 19
                    if ($isMethod) {
524 19
                        $fieldConfiguration['resolve'] = self::formatExpression(\sprintf('call(%s.%s, %s)', $currentValue, $target, self::formatArgsForExpression($args)));
525
                    } else {
526 19
                        if ($fieldName !== $target || 'value' !== $currentValue) {
527
                            $fieldConfiguration['resolve'] = self::formatExpression(\sprintf('%s.%s', $currentValue, $target));
528
                        }
529
                    }
530
                }
531
532 19
                if ($fieldAnnotation->argsBuilder) {
533 19
                    if (\is_string($fieldAnnotation->argsBuilder)) {
534
                        $fieldConfiguration['argsBuilder'] = $fieldAnnotation->argsBuilder;
535 19
                    } elseif (\is_array($fieldAnnotation->argsBuilder)) {
536 19
                        list($builder, $builderConfig) = $fieldAnnotation->argsBuilder;
537 19
                        $fieldConfiguration['argsBuilder'] = ['builder' => $builder, 'config' => $builderConfig];
538
                    } else {
539
                        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));
540
                    }
541
                }
542
543 19
                if ($fieldAnnotation->fieldBuilder) {
544 19
                    if (\is_string($fieldAnnotation->fieldBuilder)) {
545
                        $fieldConfiguration['builder'] = $fieldAnnotation->fieldBuilder;
546 19
                    } elseif (\is_array($fieldAnnotation->fieldBuilder)) {
547 19
                        list($builder, $builderConfig) = $fieldAnnotation->fieldBuilder;
548 19
                        $fieldConfiguration['builder'] = $builder;
549 19
                        $fieldConfiguration['builderConfig'] = $builderConfig ?: [];
550
                    } else {
551 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));
552
                    }
553
                } else {
554 19
                    if (!$fieldType) {
555 19
                        if ($isMethod) {
556 19
                            if ($method->hasReturnType()) {
557
                                try {
558 19
                                    $fieldConfiguration['type'] = self::resolveGraphqlTypeFromReflectionType($method->getReturnType(), self::$validOutputTypes);
559
                                } catch (\Exception $e) {
560 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()));
561
                                }
562
                            } else {
563 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));
564
                            }
565
                        } else {
566
                            try {
567 19
                                $fieldConfiguration['type'] = self::guessType($namespace, $annotations);
568 2
                            } catch (\Exception $e) {
569 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()));
570
                            }
571
                        }
572
                    }
573
                }
574
575 19
                if ($accessAnnotation) {
576 19
                    $fieldConfiguration['access'] = self::formatExpression($accessAnnotation->value);
577
                }
578
579 19
                if ($publicAnnotation) {
580 19
                    $fieldConfiguration['public'] = self::formatExpression($publicAnnotation->value);
581
                }
582
583 19
                if ($fieldAnnotation->complexity) {
584 19
                    $fieldConfiguration['complexity'] = self::formatExpression($fieldAnnotation->complexity);
585
                }
586
            }
587
588 19
            $fields[$fieldName] = $fieldConfiguration;
589
        }
590
591 20
        return $fields;
592
    }
593
594
    /**
595
     * Return fields config from Provider methods.
596
     *
597
     * @param string $className
598
     * @param array  $methods
599
     * @param bool   $isMutation
600
     *
601
     * @return array
602
     */
603 20
    private static function getGraphqlFieldsFromProviders(string $namespace, string $className, string $annotationName, string $targetType, bool $isRoot = false)
604
    {
605 20
        $fields = [];
606 20
        foreach (self::$providers as $className => $configuration) {
607 20
            $providerMethods = $configuration['methods'];
608 20
            $providerAnnotation = $configuration['annotation'];
609
610 20
            $filteredMethods = [];
611 20
            foreach ($providerMethods as $methodName => $config) {
612 20
                $annotations = $config['annotations'];
613
614 20
                $annotation = self::getFirstAnnotationMatching($annotations, \sprintf('Overblog\\GraphQLBundle\\Annotation\\%s', $annotationName));
615 20
                if (!$annotation) {
616 20
                    continue;
617
                }
618
619 20
                $annotationTarget = 'Query' === $annotationName ? $annotation->targetType : null;
620 20
                if (!$annotationTarget && $isRoot) {
621 19
                    $annotationTarget = $targetType;
622
                }
623
624 20
                if ($annotationTarget !== $targetType) {
625 20
                    continue;
626
                }
627
628 19
                $filteredMethods[$methodName] = $config;
629
            }
630
631 20
            $currentValue = \sprintf("service('%s')", self::formatNamespaceForExpression($className));
632 20
            $providerFields = self::getGraphqlFieldsFromAnnotations($namespace, $filteredMethods, false, true, $currentValue, $annotationName);
633 20
            foreach ($providerFields as $fieldName => $fieldConfig) {
634 19
                if ($providerAnnotation->prefix) {
635 19
                    $fieldName = \sprintf('%s%s', $providerAnnotation->prefix, $fieldName);
636
                }
637 19
                $fields[$fieldName] = $fieldConfig;
638
            }
639
        }
640
641 20
        return $fields;
642
    }
643
644
    /**
645
     * Get the config for description & deprecation reason.
646
     *
647
     * @param array $annotations
648
     * @param bool  $withDeprecation
649
     *
650
     * @return array
651
     */
652 20
    private static function getDescriptionConfiguration(array $annotations, bool $withDeprecation = false)
653
    {
654 20
        $config = [];
655 20
        $descriptionAnnotation = self::getFirstAnnotationMatching($annotations, 'Overblog\GraphQLBundle\Annotation\Description');
656 20
        if ($descriptionAnnotation) {
657 19
            $config['description'] = $descriptionAnnotation->value;
658
        }
659
660 20
        if ($withDeprecation) {
661 19
            $deprecatedAnnotation = self::getFirstAnnotationMatching($annotations, 'Overblog\GraphQLBundle\Annotation\Deprecated');
662 19
            if ($deprecatedAnnotation) {
663 19
                $config['deprecationReason'] = $deprecatedAnnotation->value;
664
            }
665
        }
666
667 20
        return $config;
668
    }
669
670
    /**
671
     * Get args config from an array of @Arg annotation or by auto-guessing if a method is provided.
672
     *
673
     * @param array             $args
674
     * @param \ReflectionMethod $method
675
     *
676
     * @return array
677
     */
678 19
    private static function getArgs(array $args = null, \ReflectionMethod $method = null)
679
    {
680 19
        $config = [];
681 19
        if ($args && !empty($args)) {
682 19
            foreach ($args as $arg) {
683 19
                $config[$arg->name] = ['type' => $arg->type] + ($arg->description ? ['description' => $arg->description] : []);
684
            }
685 19
        } elseif ($method) {
686 19
            $config = self::guessArgs($method);
687
        }
688
689 19
        return $config;
690
    }
691
692
    /**
693
     * Format an array of args to a list of arguments in an expression.
694
     *
695
     * @param array $args
696
     *
697
     * @return string
698
     */
699 19
    private static function formatArgsForExpression(array $args)
700
    {
701 19
        $mapping = [];
702 19
        foreach ($args as $name => $config) {
703 19
            $mapping[] = \sprintf('%s: "%s"', $name, $config['type']);
704
        }
705
706 19
        return \sprintf('arguments({%s}, args)', \implode(', ', $mapping));
707
    }
708
709
    /**
710
     * Format a namespace to be used in an expression (double escape).
711
     *
712
     * @param string $namespace
713
     *
714
     * @return string
715
     */
716 20
    private static function formatNamespaceForExpression(string $namespace)
717
    {
718 20
        return \str_replace('\\', '\\\\', $namespace);
719
    }
720
721
    /**
722
     * Get the first annotation matching given class.
723
     *
724
     * @param array        $annotations
725
     * @param string|array $annotationClass
726
     *
727
     * @return mixed
728
     */
729 20
    private static function getFirstAnnotationMatching(array $annotations, $annotationClass)
730
    {
731 20
        if (\is_string($annotationClass)) {
732 20
            $annotationClass = [$annotationClass];
733
        }
734
735 20
        foreach ($annotations as $annotation) {
736 20
            foreach ($annotationClass as $class) {
737 20
                if ($annotation instanceof $class) {
738 20
                    return $annotation;
739
                }
740
            }
741
        }
742
743 20
        return false;
744
    }
745
746
    /**
747
     * Format an expression (ie. add "@=" if not set).
748
     *
749
     * @param string $expression
750
     *
751
     * @return string
752
     */
753 19
    private static function formatExpression(string $expression)
754
    {
755 19
        return '@=' === \substr($expression, 0, 2) ? $expression : \sprintf('@=%s', $expression);
756
    }
757
758
    /**
759
     * Suffix a name if it is not already.
760
     *
761
     * @param string $name
762
     * @param string $suffix
763
     *
764
     * @return string
765
     */
766 19
    private static function suffixName(string $name, string $suffix)
767
    {
768 19
        return \substr($name, -\strlen($suffix)) === $suffix ? $name : \sprintf('%s%s', $name, $suffix);
769
    }
770
771
    /**
772
     * Try to guess a field type base on is annotations.
773
     *
774
     * @param string $namespace
775
     * @param array  $annotations
776
     *
777
     * @return string|false
778
     */
779 19
    private static function guessType(string $namespace, array $annotations)
780
    {
781 19
        $columnAnnotation = self::getFirstAnnotationMatching($annotations, 'Doctrine\ORM\Mapping\Column');
782 19
        if ($columnAnnotation) {
783 19
            $type = self::resolveTypeFromDoctrineType($columnAnnotation->type);
784 19
            $nullable = $columnAnnotation->nullable;
785 19
            if ($type) {
786 19
                return $nullable ? $type : \sprintf('%s!', $type);
787
            } else {
788 1
                throw new \Exception(\sprintf('Unable to auto-guess GraphQL type from Doctrine type "%s"', $columnAnnotation->type));
789
            }
790
        }
791
792
        $associationAnnotations = [
793 19
            'Doctrine\ORM\Mapping\OneToMany' => true,
794
            'Doctrine\ORM\Mapping\OneToOne' => false,
795
            'Doctrine\ORM\Mapping\ManyToMany' => true,
796
            'Doctrine\ORM\Mapping\ManyToOne' => false,
797
        ];
798
799 19
        $associationAnnotation = self::getFirstAnnotationMatching($annotations, \array_keys($associationAnnotations));
800 19
        if ($associationAnnotation) {
801 19
            $target = self::fullyQualifiedClassName($associationAnnotation->targetEntity, $namespace);
802 19
            $type = self::resolveTypeFromClass($target, ['type']);
803
804 19
            if ($type) {
805 19
                $isMultiple = $associationAnnotations[\get_class($associationAnnotation)];
806 19
                if ($isMultiple) {
807 19
                    return \sprintf('[%s]!', $type);
808
                } else {
809 19
                    $isNullable = false;
810 19
                    $joinColumn = self::getFirstAnnotationMatching($annotations, 'Doctrine\ORM\Mapping\JoinColumn');
811 19
                    if ($joinColumn) {
812 19
                        $isNullable = $joinColumn->nullable;
813
                    }
814
815 19
                    return \sprintf('%s%s', $type, $isNullable ? '' : '!');
816
                }
817
            } else {
818 1
                throw new \Exception(\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));
819
            }
820
        }
821
822
        throw new InvalidArgumentException(\sprintf('No Doctrine ORM annotation found.'));
823
    }
824
825
    /**
826
     * Resolve a FQN from classname and namespace.
827
     *
828
     * @param string $className
829
     * @param string $namespace
830
     *
831
     * @return string
832
     */
833 19
    public static function fullyQualifiedClassName(string $className, string $namespace)
834
    {
835 19
        if (false === \strpos($className, '\\') && $namespace) {
836 19
            return $namespace.'\\'.$className;
837
        }
838
839 1
        return $className;
840
    }
841
842
    /**
843
     * Resolve a GraphqlType from a doctrine type.
844
     *
845
     * @param string $doctrineType
846
     *
847
     * @return string|false
848
     */
849 19
    private static function resolveTypeFromDoctrineType(string $doctrineType)
850
    {
851 19
        if (isset(self::$doctrineMapping[$doctrineType])) {
852 19
            return self::$doctrineMapping[$doctrineType];
853
        }
854
855 19
        switch ($doctrineType) {
856 19
            case 'integer':
857 19
            case 'smallint':
858 19
            case 'bigint':
859 19
                return 'Int';
860
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
861 19
            case 'string':
862 1
            case 'text':
863 19
                return 'String';
864
                break;
865 1
            case 'bool':
866 1
            case 'boolean':
867
                return 'Boolean';
868
                break;
869 1
            case 'float':
870 1
            case 'decimal':
871
                return 'Float';
872
                break;
873
            default:
874 1
                return false;
875
        }
876
    }
877
878
    /**
879
     * Transform a method arguments from reflection to a list of GraphQL argument.
880
     *
881
     * @param \ReflectionMethod $method
882
     *
883
     * @return array
884
     */
885 19
    private static function guessArgs(\ReflectionMethod $method)
886
    {
887 19
        $arguments = [];
888 19
        foreach ($method->getParameters() as $index => $parameter) {
889 19
            if (!$parameter->hasType()) {
890 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()));
891
            }
892
893
            try {
894 19
                $gqlType = self::resolveGraphqlTypeFromReflectionType($parameter->getType(), self::$validInputTypes, $parameter->isDefaultValueAvailable());
895
            } catch (\Exception $e) {
896
                throw new InvalidArgumentException(\sprintf('Argument n°%s "$%s" on method "%s" cannot be auto-guessed : %s".', $index + 1, $parameter->getName(), $method->getName(), $e->getMessage()));
897
            }
898
899 19
            $argumentConfig = [];
900 19
            if ($parameter->isDefaultValueAvailable()) {
901 19
                $argumentConfig['defaultValue'] = $parameter->getDefaultValue();
902
            }
903
904 19
            $argumentConfig['type'] = $gqlType;
905
906 19
            $arguments[$parameter->getName()] = $argumentConfig;
907
        }
908
909 19
        return $arguments;
910
    }
911
912
    /**
913
     * Try to guess a GraphQL type from a Reflected Type.
914
     *
915
     * @param \ReflectionType $type
916
     *
917
     * @return string
918
     */
919 19
    private static function resolveGraphqlTypeFromReflectionType(\ReflectionType $type, array $filterGraphqlTypes = null, bool $isOptionnal = false)
920
    {
921 19
        $stype = (string) $type;
922 19
        if ($type->isBuiltin()) {
923 19
            $gqlType = self::resolveTypeFromPhpType($stype);
924 19
            if (!$gqlType) {
925 19
                throw new \Exception(\sprintf('No corresponding GraphQL type found for builtin type "%s"', $stype));
926
            }
927
        } else {
928 19
            $gqlType = self::resolveTypeFromClass($stype, $filterGraphqlTypes);
929 19
            if (!$gqlType) {
930
                throw new \Exception(\sprintf('No corresponding GraphQL %s found for class "%s"', $filterGraphqlTypes ? \implode(',', $filterGraphqlTypes) : 'object', $stype));
931
            }
932
        }
933
934 19
        return \sprintf('%s%s', $gqlType, ($type->allowsNull() || $isOptionnal) ? '' : '!');
935
    }
936
937
    /**
938
     * Resolve a GraphQL Type from a class name.
939
     *
940
     * @param string $className
941
     * @param array  $wantedTypes
942
     *
943
     * @return string|false
944
     */
945 19
    private static function resolveTypeFromClass(string $className, array $wantedTypes = null)
946
    {
947 19
        foreach (self::$classesMap as $gqlType => $config) {
948 19
            if ($config['class'] === $className) {
949 19
                if (!$wantedTypes || \in_array($config['type'], $wantedTypes)) {
950 19
                    return $gqlType;
951
                }
952
            }
953
        }
954
955 1
        return false;
956
    }
957
958
    /**
959
     * Resolve a PHP class from a GraphQL type.
960
     *
961
     * @param string $type
962
     *
963
     * @return string|false
964
     */
965 19
    private static function resolveClassFromType(string $type)
966
    {
967 19
        return self::$classesMap[$type] ?? false;
968
    }
969
970
    /**
971
     * Convert a PHP Builtin type to a GraphQL type.
972
     *
973
     * @param string $phpType
974
     *
975
     * @return string
976
     */
977 19
    private static function resolveTypeFromPhpType(string $phpType)
978
    {
979 19
        switch ($phpType) {
980 19
            case 'boolean':
981 19
            case 'bool':
982 19
                return 'Boolean';
983 19
            case 'integer':
984 19
            case 'int':
985 19
                return 'Int';
986 19
            case 'float':
987 19
            case 'double':
988 19
                return 'Float';
989 19
            case 'string':
990 19
                return 'String';
991
            default:
992
                return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type string.
Loading history...
993
        }
994
    }
995
}
996