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 (#407)
by Vincent
22:39
created

AnnotationParser::getGraphqlFieldsFromProvider()   B

Complexity

Conditions 10
Paths 10

Size

Total Lines 38

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 110

Importance

Changes 0
Metric Value
dl 0
loc 38
ccs 0
cts 0
cp 0
rs 7.6666
c 0
b 0
f 0
cc 10
nc 10
nop 3
crap 110

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Overblog\GraphQLBundle\Config\Parser;
6
7
use Doctrine\Common\Annotations\AnnotationReader;
8
use Doctrine\Common\Annotations\AnnotationRegistry;
9
use Overblog\GraphQLBundle\Annotation as GQL;
10
use 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
    /**
24
     * {@inheritdoc}
25
     *
26
     * @throws \ReflectionException
27
     * @throws InvalidArgumentException
28
     */
29
    public static function preParse(\SplFileInfo $file, ContainerBuilder $container, array $configs = []): void
30 13
    {
31
        self::proccessFile($file, $container, $configs, true);
32 13
    }
33
34 13
    /**
35
     * {@inheritdoc}
36 13
     *
37 13
     * @throws \ReflectionException
38 13
     * @throws InvalidArgumentException
39
     */
40
    public static function parse(\SplFileInfo $file, ContainerBuilder $container, array $configs = []): array
41
    {
42
        return self::proccessFile($file, $container, $configs);
43 13
    }
44
45 13
    /**
46
     * Clear the Annotation parser.
47 13
     */
48
    public static function clear(): void
49 13
    {
50 13
        self::$classesMap = [];
51 8
        self::$providers = [];
52
        self::$annotationReader = null;
53
    }
54 13
55 13
    /**
56 13
     * Process a file.
57 3
     *
58
     * @param \SplFileInfo     $file
59
     * @param ContainerBuilder $container
60 13
     * @param bool             $resolveClassMap
61
     *
62 13
     * @throws \ReflectionException
63 13
     * @throws InvalidArgumentException
64 13
     */
65 13
    public static function proccessFile(\SplFileInfo $file, ContainerBuilder $container, array $configs, bool $resolveClassMap = false): array
66 7
    {
67 7
        self::$doctrineMapping = $configs['doctrine']['types_mapping'];
68 9
69 1
        $rootQueryType = $configs['definitions']['schema']['default']['query'] ?? false;
70 1
        $rootMutationType = $configs['definitions']['schema']['default']['mutation'] ?? false;
71 9
72 2
        $container->addResource(new FileResource($file->getRealPath()));
73 2
74 8
        if (!$resolveClassMap) {
75 1
            $container->setParameter(self::CLASSESMAP_CONTAINER_PARAMETER, self::$classesMap);
76 1
        }
77 8
78 1
        try {
79 1
            $fileContent = \file_get_contents($file->getRealPath());
80 8
81 1
            $shortClassName = \substr($file->getFilename(), 0, -4);
82 1
            if (\preg_match('#namespace (.+);#', $fileContent, $namespace)) {
83
                $className = $namespace[1].'\\'.$shortClassName;
84
                $namespace = $namespace[1];
85 13
            } else {
86 13
                $className = $shortClassName;
87
            }
88
89
            $reflexionEntity = new \ReflectionClass($className);
90 13
91
            $classAnnotations = self::getAnnotationReader()->getClassAnnotations($reflexionEntity);
92
93
            $properties = [];
94
            foreach ($reflexionEntity->getProperties() as $property) {
95
                $properties[$property->getName()] = ['property' => $property, 'annotations' => self::getAnnotationReader()->getPropertyAnnotations($property)];
96
            }
97
98
            $methods = [];
99
            foreach ($reflexionEntity->getMethods() as $method) {
100
                $methods[$method->getName()] = ['method' => $method, 'annotations' => 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...
101 13
            }
102
103 13
            $gqlTypes = [];
104 1
105 1
            foreach ($classAnnotations as $classAnnotation) {
106
                $gqlConfiguration = $gqlType = $gqlName = false;
107
108
                switch (true) {
109 1
                    case $classAnnotation instanceof GQL\Type:
110 1
                        $gqlType = 'type';
111
                        $gqlName = $classAnnotation->name ?: $shortClassName;
112
                        if (!$resolveClassMap) {
113 13
                            $isRootQuery = ($rootQueryType && $gqlName === $rootQueryType);
114
                            $isRootMutation = ($rootMutationType && $gqlName === $rootMutationType);
115
                            $currentValue = ($isRootQuery || $isRootMutation) ? \sprintf("service('%s')", self::formatNamespaceForExpression($className)) : 'value';
116
117
                            $gqlConfiguration = self::getGraphqlType($classAnnotation, $classAnnotations, $properties, $methods, $namespace, $currentValue);
0 ignored issues
show
Bug introduced by
It seems like $namespace can also be of type array<integer,string>; however, Overblog\GraphQLBundle\C...arser::getGraphqlType() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
118
119
                            if ($isRootQuery || $isRootMutation) {
120
                                foreach (self::$providers as $className => $providerMethods) {
121
                                    $gqlConfiguration['config']['fields'] += self::getGraphqlFieldsFromProvider($className, $providerMethods, $isRootMutation);
122
                                }
123
                            }
124
                        }
125
                        break;
126
                    case $classAnnotation instanceof GQL\Input:
127 7
                        $gqlType = 'input';
128
                        $gqlName = $classAnnotation->name ?: self::suffixName($shortClassName, 'Input');
129 7
                        if (!$resolveClassMap) {
130 7
                            $gqlConfiguration = self::getGraphqlInput($classAnnotation, $classAnnotations, $properties, $namespace);
0 ignored issues
show
Bug introduced by
It seems like $namespace can also be of type array<integer,string>; however, Overblog\GraphQLBundle\C...rser::getGraphqlInput() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
131
                        }
132 7
                        break;
133 7
                    case $classAnnotation instanceof GQL\Scalar:
134
                        $gqlType = 'scalar';
135 7
                        if (!$resolveClassMap) {
136
                            $gqlConfiguration = self::getGraphqlScalar($className, $classAnnotation, $classAnnotations);
137
                        }
138
                        break;
139 7
                    case $classAnnotation instanceof GQL\Enum:
140
                        $gqlType = 'enum';
141 7
                        if (!$resolveClassMap) {
142 7
                            $gqlConfiguration = self::getGraphqlEnum($classAnnotation, $classAnnotations, $reflexionEntity->getConstants());
143 1
                        }
144
                        break;
145
                    case $classAnnotation instanceof GQL\Union:
146 7
                        $gqlType = 'union';
147 7
                        if (!$resolveClassMap) {
148 1
                            $gqlConfiguration = self::getGraphqlUnion($classAnnotation, $classAnnotations);
149
                        }
150
                        break;
151 7
                    case $classAnnotation instanceof GQL\TypeInterface:
152 7
                        $gqlType = 'interface';
153
                        if (!$resolveClassMap) {
154
                            $gqlConfiguration = self::getGraphqlInterface($classAnnotation, $classAnnotations, $properties, $methods, $namespace);
0 ignored issues
show
Bug introduced by
It seems like $namespace can also be of type array<integer,string>; however, Overblog\GraphQLBundle\C...::getGraphqlInterface() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
155
                        }
156 7
                        break;
157
                    case $classAnnotation instanceof GQL\Provider:
158
                        if ($resolveClassMap) {
159
                            self::$providers[$className] = $methods;
160
                        }
161
                        break;
162
                    default:
163
                        continue;
164
                }
165
166
                if ($gqlType) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $gqlType of type string|false is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
167
                    if (!$gqlName) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $gqlName of type string|false is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
168 1
                        $gqlName = $classAnnotation->name ?: $shortClassName;
169
                    }
170 1
171 1
                    if ($resolveClassMap) {
172
                        if (isset(self::$classesMap[$gqlName])) {
173 1
                            throw new InvalidArgumentException(\sprintf('The GraphQL type "%s" has already been registered in class "%s"', $gqlName, self::$classesMap[$gqlName]['class']));
174 1
                        }
175
                        self::$classesMap[$gqlName] = ['type' => $gqlType, 'class' => $className];
176 1
                    } else {
177
                        $gqlTypes += [$gqlName => $gqlConfiguration];
178
                    }
179
                }
180 1
            }
181 1
182
            return $resolveClassMap ? self::$classesMap : $gqlTypes;
183 1
        } catch (\InvalidArgumentException $e) {
184
            throw new InvalidArgumentException(\sprintf('Failed to parse GraphQL annotations from file "%s".', $file), $e->getCode(), $e);
185
        }
186
    }
