Scrutinizer GitHub App not installed

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

Install GitHub App

Passed
Pull Request — annotations (#404)
by Vincent
20:31
created

AnnotationParser::suffixName()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 2.1481

Importance

Changes 0
Metric Value
dl 0
loc 4
c 0
b 0
f 0
ccs 2
cts 3
cp 0.6667
rs 10
cc 2
nc 2
nop 2
crap 2.1481
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 1
    /**
25
     * {@inheritdoc}
26 1
     *
27
     * @throws \ReflectionException
28 1
     * @throws InvalidArgumentException
29
     */
30 1
    public static function parse(\SplFileInfo $file, ContainerBuilder $container): array
31 1
    {
32 1
        $container->addResource(new FileResource($file->getRealPath()));
33
        try {
34
            $fileContent = \file_get_contents($file->getRealPath());
35
36
            $shortClassName = \substr($file->getFilename(), 0, -4);
37 1
            if (\preg_match('#namespace (.+);#', $fileContent, $namespace)) {
38
                $className = $namespace[1].'\\'.$shortClassName;
39 1
            } else {
40 1
                $className = $shortClassName;
41
            }
42 1
43 1
            $reflexionEntity = new \ReflectionClass($className);
44
45 1
            $classAnnotations = self::getAnnotationReader()->getClassAnnotations($reflexionEntity);
46 1
47
            $properties = $reflexionEntity->getProperties();
48 1
49
            $propertiesAnnotations = [];
50 1
            foreach ($properties as $property) {
51
                $propertiesAnnotations[$property->getName()] = self::getAnnotationReader()->getPropertyAnnotations($property);
52
            }
53 1
54
            $methods = $reflexionEntity->getMethods();
55
            $methodsAnnotations = [];
56
            foreach ($methods as $method) {
57
                $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 1
            $gqlTypes = [];
61
62 1
            foreach ($classAnnotations as $classAnnotation) {
63 1
                $method = false;
64 1
                switch (\get_class($classAnnotation)) {
65
                    case 'Overblog\GraphQLBundle\Annotation\Type':
66
                        $gqlTypes += self::getGraphqlType($shortClassName, $classAnnotation, $classAnnotations, $propertiesAnnotations, $methodsAnnotations);
67
                        break;
68
                    case 'Overblog\GraphQLBundle\Annotation\InputType':
69 1
                        $gqlTypes += self::getGraphqlInputType($shortClassName, $classAnnotation, $classAnnotations, $propertiesAnnotations);
70 1
                        break;
71
                    case 'Overblog\GraphQLBundle\Annotation\Scalar':
72 1
                        $gqlTypes += self::getGraphqlScalar($shortClassName, $className, $classAnnotation, $classAnnotations);
73 1
                        break;
74 1
                    case 'Overblog\GraphQLBundle\Annotation\Enum':
75
                        $gqlTypes += self::getGraphqlEnum($shortClassName, $classAnnotation, $classAnnotations, $reflexionEntity->getConstants());
76
                        break;
77 1
                    case 'Overblog\GraphQLBundle\Annotation\Union':
78
                        $gqlTypes += self::getGraphqlUnion($shortClassName, $classAnnotation, $classAnnotations);
79
                        break;
80
                    case 'Overblog\GraphQLBundle\Annotation\TypeInterface':
81
                        $gqlTypes += self::getGraphqlInterface($shortClassName, $classAnnotation, $classAnnotations, $propertiesAnnotations, $methodsAnnotations);
82
                        break;
83
                }
84
85
                if ($method) {
86
                    $gqlTypes += self::$method($shortClassName, $classAnnotation, $propertiesAnnotations);
87 1
                }
88
            }
89 1
90
            return $gqlTypes;
91
        } catch (\InvalidArgumentException $e) {
92
            throw new InvalidArgumentException(\sprintf('Unable to parse file "%s".', $file), $e->getCode(), $e);
93 1
        }
94
    }
95
96
    /**
97
     * Retrieve annotation reader.
98
     *
99
     * @return AnnotationReader
100
     */
101
    private static function getAnnotationReader()
102
    {
103 1
        if (null === self::$annotationReader) {
104
            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
            self::$annotationReader = new AnnotationReader();
111
        }
112
113 1
        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
    private static function getGraphqlType(string $shortClassName, AnnotationType $typeAnnotation, array $classAnnotations, array $propertiesAnnotations, array $methodsAnnotations)
128
    {
129
        $typeName = $typeAnnotation->name ?: $shortClassName;
130
        $typeConfiguration = [];
131
132
        $fields = self::getGraphqlFieldsFromAnnotations($propertiesAnnotations);
133
        $fields += self::getGraphqlFieldsFromAnnotations($methodsAnnotations, false, true);
134
135
        if (empty($fields)) {
136
            return [];
137
        }
138
139
        $typeConfiguration['fields'] = $fields;
140
141
        $publicAnnotation = self::getFirstAnnotationMatching($classAnnotations, 'Overblog\GraphQLBundle\Annotation\IsPublic');
142
        if ($publicAnnotation) {
143
            $typeConfiguration['fieldsDefaultPublic'] = self::formatExpression($publicAnnotation->value);
144
        }
145
146
        $accessAnnotation = self::getFirstAnnotationMatching($classAnnotations, 'Overblog\GraphQLBundle\Annotation\Access');
147
        if ($accessAnnotation) {
148
            $typeConfiguration['fieldsDefaultAccess'] = self::formatExpression($accessAnnotation->value);
149
        }
150
151
        $typeConfiguration += self::getDescriptionConfiguration($classAnnotations);
152
        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
        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
    private static function getGraphqlInterface(string $shortClassName, AnnotationInterface $interfaceAnnotation, array $classAnnotations, array $propertiesAnnotations, array $methodsAnnotations)
169
    {
170
        $interfaceName = $interfaceAnnotation->name ?: $shortClassName;
171
        $interfaceConfiguration = [];
172
173
        $fields = self::getGraphqlFieldsFromAnnotations($propertiesAnnotations);
174
        $fields += self::getGraphqlFieldsFromAnnotations($methodsAnnotations, false, true);
175
176
        if (empty($fields)) {
177
            return [];
178
        }
179
180
        $interfaceConfiguration['fields'] = $fields;
181
        $interfaceConfiguration += self::getDescriptionConfiguration($classAnnotations);
182
183
        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
    private static function getGraphqlInputType(string $shortClassName, AnnotationInputType $inputAnnotation, array $classAnnotations, array $propertiesAnnotations)
196
    {
197
        $inputName = $inputAnnotation->name ?: self::suffixName($shortClassName, 'Input');
198
        $inputConfiguration = [];
199
        $fields = self::getGraphqlFieldsFromAnnotations($propertiesAnnotations, true);
200
201
        if (empty($fields)) {
202
            return [];
203
        }
204
205
        $inputConfiguration['fields'] = $fields;
206
        $inputConfiguration += self::getDescriptionConfiguration($classAnnotations);
207
208
        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
    private static function getGraphqlScalar(string $shortClassName, string $className, AnnotationScalar $scalarAnnotation, array $classAnnotations)
222
    {
223
        $scalarName = $scalarAnnotation->name ?: $shortClassName;
224
        $scalarConfiguration = [];
225
226
        if ($scalarAnnotation->scalarType) {
227
            $scalarConfiguration['scalarType'] = self::formatExpression($scalarAnnotation->scalarType);
228
        } else {
229
            $scalarConfiguration = [
230
                'serialize' => [$className, 'serialize'],
231
                'parseValue' => [$className, 'parseValue'],
232
                'parseLiteral' => [$className, 'parseLiteral'],
233
            ];
234
        }
235
236
        $scalarConfiguration += self::getDescriptionConfiguration($classAnnotations);
237
238
        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
    private static function getGraphqlEnum(string $shortClassName, AnnotationEnum $enumAnnotation, array $classAnnotations, array $constants)
252
    {
253
        $enumName = $enumAnnotation->name ?: self::suffixName($shortClassName, 'Enum');
254
        $enumValues = $enumAnnotation->values ? $enumAnnotation->values : [];
255
256
        $values = [];
257
258 1
        foreach ($constants as $name => $value) {
259
            $valueAnnotation = \current(\array_filter($enumValues, function ($enumValueAnnotation) use ($name) {
260
                return $enumValueAnnotation->name == $name;
261 1
            }));
262 1
            $valueConfig = [];
263
            $valueConfig['value'] = $value;
264 1
265
            if ($valueAnnotation && $valueAnnotation->description) {
266
                $valueConfig['description'] = $valueAnnotation->description;
267
            }
268
269
            if ($valueAnnotation && $valueAnnotation->deprecationReason) {
270 1
                $valueConfig['deprecationReason'] = $valueAnnotation->deprecationReason;
271 1
            }
272 1
273 1
            $values[$name] = $valueConfig;
274
        }
275 1
276
        $enumConfiguration = ['values' => $values];
277
        $enumConfiguration += self::getDescriptionConfiguration($classAnnotations);
278
279 1
        return [$enumName => ['type' => 'enum', 'config' => $enumConfiguration]];
280
    }
281
282
    /**
283 1
     * Get a Graphql Union configuration from given union annotation.
284
     *
285
     * @param string          $shortClassName
286
     * @param AnnotationUnion $unionAnnotation
287 1
     * @param array           $classAnnotations
288
     *
289
     * @return array
290 1
     */
291
    private static function getGraphqlUnion(string $shortClassName, AnnotationUnion $unionAnnotation, array $classAnnotations): array
292 1
    {
293
        $unionName = $unionAnnotation->name ?: $shortClassName;
294
        $unionConfiguration = ['types' => $unionAnnotation->types];
295
        $unionConfiguration += self::getDescriptionConfiguration($classAnnotations);
296
297
        return [$unionName => ['type' => 'union', 'config' => $unionConfiguration]];
298
    }
299
300
    /**
301
     * Create Graphql fields configuration based on annotation.
302
     *
303 1
     * @param array $annotations
304
     * @param bool  $isInput
305 1
     * @param bool  $isMethod
306 1
     *
307
     * @return array
308
     */
309
    private static function getGraphqlFieldsFromAnnotations(array $annotations, bool $isInput = false, bool $isMethod = false): array
310
    {
311
        $fields = [];
312
        foreach ($annotations as $target => $annotations) {
313 1
            $fieldAnnotation = self::getFirstAnnotationMatching($annotations, 'Overblog\GraphQLBundle\Annotation\Field');
314
            $accessAnnotation = self::getFirstAnnotationMatching($annotations, 'Overblog\GraphQLBundle\Annotation\Access');
315
            $publicAnnotation = self::getFirstAnnotationMatching($annotations, 'Overblog\GraphQLBundle\Annotation\IsPublic');
316
317
            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 1
            // Ignore field with resolver when the type is an Input
325
            if ($fieldAnnotation->resolve && $isInput) {
326
                continue;
327 1
            }
328 1
329 1
            $propertyName = $target;
330
            $fieldType = $fieldAnnotation->type;
331
            $fieldConfiguration = [];
332 1
            if ($fieldType) {
333
                $fieldConfiguration['type'] = $fieldType;
334
            }
335
336 1
            $fieldConfiguration += self::getDescriptionConfiguration($annotations, true);
337
338
            if (!$isInput) {
339
                $args = [];
340 1
                if ($fieldAnnotation->args) {
341
                    foreach ($fieldAnnotation->args as $annotationArg) {
342
                        $args[$annotationArg->name] = ['type' => $annotationArg->type] + ($annotationArg->description ? ['description' => $annotationArg->description] : []);
343
                    }
344 1
345
                    if (!empty($args)) {
346
                        $fieldConfiguration['args'] = $args;
347
                    }
348 1
349
                    $args = \array_map(function ($a) {
350
                        return \sprintf("args['%s']", $a);
351
                    }, \array_keys($args));
352 1
                }
353
354
                $propertyName = $fieldAnnotation->name ?: $propertyName;
355
356 1
                if ($fieldAnnotation->resolve) {
357
                    $fieldConfiguration['resolve'] = self::formatExpression($fieldAnnotation->resolve);
358
                } else {
359
                    if ($isMethod) {
360
                        $fieldConfiguration['resolve'] = self::formatExpression(\sprintf("value_resolver([%s], '%s')", \implode(', ', $args), $target));
361 1
                    } elseif ($fieldAnnotation->name) {
362 1
                        $fieldConfiguration['resolve'] = self::formatExpression(\sprintf('value.%s', $target));
363
                    }
364
                }
365
366 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
                    if (\is_string($fieldAnnotation->argsBuilder)) {
368
                        $fieldConfiguration['argsBuilder'] = $fieldAnnotation->argsBuilder;
369
                    } elseif (\is_array($fieldAnnotation->argsBuilder)) {
370
                        list($builder, $builderConfig) = $fieldAnnotation->argsBuilder;
371
                        $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 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
                    if (\is_string($fieldAnnotation->fieldBuilder)) {
379
                        $fieldConfiguration['builder'] = $fieldAnnotation->fieldBuilder;
380
                    } elseif (\is_array($fieldAnnotation->fieldBuilder)) {
381
                        list($builder, $builderConfig) = $fieldAnnotation->fieldBuilder;
382
                        $fieldConfiguration['builder'] = $builder;
383
                        $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
                if ($accessAnnotation) {
390
                    $fieldConfiguration['access'] = self::formatExpression($accessAnnotation->value);
391
                }
392
393
                if ($publicAnnotation) {
394
                    $fieldConfiguration['public'] = self::formatExpression($publicAnnotation->value);
395
                }
396
            }
397
398
            $fields[$propertyName] = $fieldConfiguration;
399
        }
400
401
        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
    private static function getFirstAnnotationMatching(array $annotations, $annotationClass)
413
    {
414
        foreach ($annotations as $annotation) {
415
            if ($annotation instanceof $annotationClass) {
416
                return $annotation;
417
            }
418
        }
419
420 1
        return false;
421
    }
422 1
423
    /**
424
     * Get the config for description & deprecation reason.
425
     *
426 1
     * @param array $annotations
427
     * @param bool  $withDeprecation
428
     *
429 1
     * @return array
430
     */
431
    private static function getDescriptionConfiguration(array $annotations, bool $withDeprecation = false)
432 1
    {
433 1
        $config = [];
434 1
        $descriptionAnnotation = self::getFirstAnnotationMatching($annotations, 'Overblog\GraphQLBundle\Annotation\Description');
435
        if ($descriptionAnnotation) {
436
            $config['description'] = $descriptionAnnotation->value;
437
        }
438
439
        if ($withDeprecation) {
440
            $deprecatedAnnotation = self::getFirstAnnotationMatching($annotations, 'Overblog\GraphQLBundle\Annotation\Deprecated');
441
            if ($deprecatedAnnotation) {
442
                $config['deprecationReason'] = $deprecatedAnnotation->value;
443
            }
444
        }
445
446
        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
    private static function formatExpression(string $expression)
457
    {
458
        return '@=' === \substr($expression, 0, 2) ? $expression : \sprintf('@=%s', $expression);
459
    }
460
461
    /**
462
     * Suffix a name if it is not already.
463 1
     *
464
     * @param string $name
465
     * @param string $suffix
466 1
     *
467
     * @return string
468
     */
469 1
    private static function suffixName(string $name, string $suffix)
470
    {
471 1
        return \substr($name, \strlen($suffix)) === $suffix ? $name : \sprintf('%s%s', $name, $suffix);
472
    }
473
}
474