Scrutinizer GitHub App not installed

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

Install GitHub App

Completed
Pull Request — annotations (#440)
by Vincent
16:53
created

AnnotationParser::getGraphqlType()   B

Complexity

Conditions 8
Paths 64

Size

Total Lines 35
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 8

Importance

Changes 0
Metric Value
eloc 20
dl 0
loc 35
c 0
b 0
f 0
ccs 20
cts 20
cp 1
rs 8.4444
cc 8
nc 64
nop 6
crap 8
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 Symfony\Component\Config\Resource\FileResource;
11
use Symfony\Component\DependencyInjection\ContainerBuilder;
12
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
13
14
class AnnotationParser implements PreParserInterface
15
{
16
    public const CLASSESMAP_CONTAINER_PARAMETER = 'overblog_graphql_types.classes_map';
17
18
    private static $annotationReader = null;
19
    private static $classesMap = [];
20
    private static $providers = [];
21
    private static $doctrineMapping = [];
22
23
    public const GQL_SCALAR = 'scalar';
24
    public const GQL_ENUM = 'enum';
25
    public const GQL_TYPE = 'type';
26
    public const GQL_INPUT = 'input';
27
    public const GQL_UNION = 'union';
28
    public const GQL_INTERFACE = 'interface';
29
30
    /**
31
     * @see https://facebook.github.io/graphql/draft/#sec-Input-and-Output-Types
32
     */
33
    protected static $validInputTypes = [self::GQL_SCALAR, self::GQL_ENUM, self::GQL_INPUT];
34
    protected static $validOutputTypes = [self::GQL_SCALAR, self::GQL_TYPE, self::GQL_INTERFACE, self::GQL_UNION, self::GQL_ENUM];
35
36
    /**
37
     * {@inheritdoc}
38
     *
39
     * @throws \ReflectionException
40
     * @throws InvalidArgumentException
41
     */
42 18
    public static function preParse(\SplFileInfo $file, ContainerBuilder $container, array $configs = []): void
43
    {
44 18
        self::proccessFile($file, $container, $configs, true);
45 18
    }
46
47
    /**
48
     * {@inheritdoc}
49
     *
50
     * @throws \ReflectionException
51
     * @throws InvalidArgumentException
52
     */
53 18
    public static function parse(\SplFileInfo $file, ContainerBuilder $container, array $configs = []): array
54
    {
55 18
        return self::proccessFile($file, $container, $configs);
56
    }
57
58
    /**
59
     * Clear the Annotation parser.
60
     */
61 17
    public static function clear(): void
62
    {
63 17
        self::$classesMap = [];
64 17
        self::$providers = [];
65 17
        self::$annotationReader = null;
66 17
    }
67
68
    /**
69
     * Process a file.
70
     *
71
     * @param \SplFileInfo     $file
72
     * @param ContainerBuilder $container
73
     * @param bool             $resolveClassMap
74
     *
75
     * @throws \ReflectionException
76
     * @throws InvalidArgumentException
77
     */
78 18
    public static function proccessFile(\SplFileInfo $file, ContainerBuilder $container, array $configs, bool $resolveClassMap = false): array
79
    {
80 18
        self::$doctrineMapping = $configs['doctrine']['types_mapping'];
81
82 18
        $rootQueryType = $configs['definitions']['schema']['default']['query'] ?? false;
83 18
        $rootMutationType = $configs['definitions']['schema']['default']['mutation'] ?? false;
84
85 18
        $container->addResource(new FileResource($file->getRealPath()));
86
87 18
        if (!$resolveClassMap) {
88 18
            $container->setParameter(self::CLASSESMAP_CONTAINER_PARAMETER, self::$classesMap);
89
        }
90
91
        try {
92 18
            $fileContent = \file_get_contents($file->getRealPath());
93
94 18
            $shortClassName = \substr($file->getFilename(), 0, -4);
95 18
            if (\preg_match('#namespace (.+);#', $fileContent, $namespace)) {
96 18
                $className = $namespace[1].'\\'.$shortClassName;
97 18
                $namespace = $namespace[1];
98
            } else {
99
                $className = $shortClassName;
100
            }
101
102 18
            $reflexionEntity = new \ReflectionClass($className);
103
104 18
            $classAnnotations = self::getAnnotationReader()->getClassAnnotations($reflexionEntity);
105
106 18
            $properties = [];
107 18
            foreach ($reflexionEntity->getProperties() as $property) {
108 17
                $properties[$property->getName()] = ['property' => $property, 'annotations' => self::getAnnotationReader()->getPropertyAnnotations($property)];
109
            }
110
111 18
            $methods = [];
112 18
            foreach ($reflexionEntity->getMethods() as $method) {
113 17
                $methods[$method->getName()] = ['method' => $method, 'annotations' => self::getAnnotationReader()->getMethodAnnotations($method)];
114
            }
115
116 18
            $gqlTypes = [];
117
118 18
            foreach ($classAnnotations as $classAnnotation) {
119 18
                $gqlConfiguration = $gqlType = $gqlName = false;
120
121
                switch (true) {
122 18
                    case $classAnnotation instanceof GQL\Type:
123 18
                        $gqlType = self::GQL_TYPE;
124 18
                        $gqlName = $classAnnotation->name ?: $shortClassName;
125 18
                        if (!$resolveClassMap) {
126 18
                            $isRootQuery = ($rootQueryType && $gqlName === $rootQueryType);
127 18
                            $isRootMutation = ($rootMutationType && $gqlName === $rootMutationType);
128 18
                            $currentValue = ($isRootQuery || $isRootMutation) ? \sprintf("service('%s')", self::formatNamespaceForExpression($className)) : 'value';
129
130 18
                            $gqlConfiguration = self::getGraphqlType($classAnnotation, $classAnnotations, $properties, $methods, $namespace, $currentValue);
131 18
                            $providerFields = self::getGraphqlFieldsFromProviders($namespace, $className, $isRootMutation ? 'Mutation' : 'Query', $gqlName, ($isRootQuery || $isRootMutation));
132 18
                            $gqlConfiguration['config']['fields'] = $providerFields + $gqlConfiguration['config']['fields'];
133
                        }
134 18
                        break;
135 17
                    case $classAnnotation instanceof GQL\Input:
136 17
                        $gqlType = self::GQL_INPUT;
137 17
                        $gqlName = $classAnnotation->name ?: self::suffixName($shortClassName, 'Input');
138 17
                        if (!$resolveClassMap) {
139 17
                            $gqlConfiguration = self::getGraphqlInput($classAnnotation, $classAnnotations, $properties, $namespace);
140
                        }
141 17
                        break;
142 17
                    case $classAnnotation instanceof GQL\Scalar:
143 17
                        $gqlType = self::GQL_SCALAR;
144 17
                        if (!$resolveClassMap) {
145 17
                            $gqlConfiguration = self::getGraphqlScalar($className, $classAnnotation, $classAnnotations);
146
                        }
147 17
                        break;
148 17
                    case $classAnnotation instanceof GQL\Enum:
149 17
                        $gqlType = self::GQL_ENUM;
150 17
                        if (!$resolveClassMap) {
151 17
                            $gqlConfiguration = self::getGraphqlEnum($classAnnotation, $classAnnotations, $reflexionEntity->getConstants());
152
                        }
153 17
                        break;
154 17
                    case $classAnnotation instanceof GQL\Union:
155 17
                        $gqlType = self::GQL_UNION;
156 17
                        if (!$resolveClassMap) {
157 17
                            $gqlConfiguration = self::getGraphqlUnion($className, $classAnnotation, $classAnnotations, $methods);
158
                        }
159 17
                        break;
160 17
                    case $classAnnotation instanceof GQL\TypeInterface:
161 17
                        $gqlType = self::GQL_INTERFACE;
162 17
                        if (!$resolveClassMap) {
163 17
                            $gqlConfiguration = self::getGraphqlInterface($classAnnotation, $classAnnotations, $properties, $methods, $namespace);
164
                        }
165 17
                        break;
166 17
                    case $classAnnotation instanceof GQL\Provider:
167 17
                        if ($resolveClassMap) {
168 17
                            self::$providers[$className] = ['annotation' => $classAnnotation, 'methods' => $methods];
169
                        }
170 17
                        break;
171
                    default:
172 17
                        continue 2;
173
                }
174
175 18
                if ($gqlType) {
176 18
                    if (!$gqlName) {
177 17
                        $gqlName = $classAnnotation->name ?: $shortClassName;
0 ignored issues
show
Bug introduced by
The property name does not seem to exist on Overblog\GraphQLBundle\Annotation\Provider.
Loading history...
178
                    }
179
180 18
                    if ($resolveClassMap) {
181 18
                        if (isset(self::$classesMap[$gqlName])) {
182 1
                            throw new InvalidArgumentException(\sprintf('The GraphQL type "%s" has already been registered in class "%s"', $gqlName, self::$classesMap[$gqlName]['class']));
183
                        }
184 18
                        self::$classesMap[$gqlName] = ['type' => $gqlType, 'class' => $className];
185
                    } else {
186 18
                        $gqlTypes = [$gqlName => $gqlConfiguration] + $gqlTypes;
187
                    }
188
                }
189
            }
190
191 18
            return $resolveClassMap ? self::$classesMap : $gqlTypes;
192 8
        } catch (\InvalidArgumentException $e) {
193 8
            throw new InvalidArgumentException(\sprintf('Failed to parse GraphQL annotations from file "%s".', $file), $e->getCode(), $e);
194
        }
195
    }
196
197
    /**
198
     * Retrieve annotation reader.
199
     *
200
     * @return AnnotationReader
201
     */
202 18
    private static function getAnnotationReader()
203
    {
204 18
        if (null === self::$annotationReader) {
205 17
            if (!\class_exists('\\Doctrine\\Common\\Annotations\\AnnotationReader') ||
206 17
                !\class_exists('\\Doctrine\\Common\\Annotations\\AnnotationRegistry')) {
207
                throw new \Exception('In order to use graphql annotation, you need to require doctrine annotations');
208
            }
209
210 17
            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

210
            /** @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...
211 17
            self::$annotationReader = new AnnotationReader();
212
        }
213
214 18
        return self::$annotationReader;
215
    }
216
217
    /**
218
     * Create a GraphQL Type configuration from annotations on class, properties and methods.
219
     *
220
     * @param GQL\Type $typeAnnotation
221
     * @param array    $classAnnotations
222
     * @param array    $properties
223
     * @param array    $methods
224
     * @param string   $namespace
225
     * @param string   $currentValue
226
     *
227
     * @return array
228
     */
229 18
    private static function getGraphqlType(GQL\Type $typeAnnotation, array $classAnnotations, array $properties, array $methods, string $namespace, string $currentValue)
230
    {
231 18
        $typeConfiguration = [];
232
233 18
        $fields = self::getGraphqlFieldsFromAnnotations($namespace, $properties, false, false, $currentValue);
234 18
        $fields = self::getGraphqlFieldsFromAnnotations($namespace, $methods, false, true, $currentValue) + $fields;
235
236 18
        $typeConfiguration['fields'] = $fields;
237 18
        $typeConfiguration = self::getDescriptionConfiguration($classAnnotations) + $typeConfiguration;
238
239 18
        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...
240 17
            $typeConfiguration['interfaces'] = $typeAnnotation->interfaces;
241
        }
242
243 18
        if ($typeAnnotation->resolveField) {
244 17
            $typeConfiguration['resolveField'] = self::formatExpression($typeAnnotation->resolveField);
245
        }
246
247 18
        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...
248
            $typeConfiguration['builders'] = \array_map(function ($fieldsBuilderAnnotation) {
249 17
                return ['builder' => $fieldsBuilderAnnotation->builder, 'builderConfig' => $fieldsBuilderAnnotation->builderConfig];
250 17
            }, $typeAnnotation->builders);
251
        }
252
253 18
        $publicAnnotation = self::getFirstAnnotationMatching($classAnnotations, 'Overblog\GraphQLBundle\Annotation\IsPublic');
254 18
        if ($publicAnnotation) {
255 17
            $typeConfiguration['fieldsDefaultPublic'] = self::formatExpression($publicAnnotation->value);
256
        }
257
258 18
        $accessAnnotation = self::getFirstAnnotationMatching($classAnnotations, 'Overblog\GraphQLBundle\Annotation\Access');
259 18
        if ($accessAnnotation) {
260 17
            $typeConfiguration['fieldsDefaultAccess'] = self::formatExpression($accessAnnotation->value);
261
        }
262
263 18
        return ['type' => $typeAnnotation->isRelay ? 'relay-mutation-payload' : 'object', 'config' => $typeConfiguration];
264
    }
265
266
    /**
267
     * Create a GraphQL Interface type configuration from annotations on properties.
268
     *
269
     * @param string        $shortClassName
270
     * @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...
271
     * @param array         $properties
272
     * @param array         $methods
273
     * @param string        $namespace
274
     *
275
     * @return array
276
     */
277 17
    private static function getGraphqlInterface(GQL\TypeInterface $interfaceAnnotation, array $classAnnotations, array $properties, array $methods, string $namespace)
278
    {
279 17
        $interfaceConfiguration = [];
280
281 17
        $fields = self::getGraphqlFieldsFromAnnotations($namespace, $properties);
282 17
        $fields = self::getGraphqlFieldsFromAnnotations($namespace, $methods, false, true) + $fields;
283
284 17
        $interfaceConfiguration['fields'] = $fields;
285 17
        $interfaceConfiguration = self::getDescriptionConfiguration($classAnnotations) + $interfaceConfiguration;
286
287 17
        $interfaceConfiguration['resolveType'] = self::formatExpression($interfaceAnnotation->resolveType);
288
289 17
        return ['type' => 'interface', 'config' => $interfaceConfiguration];
290
    }
291
292
    /**
293
     * Create a GraphQL Input type configuration from annotations on properties.
294
     *
295
     * @param string    $shortClassName
296
     * @param GQL\Input $inputAnnotation
297
     * @param array     $properties
298
     * @param string    $namespace
299
     *
300
     * @return array
301
     */
302 17
    private static function getGraphqlInput(GQL\Input $inputAnnotation, array $classAnnotations, array $properties, string $namespace)
303
    {
304 17
        $inputConfiguration = [];
305 17
        $fields = self::getGraphqlFieldsFromAnnotations($namespace, $properties, true);
306
307 17
        $inputConfiguration['fields'] = $fields;
308 17
        $inputConfiguration = self::getDescriptionConfiguration($classAnnotations) + $inputConfiguration;
309
310 17
        return ['type' => $inputAnnotation->isRelay ? 'relay-mutation-input' : 'input-object', 'config' => $inputConfiguration];
311
    }
312
313
    /**
314
     * Get a Graphql scalar configuration from given scalar annotation.
315
     *
316
     * @param string     $shortClassName
317
     * @param string     $className
318
     * @param GQL\Scalar $scalarAnnotation
319
     * @param array      $classAnnotations
320
     *
321
     * @return array
322
     */
323 17
    private static function getGraphqlScalar(string $className, GQL\Scalar $scalarAnnotation, array $classAnnotations)
324
    {
325 17
        $scalarConfiguration = [];
326
327 17
        if ($scalarAnnotation->scalarType) {
328 17
            $scalarConfiguration['scalarType'] = self::formatExpression($scalarAnnotation->scalarType);
329
        } else {
330
            $scalarConfiguration = [
331 17
                'serialize' => [$className, 'serialize'],
332 17
                'parseValue' => [$className, 'parseValue'],
333 17
                'parseLiteral' => [$className, 'parseLiteral'],
334
            ];
335
        }
336
337 17
        $scalarConfiguration = self::getDescriptionConfiguration($classAnnotations) + $scalarConfiguration;
338
339 17
        return ['type' => 'custom-scalar', 'config' => $scalarConfiguration];
340
    }
341
342
    /**
343
     * Get a Graphql Enum configuration from given enum annotation.
344
     *
345
     * @param string   $shortClassName
346
     * @param GQL\Enum $enumAnnotation
347
     * @param array    $classAnnotations
348
     * @param array    $constants
349
     *
350
     * @return array
351
     */
352 17
    private static function getGraphqlEnum(GQL\Enum $enumAnnotation, array $classAnnotations, array $constants)
353
    {
354 17
        $enumValues = $enumAnnotation->values ? $enumAnnotation->values : [];
355
356 17
        $values = [];
357
358 17
        foreach ($constants as $name => $value) {
359
            $valueAnnotation = \current(\array_filter($enumValues, function ($enumValueAnnotation) use ($name) {
360 17
                return $enumValueAnnotation->name == $name;
361 17
            }));
362 17
            $valueConfig = [];
363 17
            $valueConfig['value'] = $value;
364
365 17
            if ($valueAnnotation && $valueAnnotation->description) {
366 17
                $valueConfig['description'] = $valueAnnotation->description;
367
            }
368
369 17
            if ($valueAnnotation && $valueAnnotation->deprecationReason) {
370 17
                $valueConfig['deprecationReason'] = $valueAnnotation->deprecationReason;
371
            }
372
373 17
            $values[$name] = $valueConfig;
374
        }
375
376 17
        $enumConfiguration = ['values' => $values];
377 17
        $enumConfiguration = self::getDescriptionConfiguration($classAnnotations) + $enumConfiguration;
378
379 17
        return ['type' => 'enum', 'config' => $enumConfiguration];
380
    }
381
382
    /**
383
     * Get a Graphql Union configuration from given union annotation.
384
     *
385
     * @param string    $className
386
     * @param GQL\Union $unionAnnotation
387
     * @param array     $classAnnotations
388
     * @param array     $methods
389
     *
390
     * @return array
391
     */
392 17
    private static function getGraphqlUnion(string $className, GQL\Union $unionAnnotation, array $classAnnotations, array $methods): array
393
    {
394 17
        $unionConfiguration = ['types' => $unionAnnotation->types];
395 17
        $unionConfiguration = self::getDescriptionConfiguration($classAnnotations) + $unionConfiguration;
396
397 17
        if ($unionAnnotation->resolveType) {
398 17
            $unionConfiguration['resolveType'] = self::formatExpression($unionAnnotation->resolveType);
399
        } else {
400 17
            if (isset($methods['resolveType'])) {
401 17
                $method = $methods['resolveType']['method'];
402 17
                if ($method->isStatic() && $method->isPublic()) {
403 17
                    $unionConfiguration['resolveType'] = self::formatExpression(\sprintf("@=call('%s::%s', [service('overblog_graphql.type_resolver'), value], true)", self::formatNamespaceForExpression($className), 'resolveType'));
404
                } else {
405 17
                    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.'));
406
                }
407
            } else {
408 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.'));
409
            }
410
        }
411
412 17
        return ['type' => 'union', 'config' => $unionConfiguration];
413
    }
414
415
    /**
416
     * Create Graphql fields configuration based on annotations.
417
     *
418
     * @param string $namespace
419
     * @param array  $propertiesOrMethods
420
     * @param bool   $isInput
421
     * @param bool   $isMethod
422
     * @param string $currentValue
423
     *
424
     * @return array
425
     */
426 18
    private static function getGraphqlFieldsFromAnnotations(string $namespace, array $propertiesOrMethods, bool $isInput = false, bool $isMethod = false, string $currentValue = 'value', string $fieldAnnotationName = 'Field'): array
427
    {
428 18
        $fields = [];
429 18
        foreach ($propertiesOrMethods as $target => $config) {
430 17
            $annotations = $config['annotations'];
431 17
            $method = $isMethod ? $config['method'] : false;
432 17
            $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...
433
434 17
            $fieldAnnotation = self::getFirstAnnotationMatching($annotations, \sprintf('Overblog\GraphQLBundle\Annotation\%s', $fieldAnnotationName));
435 17
            $accessAnnotation = self::getFirstAnnotationMatching($annotations, 'Overblog\GraphQLBundle\Annotation\Access');
436 17
            $publicAnnotation = self::getFirstAnnotationMatching($annotations, 'Overblog\GraphQLBundle\Annotation\IsPublic');
437
438 17
            if (!$fieldAnnotation) {
439 1
                if ($accessAnnotation || $publicAnnotation) {
440 1
                    throw new InvalidArgumentException(\sprintf('The annotations "@Access" and/or "@Visible" defined on "%s" are only usable in addition of annotation "@Field"', $target));
441
                }
442
                continue;
443
            }
444
445 17
            if ($isMethod && !$method->isPublic()) {
446 1
                throw new InvalidArgumentException(\sprintf('The Annotation "@Field" can only be applied to public method. The method "%s" is not public.', $target));
447
            }
448
449
            // Ignore field with resolver when the type is an Input
450 17
            if ($fieldAnnotation->resolve && $isInput) {
451
                continue;
452
            }
453
454 17
            $fieldName = $target;
455 17
            $fieldType = $fieldAnnotation->type;
456 17
            $fieldConfiguration = [];
457 17
            if ($fieldType) {
458 17
                $resolvedType = self::resolveClassFromType($fieldType);
459 17
                if ($resolvedType) {
460 17
                    if ($isInput && !\in_array($resolvedType['type'], self::$validInputTypes)) {
461
                        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']));
462
                    }
463
                }
464
465 17
                $fieldConfiguration['type'] = $fieldType;
466
            }
467
468 17
            $fieldConfiguration = self::getDescriptionConfiguration($annotations, true) + $fieldConfiguration;
469
470 17
            if (!$isInput) {
471 17
                $args = [];
0 ignored issues
show
Unused Code introduced by
The assignment to $args is dead and can be removed.
Loading history...
472 17
                $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

472
                $args = self::getArgs($fieldAnnotation->args, /** @scrutinizer ignore-type */ $isMethod && !$fieldAnnotation->argsBuilder ? $method : null);
Loading history...
473
474 17
                if (!empty($args)) {
475 17
                    $fieldConfiguration['args'] = $args;
476
                }
477
478 17
                $fieldName = $fieldAnnotation->name ?: $fieldName;
479
480 17
                if ($fieldAnnotation->resolve) {
481 17
                    $fieldConfiguration['resolve'] = self::formatExpression($fieldAnnotation->resolve);
482
                } else {
483 17
                    if ($isMethod) {
484 17
                        $fieldConfiguration['resolve'] = self::formatExpression(\sprintf('call(%s.%s, %s)', $currentValue, $target, self::formatArgsForExpression($args)));
485
                    } else {
486 17
                        if ($fieldName !== $target || 'value' !== $currentValue) {
487
                            $fieldConfiguration['resolve'] = self::formatExpression(\sprintf('%s.%s', $currentValue, $target));
488
                        }
489
                    }
490
                }
491
492 17
                if ($fieldAnnotation->argsBuilder) {
493 17
                    if (\is_string($fieldAnnotation->argsBuilder)) {
494
                        $fieldConfiguration['argsBuilder'] = $fieldAnnotation->argsBuilder;
495 17
                    } elseif (\is_array($fieldAnnotation->argsBuilder)) {
496 17
                        list($builder, $builderConfig) = $fieldAnnotation->argsBuilder;
497 17
                        $fieldConfiguration['argsBuilder'] = ['builder' => $builder, 'config' => $builderConfig];
498
                    } else {
499
                        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));
500
                    }
501
                }
502
503 17
                if ($fieldAnnotation->fieldBuilder) {
504 17
                    if (\is_string($fieldAnnotation->fieldBuilder)) {
505
                        $fieldConfiguration['builder'] = $fieldAnnotation->fieldBuilder;
506 17
                    } elseif (\is_array($fieldAnnotation->fieldBuilder)) {
507 17
                        list($builder, $builderConfig) = $fieldAnnotation->fieldBuilder;
508 17
                        $fieldConfiguration['builder'] = $builder;
509 17
                        $fieldConfiguration['builderConfig'] = $builderConfig ?: [];
510
                    } else {
511 17
                        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));
512
                    }
513
                } else {
514 17
                    if (!$fieldType) {
515 17
                        if ($isMethod) {
516 17
                            if ($method->hasReturnType()) {
517
                                try {
518 17
                                    $fieldConfiguration['type'] = self::resolveGraphqlTypeFromReflectionType($method->getReturnType(), self::$validOutputTypes);
519
                                } catch (\Exception $e) {
520 17
                                    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()));
521
                                }
522
                            } else {
523 17
                                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));
524
                            }