187
188
    /**
189
     * Retrieve annotation reader.
190
     *
191
     * @return AnnotationReader
192
     */
193
    private static function getAnnotationReader()
194
    {
195 1
        if (null === self::$annotationReader) {
196
            if (!\class_exists('\\Doctrine\\Common\\Annotations\\AnnotationReader') ||
197 1
                !\class_exists('\\Doctrine\\Common\\Annotations\\AnnotationRegistry')) {
198 1
                throw new \Exception('In order to use graphql annotation, you need to require doctrine annotations');
199 1
            }
200
201 1
            AnnotationRegistry::registerLoader('class_exists');
202
            self::$annotationReader = new AnnotationReader();
203
        }
204
205 1
        return self::$annotationReader;
206 1
    }
207
208 1
    /**
209
     * Create a GraphQL Type configuration from annotations on class, properties and methods.
210
     *
211
     * @param GQL\Type $typeAnnotation
212
     * @param array    $classAnnotations
213
     * @param array    $properties
214
     * @param array    $methods
215
     * @param string   $namespace
216
     * @param string   $currentValue
217
     *
218
     * @return array
219
     */
220
    private static function getGraphqlType(GQL\Type $typeAnnotation, array $classAnnotations, array $properties, array $methods, string $namespace, string $currentValue)
