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 (#404)
by Vincent
18:58 queued 12:53
created

AnnotationParser::getGraphQLRelayMutationField()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 22

Duplication

Lines 5
Ratio 22.73 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
dl 5
loc 22
ccs 0
cts 11
cp 0
rs 9.568
c 0
b 0
f 0
cc 3
nc 3
nop 1
crap 12
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\Enum as AnnotationEnum;
10
use Overblog\GraphQLBundle\Annotation\InputType as AnnotationInputType;
11
use Overblog\GraphQLBundle\Annotation\Scalar as AnnotationScalar;
12
use Overblog\GraphQLBundle\Annotation\Type as AnnotationType;
13
use Overblog\GraphQLBundle\Annotation\TypeInterface as AnnotationInterface;
14
use Overblog\GraphQLBundle\Annotation\Union as AnnotationUnion;
15
use Symfony\Component\Config\Resource\FileResource;
16
use Symfony\Component\DependencyInjection\ContainerBuilder;
17
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
18
19
class AnnotationParser implements ParserInterface
20
{
21
    private static $annotationReader = null;
22
    private static $classesMap = [];
23
24
    /**
25
     * {@inheritdoc}
26
     *
27
     * @throws \ReflectionException
28
     * @throws InvalidArgumentException
29
     */
30 13
    public static function parse(\SplFileInfo $file, ContainerBuilder $container): array
31
    {
32 13
        $container->addResource(new FileResource($file->getRealPath()));
33
        try {
34 13
            $fileContent = \file_get_contents($file->getRealPath());
35
36 13
            $shortClassName = \substr($file->getFilename(), 0, -4);
37 13
            if (\preg_match('#namespace (.+);#', $fileContent, $namespace)) {
38 13
                $className = $namespace[1].'\\'.$shortClassName;
39
            } else {
40
                $className = $shortClassName;
41
            }
42
43 13
            $reflexionEntity = new \ReflectionClass($className);
44
45 13
            $classAnnotations = self::getAnnotationReader()->getClassAnnotations($reflexionEntity);
46
47 13
            $properties = $reflexionEntity->getProperties();
48
49 13
            $propertiesAnnotations = [];
50 13
            foreach ($properties as $property) {
51 8
                $propertiesAnnotations[$property->getName()] = self::getAnnotationReader()->getPropertyAnnotations($property);
52
            }
53
54 13
            $methods = $reflexionEntity->getMethods();
55 13
            $methodsAnnotations = [];
56 13
            foreach ($methods as $method) {
57 3
                $methodsAnnotations[$method->getName()] = self::getAnnotationReader()->getMethodAnnotations($method);
0 ignored issues
show
Bug introduced by
Consider using $method->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
58
            }
59
60 13
            $gqlTypes = [];
61
62 13
            foreach ($classAnnotations as $classAnnotation) {
63 13
                $method = false;
64 13
                switch (\get_class($classAnnotation)) {
65 13
                    case 'Overblog\GraphQLBundle\Annotation\Type':
66 7
                        $gqlTypes += self::getGraphqlType($shortClassName, $classAnnotation, $classAnnotations, $propertiesAnnotations, $methodsAnnotations);
67 7
                        break;
68 9
                    case 'Overblog\GraphQLBundle\Annotation\InputType':
69 1
                        $gqlTypes += self::getGraphqlInputType($shortClassName, $classAnnotation, $classAnnotations, $propertiesAnnotations);
70 1
                        break;
71 9
                    case 'Overblog\GraphQLBundle\Annotation\Scalar':
72 2
                        $gqlTypes += self::getGraphqlScalar($shortClassName, $className, $classAnnotation, $classAnnotations);
73 2
                        break;
74 8
                    case 'Overblog\GraphQLBundle\Annotation\Enum':
75 1
                        $gqlTypes += self::getGraphqlEnum($shortClassName, $classAnnotation, $classAnnotations, $reflexionEntity->getConstants());
76 1
                        break;
77 8
                    case 'Overblog\GraphQLBundle\Annotation\Union':
78 1
                        $gqlTypes += self::getGraphqlUnion($shortClassName, $classAnnotation, $classAnnotations);
79 1
                        break;
80 8
                    case 'Overblog\GraphQLBundle\Annotation\TypeInterface':
81 1
                        $gqlTypes += self::getGraphqlInterface($shortClassName, $classAnnotation, $classAnnotations, $propertiesAnnotations, $methodsAnnotations);
82 1
                        break;
83
                }
84
85 13
                if ($method) {
86 13
                    $gqlTypes += self::$method($shortClassName, $classAnnotation, $propertiesAnnotations);
87
                }
88
            }
89
90 13
            return $gqlTypes;
91
        } catch (\InvalidArgumentException $e) {
92
            throw new InvalidArgumentException(\sprintf('Unable to parse file "%s".', $file), $e->getCode(), $e);
93
        }
94
    }
95
96
    /**
97
     * Retrieve annotation reader.
98
     *
99
     * @return AnnotationReader
100
     */
101 13
    private static function getAnnotationReader()
102
    {
103 13
        if (null === self::$annotationReader) {
104 1
            if (!\class_exists('\\Doctrine\\Common\\Annotations\\AnnotationReader') ||
105 1
                !\class_exists('\\Doctrine\\Common\\Annotations\\AnnotationRegistry')) {
106
                throw new \Exception('In order to use graphql annotation, you need to require doctrine annotations');
107
            }
108
109 1
            AnnotationRegistry::registerLoader('class_exists');
110 1
            self::$annotationReader = new AnnotationReader();
111
        }
112
113 13
        return self::$annotationReader;
114
    }
115
116
    /**
117
     * Create a GraphQL Type configuration from annotations on class, properties and methods.
118
     *
119
     * @param string         $shortClassName
120
     * @param AnnotationType $typeAnnotation
121
     * @param array          $classAnnotations
122
     * @param array          $propertiesAnnotations
123
     * @param array          $methodsAnnotations
124
     *
125
     * @return array
126
     */
127 7
    private static function getGraphqlType(string $shortClassName, AnnotationType $typeAnnotation, array $classAnnotations, array $propertiesAnnotations, array $methodsAnnotations)
128
    {
129 7
        $typeName = $typeAnnotation->name ?: $shortClassName;
130 7
        $typeConfiguration = [];
131
132 7
        $fields = self::getGraphqlFieldsFromAnnotations($propertiesAnnotations);
133 7
        $fields += self::getGraphqlFieldsFromAnnotations($methodsAnnotations, false, true);
134
135 7
        if (empty($fields)) {
136
            return [];
137
        }
138
139 7
        $typeConfiguration['fields'] = $fields;
140
141 7
        $publicAnnotation = self::getFirstAnnotationMatching($classAnnotations, 'Overblog\GraphQLBundle\Annotation\IsPublic');
142 7
        if ($publicAnnotation) {
143 1
            $typeConfiguration['fieldsDefaultPublic'] = self::formatExpression($publicAnnotation->value);
144
        }
145
146 7
        $accessAnnotation = self::getFirstAnnotationMatching($classAnnotations, 'Overblog\GraphQLBundle\Annotation\Access');
147 7
        if ($accessAnnotation) {
148 1
            $typeConfiguration['fieldsDefaultAccess'] = self::formatExpression($accessAnnotation->value);
149
        }
150
151 7
        $typeConfiguration += self::getDescriptionConfiguration($classAnnotations);
152 7
        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...
153
            $typeConfiguration['interfaces'] = $typeAnnotation->interfaces;
154
        }
155
156 7
        return [$typeName => ['type' => $typeAnnotation->isRelay ? 'relay-mutation-payload' : 'object', 'config' => $typeConfiguration]];
157
    }
158
159
    /**
160
     * Create a GraphQL Interface type configuration from annotations on properties.
161
     *
162
     * @param string              $shortClassName
163
     * @param AnnotationInterface $interfaceAnnotation
164
     * @param array               $propertiesAnnotations
165
     *
166
     * @return array
167
     */
168 1
    private static function getGraphqlInterface(string $shortClassName, AnnotationInterface $interfaceAnnotation, array $classAnnotations, array $propertiesAnnotations, array $methodsAnnotations)
169
    {
170 1
        $interfaceName = $interfaceAnnotation->name ?: $shortClassName;
171 1
        $interfaceConfiguration = [];
172
173 1
        $fields = self::getGraphqlFieldsFromAnnotations($propertiesAnnotations);
174 1
        $fields += self::getGraphqlFieldsFromAnnotations($methodsAnnotations, false, true);
175
176 1
        if (empty($fields)) {
177
            return [];
178
        }
179
180 1
        $interfaceConfiguration['fields'] = $fields;
181 1
        $interfaceConfiguration += self::getDescriptionConfiguration($classAnnotations);
182
183 1
        return [$interfaceName => ['type' => 'interface', 'config' => $interfaceConfiguration]];
184
    }
185
186
    /**
187
     * Create a GraphQL Input type configuration from annotations on properties.
188
     *
189
     * @param string              $shortClassName
190
     * @param AnnotationInputType $inputAnnotation
191
     * @param array               $propertiesAnnotations
192
     *
193
     * @return array
194
     */
195 1
    private static function getGraphqlInputType(string $shortClassName, AnnotationInputType $inputAnnotation, array $classAnnotations, array $propertiesAnnotations)
196
    {
197 1
        $inputName = $inputAnnotation->name ?: self::suffixName($shortClassName, 'Input');
198 1
        $inputConfiguration = [];
199 1
        $fields = self::getGraphqlFieldsFromAnnotations($propertiesAnnotations, true);
200
201 1
        if (empty($fields)) {
202
            return [];
203
        }
204
205 1
        $inputConfiguration['fields'] = $fields;
206 1
        $inputConfiguration += self::getDescriptionConfiguration($classAnnotations);
207
208 1
        return [$inputName => ['type' => $inputAnnotation->isRelay ? 'relay-mutation-input' : 'input-object', 'config' => $inputConfiguration]];
209
    }
210
211
    /**
212
     * Get a Graphql scalar configuration from given scalar annotation.
213
     *
214
     * @param string           $shortClassName
215
     * @param string           $className
216
     * @param AnnotationScalar $scalarAnnotation
217
     * @param array            $classAnnotations
218
     *
219
     * @return array
220
     */
221 2
    private static function getGraphqlScalar(string $shortClassName, string $className, AnnotationScalar $scalarAnnotation, array $classAnnotations)
222
    {
223 2
        $scalarName = $scalarAnnotation->name ?: $shortClassName;
224 2
        $scalarConfiguration = [];
225
226 2
        if ($scalarAnnotation->scalarType) {
227 1
            $scalarConfiguration['scalarType'] = self::formatExpression($scalarAnnotation->scalarType);
228
        } else {
229
            $scalarConfiguration = [
230 1
                'serialize' => [$className, 'serialize'],
231 1
                'parseValue' => [$className, 'parseValue'],
232 1
                'parseLiteral' => [$className, 'parseLiteral'],
233
            ];
234
        }
235
236 2
        $scalarConfiguration += self::getDescriptionConfiguration($classAnnotations);
237
238 2
        return [$scalarName => ['type' => 'custom-scalar', 'config' => $scalarConfiguration]];
239
    }
240
241
    /**
242
     * Get a Graphql Enum configuration from given enum annotation.
243
     *
244
     * @param string         $shortClassName
245
     * @param AnnotationEnum $enumAnnotation
246
     * @param array          $classAnnotations
247
     * @param array          $constants
248
     *
249
     * @return array
250
     */
251 1
    private static function getGraphqlEnum(string $shortClassName, AnnotationEnum $enumAnnotation, array $classAnnotations, array $constants)
252
    {
253 1
        $enumName = $enumAnnotation->name ?: self::suffixName($shortClassName, 'Enum');
254 1
        $enumValues = $enumAnnotation->values ? $enumAnnotation->values : [];
255
256 1
        $values = [];
257
258 1
        foreach ($constants as $name => $value) {
259
            $valueAnnotation = \current(\array_filter($enumValues, function ($enumValueAnnotation) use ($name) {
260 1
                return $enumValueAnnotation->name == $name;
261 1
            }));
262 1
            $valueConfig = [];
263 1
            $valueConfig['value'] = $value;
264
265 1
            if ($valueAnnotation && $valueAnnotation->description) {
266 1
                $valueConfig['description'] = $valueAnnotation->description;
267
            }
268
269 1
            if ($valueAnnotation && $valueAnnotation->deprecationReason) {
270
                $valueConfig['deprecationReason'] = $valueAnnotation->deprecationReason;
271
            }
272
273 1
            $values[$name] = $valueConfig;
274
        }
275
276 1
        $enumConfiguration = ['values' => $values];
277 1
        $enumConfiguration += self::getDescriptionConfiguration($classAnnotations);
278
279 1
        return [$enumName => ['type' => 'enum', 'config' => $enumConfiguration]];
280
    }
281
282
    /**
283
     * Get a Graphql Union configuration from given union annotation.
284
     *
285
     * @param string          $shortClassName
286
     * @param AnnotationUnion $unionAnnotation
287
     * @param array           $classAnnotations
288
     *
289
     * @return array
290
     */
291 1
    private static function getGraphqlUnion(string $shortClassName, AnnotationUnion $unionAnnotation, array $classAnnotations): array
292
    {
293 1
        $unionName = $unionAnnotation->name ?: $shortClassName;
294 1
        $unionConfiguration = ['types' => $unionAnnotation->types];
295 1
        $unionConfiguration += self::getDescriptionConfiguration($classAnnotations);
296
297 1
        return [$unionName => ['type' => 'union', 'config' => $unionConfiguration]];
298
    }
299
300
    /**
301
     * Create Graphql fields configuration based on annotation.
302
     *
303
     * @param array $annotations
304
     * @param bool  $isInput
305
     * @param bool  $isMethod
306
     *
307
     * @return array
308
     */
309 9
    private static function getGraphqlFieldsFromAnnotations(array $annotations, bool $isInput = false, bool $isMethod = false): array
310
    {
311 9
        $fields = [];
312 9
        foreach ($annotations as $target => $annotations) {
313 9
            $fieldAnnotation = self::getFirstAnnotationMatching($annotations, 'Overblog\GraphQLBundle\Annotation\Field');
314 9
            $accessAnnotation = self::getFirstAnnotationMatching($annotations, 'Overblog\GraphQLBundle\Annotation\Access');
315 9
            $publicAnnotation = self::getFirstAnnotationMatching($annotations, 'Overblog\GraphQLBundle\Annotation\IsPublic');
316
317 9
            if (!$fieldAnnotation) {
318
                if ($accessAnnotation || $publicAnnotation) {
319
                    throw new InvalidArgumentException(\sprintf('The annotations "@Access" and/or "@Visible" defined on "%s" are only usable in addition of annotation @Field', $target));
320
                }
321
                continue;
322
            }
323
324
            // Ignore field with resolver when the type is an Input
325 9
            if ($fieldAnnotation->resolve && $isInput) {
326
                continue;
327
            }
328
329 9
            $propertyName = $target;
330 9
            $fieldType = $fieldAnnotation->type;
331 9
            $fieldConfiguration = [];
332 9
            if ($fieldType) {
333 7
                $fieldConfiguration['type'] = $fieldType;
334
            }
335
336 9
            $fieldConfiguration += self::getDescriptionConfiguration($annotations, true);
337
338 9
            if (!$isInput) {
339 8
                $args = [];
340 8
                if ($fieldAnnotation->args) {
341 1
                    foreach ($fieldAnnotation->args as $annotationArg) {
342 1
                        $args[$annotationArg->name] = ['type' => $annotationArg->type] + ($annotationArg->description ? ['description' => $annotationArg->description] : []);
343
                    }
344
345 1
                    if (!empty($args)) {
346 1
                        $fieldConfiguration['args'] = $args;
347
                    }
348
349
                    $args = \array_map(function ($a) {
350 1
                        return \sprintf("args['%s']", $a);
351 1
                    }, \array_keys($args));
352
                }
353
354 8
                $propertyName = $fieldAnnotation->name ?: $propertyName;
355
356 8
                if ($fieldAnnotation->resolve) {
357 1
                    $fieldConfiguration['resolve'] = self::formatExpression($fieldAnnotation->resolve);
358
                } else {
359 8
                    if ($isMethod) {
360 2
                        $fieldConfiguration['resolve'] = self::formatExpression(\sprintf('value.%s(%s)', $target, \implode(', ', $args)));
361 7
                    } elseif ($fieldAnnotation->name) {
362
                        $fieldConfiguration['resolve'] = self::formatExpression(\sprintf('value.%s', $target));
363
                    }
364
                }
365
366 8 View Code Duplication
                if ($fieldAnnotation->argsBuilder) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
367 1
                    if (\is_string($fieldAnnotation->argsBuilder)) {
368 1
                        $fieldConfiguration['argsBuilder'] = $fieldAnnotation->argsBuilder;
369 1
                    } elseif (\is_array($fieldAnnotation->argsBuilder)) {
370 1
                        list($builder, $builderConfig) = $fieldAnnotation->argsBuilder;
371 1
                        $fieldConfiguration['argsBuilder'] = ['builder' => $builder, 'config' => $builderConfig];
372
                    } else {
373
                        throw new InvalidArgumentException(\sprintf('The attribute "argsBuilder" on Graphql annotation "@Field" defined on %s must be a string or an array where first index is the builder name and the second is the config.', $target));
374
                    }
375
                }
376
377 8 View Code Duplication
                if ($fieldAnnotation->fieldBuilder) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
378 2
                    if (\is_string($fieldAnnotation->fieldBuilder)) {
379 2
                        $fieldConfiguration['builder'] = $fieldAnnotation->fieldBuilder;
380 2
                    } elseif (\is_array($fieldAnnotation->fieldBuilder)) {
381 2
                        list($builder, $builderConfig) = $fieldAnnotation->fieldBuilder;
382 2
                        $fieldConfiguration['builder'] = $builder;
383 2
                        $fieldConfiguration['builderConfig'] = $builderConfig ?: [];
384
                    } else {
385
                        throw new InvalidArgumentException(\sprintf('The attribute "argsBuilder" on Graphql annotation "@Field" defined on %s must be a string or an array where first index is the builder name and the second is the config.', $target));
386
                    }
387
                }
388
389 8
                if ($accessAnnotation) {
390 1
                    $fieldConfiguration['access'] = self::formatExpression($accessAnnotation->value);
391
                }
392
393 8
                if ($publicAnnotation) {
394 1
                    $fieldConfiguration['public'] = self::formatExpression($publicAnnotation->value);
395
                }
396
            }
397
398 9
            $fields[$propertyName] = $fieldConfiguration;
399
        }