525
                        } else {
526
                            try {
527 17
                                $fieldConfiguration['type'] = self::guessType($namespace, $annotations);
528 2
                            } catch (\Exception $e) {
529 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()));
530
                            }
531
                        }
532
                    }
533
                }
534
535 17
                if ($accessAnnotation) {
536 17
                    $fieldConfiguration['access'] = self::formatExpression($accessAnnotation->value);
537
                }
538
539 17
                if ($publicAnnotation) {
540 17
                    $fieldConfiguration['public'] = self::formatExpression($publicAnnotation->value);
541
                }
542
543 17
                if ($fieldAnnotation->complexity) {
544 17
                    $fieldConfiguration['complexity'] = self::formatExpression($fieldAnnotation->complexity);
545
                }
546
            }
547
548 17
            $fields[$fieldName] = $fieldConfiguration;
549
        }
550
551 18
        return $fields;
552
    }
553
554
    /**
555
     * Return fields config from Provider methods.
556
     *
557
     * @param string $className
558
     * @param array  $methods
559
     * @param bool   $isMutation
560
     *
561
     * @return array
562
     */
563 18
    private static function getGraphqlFieldsFromProviders(string $namespace, string $className, string $annotationName, string $targetType, bool $isRoot = false)
0 ignored issues
show
Unused Code introduced by
The parameter $className is not used and could be removed. ( Ignorable by Annotation )

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

563
    private static function getGraphqlFieldsFromProviders(string $namespace, /** @scrutinizer ignore-unused */ string $className, string $annotationName, string $targetType, bool $isRoot = false)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

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