221 2
    {
222
        $typeConfiguration = [];
223 2
224 2
        $fields = self::getGraphqlFieldsFromAnnotations($namespace, $properties, false, false, $currentValue);
225
        $fields += self::getGraphqlFieldsFromAnnotations($namespace, $methods, false, true, $currentValue);
226 2
227 1
        $typeConfiguration['fields'] = $fields;
228
        $typeConfiguration += self::getDescriptionConfiguration($classAnnotations);
229
230 1
        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...
231 1
            $typeConfiguration['interfaces'] = $typeAnnotation->interfaces;
232 1
        }
233
234
        if ($typeAnnotation->resolveField) {
235
            $typeConfiguration['resolveField'] = self::formatExpression($typeAnnotation->resolveField);
236 2
        }
237
238 2
        $publicAnnotation = self::getFirstAnnotationMatching($classAnnotations, 'Overblog\GraphQLBundle\Annotation\IsPublic');
239
        if ($publicAnnotation) {
240
            $typeConfiguration['fieldsDefaultPublic'] = self::formatExpression($publicAnnotation->value);
241
        }
242
243
        $accessAnnotation = self::getFirstAnnotationMatching($classAnnotations, 'Overblog\GraphQLBundle\Annotation\Access');
244
        if ($accessAnnotation) {
245
            $typeConfiguration['fieldsDefaultAccess'] = self::formatExpression($accessAnnotation->value);
246
        }
247
248
        return ['type' => $typeAnnotation->isRelay ? 'relay-mutation-payload' : 'object', 'config' => $typeConfiguration];
249
    }
250
251 1
    /**
252
     * Create a GraphQL Interface type configuration from annotations on properties.
253 1
     *
254 1
     * @param string        $shortClassName
0 ignored issues
show
Bug introduced by
There is no parameter named $shortClassName. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
255
     * @param GQL\Interface $interfaceAnnotation
256 1
     * @param array         $properties
257
     * @param array         $methods
258 1
     * @param string        $namespace
259
     *
260 1
     * @return array
261 1
     */
262 1
    private static function getGraphqlInterface(GQL\TypeInterface $interfaceAnnotation, array $classAnnotations, array $properties, array $methods, string $namespace)
263 1
    {
264
        $interfaceConfiguration = [];
265 1
266 1
        $fields = self::getGraphqlFieldsFromAnnotations($namespace, $properties);
267
        $fields += self::getGraphqlFieldsFromAnnotations($namespace, $methods, false, true);
268
269 1
        $interfaceConfiguration['fields'] = $fields;
270
        $interfaceConfiguration += self::getDescriptionConfiguration($classAnnotations);
271
272
        $interfaceConfiguration['resolveType'] = $interfaceAnnotation->resolveType;
273 1
274
        return ['type' => 'interface', 'config' => $interfaceConfiguration];
275
    }
276 1
277 1
    /**
278
     * Create a GraphQL Input type configuration from annotations on properties.
279 1
     *
280
     * @param string    $shortClassName
0 ignored issues
show
Bug introduced by
There is no parameter named $shortClassName. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
281
     * @param GQL\Input $inputAnnotation
282
     * @param array     $properties
283
     * @param string    $namespace
284
     *
285
     * @return array
286
     */
287
    private static function getGraphqlInput(GQL\Input $inputAnnotation, array $classAnnotations, array $properties, string $namespace)
