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 ( 582d88...d84805 )
by Jérémiah
28:13 queued 19:34
created

AnnotationParser::getArgs()   A

Complexity

Conditions 6
Paths 3

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 6

Importance

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

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

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