400
401 9
        return $fields;
402
    }
403
404
    /**
405
     * Get the first annotation matching given class.
406
     *
407
     * @param array  $annotations
408
     * @param string $annotationClass
409
     *
410
     * @return mixed
411
     */
412 13
    private static function getFirstAnnotationMatching(array $annotations, $annotationClass)
413
    {
414 13
        foreach ($annotations as $annotation) {
415 13
            if ($annotation instanceof $annotationClass) {
416 13
                return $annotation;
417
            }
418
        }
419
420 10
        return false;
421
    }
422
423
    /**
424
     * Get the config for description & deprecation reason.
425
     *
426
     * @param array $annotations
427
     * @param bool  $withDeprecation
428
     *
429
     * @return array
430
     */
431 13
    private static function getDescriptionConfiguration(array $annotations, bool $withDeprecation = false)
432
    {
433 13
        $config = [];
434 13
        $descriptionAnnotation = self::getFirstAnnotationMatching($annotations, 'Overblog\GraphQLBundle\Annotation\Description');
435 13
        if ($descriptionAnnotation) {
436 6
            $config['description'] = $descriptionAnnotation->value;
437
        }
438
439 13
        if ($withDeprecation) {
440 9
            $deprecatedAnnotation = self::getFirstAnnotationMatching($annotations, 'Overblog\GraphQLBundle\Annotation\Deprecated');
441 9
            if ($deprecatedAnnotation) {
442 1
                $config['deprecationReason'] = $deprecatedAnnotation->value;
443
            }
444
        }
445
446 13
        return $config;
447
    }
448
449
    /**
450
     * Format an expression (ie. add "@=" if not set).
451
     *
452
     * @param string $expression
453
     *
454
     * @return string
455
     */
456 6
    private static function formatExpression(string $expression)
457
    {
458 6
        return '@=' === \substr($expression, 0, 2) ? $expression : \sprintf('@=%s', $expression);
459
    }
460
461
    /**
462
     * Suffix a name if it is not already.
463
     *
464
     * @param string $name
465
     * @param string $suffix
466
     *
467
     * @return string
468
     */
469 2
    private static function suffixName(string $name, string $suffix)
470
    {
471 2
        return \substr($name, \strlen($suffix)) === $suffix ? $name : \sprintf('%s%s', $name, $suffix);
472
    }
473
}
474