288
    {
289
        $inputConfiguration = [];
290
        $fields = self::getGraphqlFieldsFromAnnotations($namespace, $properties, true);
291 1
292
        $inputConfiguration['fields'] = $fields;
293 1
        $inputConfiguration += self::getDescriptionConfiguration($classAnnotations);
294 1
295 1
        return ['type' => $inputAnnotation->isRelay ? 'relay-mutation-input' : 'input-object', 'config' => $inputConfiguration];
296
    }
297 1
298
    /**
299
     * Get a Graphql scalar configuration from given scalar annotation.
300
     *
301
     * @param string     $shortClassName
0 ignored issues
show
Documentation introduced by
There is no parameter named $shortClassName. Did you maybe mean $className?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
302
     * @param string     $className
303
     * @param GQL\Scalar $scalarAnnotation
304
     * @param array      $classAnnotations
305
     *
306
     * @return array
307
     */
308
    private static function getGraphqlScalar(string $className, GQL\Scalar $scalarAnnotation, array $classAnnotations)
309 9
    {
310
        $scalarConfiguration = [];
311 9
312 9
        if ($scalarAnnotation->scalarType) {
313 9
            $scalarConfiguration['scalarType'] = self::formatExpression($scalarAnnotation->scalarType);
314 9
        } else {
315 9
            $scalarConfiguration = [
316
                'serialize' => [$className, 'serialize'],
317 9
                'parseValue' => [$className, 'parseValue'],
318
                'parseLiteral' => [$className, 'parseLiteral'],
319
            ];
320
        }
321
322
        $scalarConfiguration += self::getDescriptionConfiguration($classAnnotations);
323
324
        return ['type' => 'custom-scalar', 'config' => $scalarConfiguration];
325 9
    }
326
327
    /**
328
     * Get a Graphql Enum configuration from given enum annotation.
329 9
     *
330 9
     * @param string   $shortClassName
0 ignored issues
show
Bug introduced by
There is no parameter named $shortClassName. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
331 9
     * @param GQL\Enum $enumAnnotation
332 9
     * @param array    $classAnnotations
333 7
     * @param array    $constants
334
     *
335
     * @return array
336 9
     */
337
    private static function getGraphqlEnum(GQL\Enum $enumAnnotation, array $classAnnotations, array $constants)
338 9
    {
339 8
        $enumValues = $enumAnnotation->values ? $enumAnnotation->values : [];
340 8
341 1
        $values = [];
342 1
343
        foreach ($constants as $name => $value) {
344
            $valueAnnotation = \current(\array_filter($enumValues, function ($enumValueAnnotation) use ($name) {
345 1
                return $enumValueAnnotation->name == $name;
346 1
            }));
347
            $valueConfig = [];
348
            $valueConfig['value'] = $value;
349
350 1
            if ($valueAnnotation && $valueAnnotation->description) {
351 1
                $valueConfig['description'] = $valueAnnotation->description;
352
            }
353
354 8
            if ($valueAnnotation && $valueAnnotation->deprecationReason) {
355
                $valueConfig['deprecationReason'] = $valueAnnotation->deprecationReason;
356 8
            }
357 1
358
            $values[$name] = $valueConfig;
359 8
        }
360 2
361 7
        $enumConfiguration = ['values' => $values];
362
        $enumConfiguration += self::getDescriptionConfiguration($classAnnotations);
363
364
        return ['type' => 'enum', 'config' => $enumConfiguration];
365
    }
366 8
367 1
    /**
368 1
     * Get a Graphql Union configuration from given union annotation.
369 1
     *
370 1
     * @param string    $shortClassName
0 ignored issues
show
Bug introduced by
There is no parameter named $shortClassName. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
371 1
     * @param GQL\Union $unionAnnotation
372
     * @param array     $classAnnotations
373
     *
374
     * @return array
375
     */
376
    private static function getGraphqlUnion(GQL\Union $unionAnnotation, array $classAnnotations): array
377 8
    {
378 2
        $unionConfiguration = ['types' => $unionAnnotation->types];
379 2
        $unionConfiguration += self::getDescriptionConfiguration($classAnnotations);
380 2
381 2
        return ['type' => 'union', 'config' => $unionConfiguration];
382 2
    }
383 2
384
    /**
385
     * Create Graphql fields configuration based on annotation.
386
     *
387
     * @param string $namespace
388
     * @param array  $propertiesOrMethods
389 8
     * @param bool   $isInput
390 1
     * @param bool   $isMethod
391
     * @param string $currentValue
392
     *
393 8
     * @return array
394 1
     */
395
    private static function getGraphqlFieldsFromAnnotations(string $namespace, array $propertiesOrMethods, bool $isInput = false, bool $isMethod = false, string $currentValue = 'value'): array
396
    {
397
        $fields = [];
398 9
        foreach ($propertiesOrMethods as $target => $config) {
399
            $annotations = $config['annotations'];
400
            $method = $isMethod ? $config['method'] : false;
401 9
            $property = $isMethod ? false : $config['property'];
0 ignored issues
show
Unused Code introduced by
$property is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
402
403
            $fieldAnnotation = self::getFirstAnnotationMatching($annotations, 'Overblog\GraphQLBundle\Annotation\Field');
404
            $accessAnnotation = self::getFirstAnnotationMatching($annotations, 'Overblog\GraphQLBundle\Annotation\Access');
405
            $publicAnnotation = self::getFirstAnnotationMatching($annotations, 'Overblog\GraphQLBundle\Annotation\IsPublic');
406
407
            if (!$fieldAnnotation) {
408
                if ($accessAnnotation || $publicAnnotation) {
409
                    throw new InvalidArgumentException(\sprintf('The annotations "@Access" and/or "@Visible" defined on "%s" are only usable in addition of annotation "@Field"', $target));
410
                }
411
                continue;
412 13
            }
413
414 13
            if ($isMethod && !$method->isPublic()) {
415 13
                throw new InvalidArgumentException(\sprintf('The Annotation "@Field" can only be applied to public method. The method "%s" is not public.', $target));
416 13
            }
417
418
            // Ignore field with resolver when the type is an Input
419
            if ($fieldAnnotation->resolve && $isInput) {
420 10
                continue;
421
            }
422
423
            $propertyName = $target;
424
            $fieldType = $fieldAnnotation->type;
425
            $fieldConfiguration = [];
426
            if ($fieldType) {
427
                $resolvedType = self::resolveClassFromType($fieldType);
428
                if ($resolvedType) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $resolvedType of type string|false is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
429
                    if ($isInput && !\in_array($resolvedType['type'], ['input', 'scalar', 'enum'])) {
430
                        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']));
431 13
                    }
432
                }
433 13
434 13
                $fieldConfiguration['type'] = $fieldType;
435 13
            }
436 6
437
            $fieldConfiguration += self::getDescriptionConfiguration($annotations, true);
438
439 13
            if (!$isInput) {
440 9
                $args = [];
0 ignored issues
show
Unused Code introduced by
$args is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
441 9
                $args = self::getArgs($fieldAnnotation->args, $isMethod && !$fieldAnnotation->argsBuilder ? $method : null);
442 1
443
                if (!empty($args)) {
444
                    $fieldConfiguration['args'] = $args;
445
                }
446 13
447
                $propertyName = $fieldAnnotation->name ?: $propertyName;
448
449
                if ($fieldAnnotation->resolve) {
450
                    $fieldConfiguration['resolve'] = self::formatExpression($fieldAnnotation->resolve);
451
                } else {
452
                    if ($isMethod) {
453
                        $fieldConfiguration['resolve'] = self::formatExpression(\sprintf('@=call(%s.%s, %s)', $currentValue, $target, self::formatArgsForExpression($args)));
454
                    } elseif ($fieldAnnotation->name) {
455
                        $fieldConfiguration['resolve'] = self::formatExpression(\sprintf('@=call(%s.%s, [])', $currentValue, $target));
456 6
                    }
457
                }
458 6
459
                if ($fieldAnnotation->argsBuilder) {
460
                    if (\is_string($fieldAnnotation->argsBuilder)) {
461
                        $fieldConfiguration['argsBuilder'] = $fieldAnnotation->argsBuilder;
462
                    } elseif (\is_array($fieldAnnotation->argsBuilder)) {
463
                        list($builder, $builderConfig) = $fieldAnnotation->argsBuilder;
464
                        $fieldConfiguration['argsBuilder'] = ['builder' => $builder, 'config' => $builderConfig];
465
                    } else {
466
                        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));
467
                    }
468
                }
469 2
470
                if ($fieldAnnotation->fieldBuilder) {
471 2
                    if (\is_string($fieldAnnotation->fieldBuilder)) {
472
                        $fieldConfiguration['builder'] = $fieldAnnotation->fieldBuilder;
473
                    } elseif (\is_array($fieldAnnotation->fieldBuilder)) {
474
                        list($builder, $builderConfig) = $fieldAnnotation->fieldBuilder;
475
                        $fieldConfiguration['builder'] = $builder;
476
                        $fieldConfiguration['builderConfig'] = $builderConfig ?: [];
477
                    } else {
478
                        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));
479
                    }
480
                } else {
481
                    if (!$fieldType) {
482
                        if ($isMethod) {
483
                            if ($method->hasReturnType()) {
484
                                try {
485
                                    $fieldConfiguration['type'] = self::resolveGraphqlTypeFromReflectionType($method->getReturnType(), 'type').'!';
486
                                } catch (\Exception $e) {
487
                                    throw new InvalidArgumentException(\sprintf('The attribute "type" on GraphQL annotation "@Field" is missing on method "%s" and cannot be auto-guessed from type hint "%s"', $target, (string) $method->getReturnType()));
488
                                }
489
                            } else {
490
                                throw new InvalidArgumentException(\sprintf('The attribute "type" on GraphQL annotation "@Field" is missing on method "%s" and cannot be auto-guessed as there is not return type hint.', $target));
491
                            }
492
                        } else {
493
                            try {
494
                                $fieldConfiguration['type'] = self::guessType($namespace, $annotations);
495
                            } catch (\Exception $e) {
496
                                throw new InvalidArgumentException(\sprintf('The attribute "type" on "@Field" defined on "%s" is required and cannot be auto-guessed : %s.', $target, $e->getMessage()));
497
                            }
498
                        }
499
                    }
500
                }
501
502
                if ($accessAnnotation) {
503
                    $fieldConfiguration['access'] = self::formatExpression($accessAnnotation->value);
504
                }
505
506
                if ($publicAnnotation) {
507
                    $fieldConfiguration['public'] = self::formatExpression($publicAnnotation->value);
508
                }
509
            }
510
511
            $fields[$propertyName] = $fieldConfiguration;
512
        }
513
514
        return $fields;
515
    }
516
517
    /**
518
     * ArgTransformer
519
     *   Transform Arg type hint with enum as newObject(enumClassTypeHint, arg['a'])new EnumClass(arg['a'])
520
     *   Transform Arg type hint input as populate(InputClass, arg['a']).
521
     */
522
523
    /**
524
     * Return fields config from Provider methods.
525
     *
526
     * @param string $className
527
     * @param array  $methods
528
     * @param bool   $isMutation
529
     *
530
     * @return array
531
     */
532
    private static function getGraphqlFieldsFromProvider(string $className, array $methods, bool $isMutation = false)
533
    {
534
        $fields = [];
535
        foreach ($methods as $methodName => $config) {
536
            $annotations = $config['annotations'];
537
            $method = $config['method'];
538
539
            $annotation = self::getFirstAnnotationMatching($annotations, \sprintf('Overblog\\GraphQLBundle\\Annotation\\%s', $isMutation ? 'Mutation' : 'Query'));
540
            if (!$annotation) {
541
                continue;
542
            }
543
544
            $name = $annotation->name ?: $methodName;
545
            $type = $annotation->type;
546
            $args = self::getArgs($annotation->args, $method);
547
            if (!$type) {
548
                if ($method->hasReturnType()) {
549
                    try {
550
                        $type = self::resolveGraphqlTypeFromReflectionType($method->getReturnType(), 'type');
551
                    } catch (\Exception $e) {
552
                        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"', $isMutation ? 'Mutation' : 'Query', $method, (string) $method->getReturnType()));
553
                    }
554
                } else {
555
                    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.', $isMutation ? 'Mutation' : 'Query', $method));
556
                }
557
            }
558
559
            $resolve = \sprintf("@=call(service('%s').%s, %s)", self::formatNamespaceForExpression($className), $methodName, self::formatArgsForExpression($args));
560
561
            $fields[$name] = [
562
                'type' => $type,
563
                'args' => $args,
564
                'resolve' => $resolve,
565
            ];
566
        }
567
568
        return $fields;
569
    }
570
571
    /**
572
     * Get the config for description & deprecation reason.
573
     *
574
     * @param array $annotations
575
     * @param bool  $withDeprecation
576
     *
577
     * @return array
578
     */
579
    private static function getDescriptionConfiguration(array $annotations, bool $withDeprecation = false)
580
    {
581
        $config = [];
582
        $descriptionAnnotation = self::getFirstAnnotationMatching($annotations, 'Overblog\GraphQLBundle\Annotation\Description');
583
        if ($descriptionAnnotation) {
584
            $config['description'] = $descriptionAnnotation->value;
585
        }
586
587
        if ($withDeprecation) {
588
            $deprecatedAnnotation = self::getFirstAnnotationMatching($annotations, 'Overblog\GraphQLBundle\Annotation\Deprecated');
589
            if ($deprecatedAnnotation) {
590
                $config['deprecationReason'] = $deprecatedAnnotation->value;
591
            }
592
        }
593
594
        return $config;
595
    }
596
597
    /**
598
     * Get args config from an array of @Arg annotation or by auto-guessing if a method is provided.
599
     *
600
     * @param array             $args
601
     * @param \ReflectionMethod $method
602
     *
603
     * @return array
604
     */
605
    private static function getArgs(array $args = null, \ReflectionMethod $method = null)
606
    {
607
        $config = [];
608
        if ($args && !empty($args)) {
609
            foreach ($args as $arg) {
610
                $config[$arg->name] = ['type' => $arg->type] + ($arg->description ? ['description' => $arg->description] : []);
611
            }
612
        } elseif ($method) {
613
            $config = self::guessArgs($method);
614
        }
615
616
        return $config;
617
    }
618
619
    private static function formatArgsForExpression(array $args)
620
    {
621
        $mapping = [];
622
        foreach ($args as $name => $config) {
623
            $mapping[] = \sprintf('%s: "%s"', $name, $config['type']);
624
        }
625
626
        return \sprintf('arguments({%s}, args)', \implode(', ', $mapping));
627
    }
628
629
    /**
630
     * Format an array of args to a list of arguments in an expression.
631
     *
632
     * @param array $args
633
     *
634
     * @return string
635
     */
636
    /*
637
    private static function formatArgsForExpression(array $args)
638
    {
639
        $resolvedArgs = [];
640
        foreach ($args as $name => $config) {
641
            $cleanedType = \str_replace(['[', ']', '!'], '', $config['type']);
642
            $definition = self::resolveClassFromType($cleanedType);
643
            $defaultFormat = \sprintf("args['%s']", $name);
644
            if (!$definition) {
645
                $resolvedArgs[] = $defaultFormat;
646
            } else {
647
                switch ($definition['type']) {
648
                    case 'input':
649
                    case 'enum':
650
                        $resolvedArgs[] = \sprintf("input('%s', args['%s'], '%s')", $config['type'], $name, $name);
651
                        break;
652
                    default:
653
                        $resolvedArgs[] = $defaultFormat;
654
                        break;
655
                }
656
            }
657
        }
658
659
        return sprintf("inputs(%s)", \implode(', ', $resolvedArgs));
660
    }
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
    private static function formatNamespaceForExpression(string $namespace)
671
    {
672
        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
    private static function getFirstAnnotationMatching(array $annotations, $annotationClass)
684
    {
685
        if (\is_string($annotationClass)) {
686
            $annotationClass = [$annotationClass];
687
        }
688
689
        foreach ($annotations as $annotation) {
690
            foreach ($annotationClass as $class) {
691
                if ($annotation instanceof $class) {
692
                    return $annotation;
693
                }
694
            }
695
        }
696
697
        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
    private static function formatExpression(string $expression)
708
    {
709
        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
    private static function suffixName(string $name, string $suffix)
721
    {
722
        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
    private static function guessType(string $namespace, array $annotations)
734
    {
735
        $columnAnnotation = self::getFirstAnnotationMatching($annotations, 'Doctrine\ORM\Mapping\Column');
736
        if ($columnAnnotation) {
737
            $type = self::resolveTypeFromDoctrineType($columnAnnotation->type);
738
            $nullable = $columnAnnotation->nullable;
739
            if ($type) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $type of type string|false is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
740
                return $nullable ? $type : \sprintf('%s!', $type);
741
            } else {
742
                throw new \Exception(\sprintf('Unable to auto-guess GraphQL type from Doctrine type "%s"', $columnAnnotation->type));
743
            }
744
        }
745
746
        $associationAnnotations = [
747
            '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
        $associationAnnotation = self::getFirstAnnotationMatching($annotations, \array_keys($associationAnnotations));
754
        if ($associationAnnotation) {
755
            $target = self::fullyQualifiedClassName($associationAnnotation->targetEntity, $namespace);
756
            $type = self::resolveTypeFromClass($target, 'type');
757
758
            if ($type) {
759
                $isMultiple = $associationAnnotations[\get_class($associationAnnotation)];
760
                if ($isMultiple) {
761
                    return \sprintf('[%s]!', $type);
762
                } else {
763
                    $isNullable = false;
764
                    $joinColumn = self::getFirstAnnotationMatching($annotations, 'Doctrine\ORM\Mapping\JoinColumn');
765
                    if ($joinColumn) {
766
                        $isNullable = $joinColumn->nullable;
767
                    }
768
769
                    return \sprintf('%s%s', $type, $isNullable ? '' : '!');
770
                }
771
            } else {
772
                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
    public static function fullyQualifiedClassName(string $className, string $namespace)
788
    {
789
        if (false === \strpos($className, '\\') && $namespace) {
790
            return $namespace.'\\'.$className;
791
        }
792
793
        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
    private static function resolveTypeFromDoctrineType(string $doctrineType)
804
    {
805
        if (isset(self::$doctrineMapping[$doctrineType])) {
806
            return self::$doctrineMapping[$doctrineType];
807
        }
808
809
        switch ($doctrineType) {
810
            case 'integer':
811
            case 'smallint':
812
            case 'bigint':
813
                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
            case 'string':
816
            case 'text':
817
                return 'String';
818
                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...
819
            case 'bool':
820
            case 'boolean':
821
                return 'Boolean';
822
                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...
823
            case 'float':
824
            case 'decimal':
825
                return 'Float';
826
                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...
827
            default:
828
                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
    private static function guessArgs(\ReflectionMethod $method)
840
    {
841
        $arguments = [];
842
        foreach ($method->getParameters() as $index => $parameter) {
843
            if (!$parameter->hasType()) {
844
                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()));
0 ignored issues
show
Bug introduced by
Consider using $parameter->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
845
            }
846
847
            try {
848
                $gqlType = self::resolveGraphqlTypeFromReflectionType($parameter->getType(), 'input');
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()));
0 ignored issues
show
Bug introduced by
Consider using $parameter->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
851
            }
852
853
            $argumentConfig = [];
854
            if ($parameter->isDefaultValueAvailable()) {
855
                $argumentConfig['defaultValue'] = $parameter->getDefaultValue();
856
            } else {
857
                $gqlType .= '!';
858
            }
859
860
            $argumentConfig['type'] = $gqlType;
861
862
            $arguments[$parameter->getName()] = $argumentConfig;
0 ignored issues
show
Bug introduced by
Consider using $parameter->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
863
        }
864
865
        return $arguments;
866
    }
867
868
    /**
869
     * Try to guess a GraphQL type from a Reflected Type.
870
     *
871
     * @param \ReflectionType $type
872
     *
873
     * @return string
874
     */
875
    private static function resolveGraphqlTypeFromReflectionType(\ReflectionType $type, string $filterGraphqlType = null)
876
    {
877
        $stype = (string) $type;
878
        if ($type->isBuiltin()) {
879
            $gqlType = self::resolveTypeFromPhpType($stype);
880
            if (!$gqlType) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $gqlType of type string|false is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
881
                throw new \Exception(\sprintf('No corresponding GraphQL type found for builtin type "%s"', $stype));
882
            }
883
        } else {
884
            $gqlType = self::resolveTypeFromClass($stype, $filterGraphqlType);
885
            if (!$gqlType) {
886
                throw new \Exception(\sprintf('No corresponding GraphQL %s found for class "%s"', $filterGraphqlType ?: 'object', $stype));
887
            }
888
        }
889
890
        return $gqlType;
891
    }
892
893
    /**
894
     * Resolve a GraphQL Type from a class name.
895
     *
896
     * @param string $className
897
     * @param string $wantedType
898
     *
899
     * @return string|false
900
     */
901
    private static function resolveTypeFromClass(string $className, string $wantedType = null)
902
    {
903
        foreach (self::$classesMap as $gqlType => $config) {
904
            if ($config['class'] === $className) {
905
                if (!$wantedType || ($wantedType && $wantedType === $config['type'])) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $wantedType of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
906
                    return $gqlType;
907
                }
908
            }
909
        }
910
911
        return false;
912
    }
913
914
    /**
915
     * Resolve a PHP class from a GraphQL type.
916
     *
917
     * @param string $type
918
     *
919
     * @return string|false
920
     */
921
    private static function resolveClassFromType(string $type)
922
    {
923
        return self::$classesMap[$type] ?? false;
924
    }
925
926
    /**
927
     * Convert a PHP Builtin type to a GraphQL type.
928
     *
929
     * @param string $phpType
930
     *
931
     * @return string
932
     */
933
    private static function resolveTypeFromPhpType(string $phpType)
934
    {
935
        switch ($phpType) {
936
            case 'boolean':
937
            case 'bool':
938
                return 'Boolean';
939
            case 'integer':
940
            case 'int':
941
                return 'Int';
942
            case 'float':
943
            case 'double':
944
                return 'Float';
945
            case 'string':
946
                return 'String';
947
            default:
948
                return false;
949
        }
950
    }
951
}
952