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 — master (#695)
by Timur
21:55
created

AnnotationParser::getArgs()   A

Complexity

Conditions 6
Paths 3

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 6

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 9
dl 0
loc 14
ccs 5
cts 5
cp 1
rs 9.2222
c 1
b 0
f 0
cc 6
nc 3
nop 2
crap 6
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Overblog\GraphQLBundle\Config\Parser;
6
7
use Doctrine\Common\Annotations\AnnotationReader;
8
use Doctrine\Common\Annotations\AnnotationRegistry;
9
use Doctrine\ORM\Mapping\Column;
10
use Doctrine\ORM\Mapping\JoinColumn;
11
use Doctrine\ORM\Mapping\ManyToMany;
12
use Doctrine\ORM\Mapping\ManyToOne;
13
use Doctrine\ORM\Mapping\OneToMany;
14
use Doctrine\ORM\Mapping\OneToOne;
15
use Exception;
16
use Overblog\GraphQLBundle\Annotation as GQL;
17
use Overblog\GraphQLBundle\Relay\Connection\ConnectionInterface;
18
use Overblog\GraphQLBundle\Relay\Connection\EdgeInterface;
19
use ReflectionClass;
20
use ReflectionException;
21
use ReflectionMethod;
22
use ReflectionNamedType;
23
use RuntimeException;
24
use SplFileInfo;
25
use Symfony\Component\Config\Resource\FileResource;
26
use Symfony\Component\DependencyInjection\ContainerBuilder;
27
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
28
use function array_filter;
29
use function array_keys;
30
use function array_map;
31
use function array_unshift;
32
use function class_exists;
33
use function current;
34
use function file_get_contents;
35
use function get_class;
36
use function implode;
37
use function in_array;
38
use function is_array;
39
use function is_string;
40
use function preg_match;
41
use function sprintf;
42
use function str_replace;
43
use function strlen;
44
use function strpos;
45
use function substr;
46
use function trim;
47
48
class AnnotationParser implements PreParserInterface
49 20
{
50
    private static ?AnnotationReader $annotationReader = null;
51 20
    private static array $classesMap = [];
52 20
    private static array $providers = [];
53
    private static array $doctrineMapping = [];
54
    private static array $classAnnotationsCache = [];
55
56
    private const GQL_SCALAR = 'scalar';
57
    private const GQL_ENUM = 'enum';
58
    private const GQL_TYPE = 'type';
59
    private const GQL_INPUT = 'input';
60 20
    private const GQL_UNION = 'union';
61
    private const GQL_INTERFACE = 'interface';
62 20
63
    /**
64
     * @see https://facebook.github.io/graphql/draft/#sec-Input-and-Output-Types
65
     */
66
    private const VALID_INPUT_TYPES = [self::GQL_SCALAR, self::GQL_ENUM, self::GQL_INPUT];
67
    private const VALID_OUTPUT_TYPES = [self::GQL_SCALAR, self::GQL_TYPE, self::GQL_INTERFACE, self::GQL_UNION, self::GQL_ENUM];
68 58
69
    /**
70 58
     * {@inheritdoc}
71 58
     *
72 58
     * @throws ReflectionException
73 58
     * @throws InvalidArgumentException
74 58
     */
75
    public static function preParse(SplFileInfo $file, ContainerBuilder $container, array $configs = []): void
76
    {
77
        $container->setParameter('overblog_graphql_types.classes_map', self::processFile($file, $container, $configs, true));
78
    }
79
80
    /**
81
     * @throws ReflectionException
82 20
     * @throws InvalidArgumentException
83
     */
84 20
    public static function parse(SplFileInfo $file, ContainerBuilder $container, array $configs = []): array
85 20
    {
86
        return self::processFile($file, $container, $configs, false);
87
    }
88 20
89 20
    /**
90 20
     * @internal
91
     */
92 20
    public static function reset(): void
93 20
    {
94
        self::$classesMap = [];
95 20
        self::$providers = [];
96 20
        self::$classAnnotationsCache = [];
97 20
        self::$annotationReader = null;
98
    }
99
100
    /**
101
     * Process a file.
102
     *
103
     * @throws InvalidArgumentException
104
     */
105
    private static function processFile(SplFileInfo $file, ContainerBuilder $container, array $configs, bool $preProcess): array
106
    {
107
        self::$doctrineMapping = $configs['doctrine']['types_mapping'];
108 20
        $container->addResource(new FileResource($file->getRealPath()));
109 8
110 8
        try {
111
            $className = $file->getBasename('.php');
112
            if (preg_match('#namespace (.+);#', file_get_contents($file->getRealPath()), $matches)) {
113
                $className = trim($matches[1]).'\\'.$className;
114
            }
115
            [$reflectionEntity, $classAnnotations, $properties, $methods] = self::extractClassAnnotations($className);
116
            $gqlTypes = [];
117 20
118
            foreach ($classAnnotations as $classAnnotation) {
119
                $gqlTypes = self::classAnnotationsToGQLConfiguration(
120
                    $reflectionEntity,
121
                    $classAnnotation,
122
                    $configs,
123
                    $classAnnotations,
124
                    $properties,
125
                    $methods,
126
                    $gqlTypes,
127 20
                    $preProcess
128
                );
129
            }
130 20
131 20
            return $preProcess ? self::$classesMap : $gqlTypes;
132 20
        } catch (\InvalidArgumentException $e) {
133 20
            throw new InvalidArgumentException(sprintf('Failed to parse GraphQL annotations from file "%s".', $file), $e->getCode(), $e);
134 20
        }
135 20
    }
136
137
    private static function classAnnotationsToGQLConfiguration(
138 20
        ReflectionClass $reflectionEntity,
139 19
        object $classAnnotation,
140
        array $configs,
141
        array $classAnnotations,
142
        array $properties,
143 19
        array $methods,
144
        array $gqlTypes,
145
        bool $preProcess
146
    ): array {
147 19
        $gqlConfiguration = $gqlType = $gqlName = null;
148 19
149 19
        switch (true) {
150 19
            case $classAnnotation instanceof GQL\Type:
151 19
                $gqlType = self::GQL_TYPE;
152
                $gqlName = $classAnnotation->name ?: $reflectionEntity->getShortName();
153
                if (!$preProcess) {
154 19
                    $gqlConfiguration = self::typeAnnotationToGQLConfiguration(
155
                        $reflectionEntity, $classAnnotation, $gqlName, $classAnnotations, $properties, $methods, $configs
156
                    );
157
158
                    if ($classAnnotation instanceof GQL\Relay\Connection) {
159 19
                        if (!$reflectionEntity->implementsInterface(ConnectionInterface::class)) {
160 19
                            throw new InvalidArgumentException(sprintf('The annotation @Connection on class "%s" can only be used on class implementing the ConnectionInterface.', $reflectionEntity->getName()));
161
                        }
162 19
163
                        if (!($classAnnotation->edge xor $classAnnotation->node)) {
164
                            throw new InvalidArgumentException(sprintf('The annotation @Connection on class "%s" is invalid. You must define the "edge" OR the "node" attribute.', $reflectionEntity->getName()));
165 20
                        }
166
167 19
                        $edgeType = $classAnnotation->edge;
168 19
                        if (!$edgeType) {
169 19
                            $edgeType = sprintf('%sEdge', $gqlName);
170 19
                            $gqlTypes[$edgeType] = [
171 19
                                'type' => 'object',
172 19
                                'config' => [
173
                                    'builders' => [
174
                                        ['builder' => 'relay-edge', 'builderConfig' => ['nodeType' => $classAnnotation->node]],
175 19
                                    ],
176
                                ],
177 19
                            ];
178 19
                        }
179 19
                        if (!isset($gqlConfiguration['config']['builders'])) {
180 19
                            $gqlConfiguration['config']['builders'] = [];
181 19
                        }
182
                        array_unshift($gqlConfiguration['config']['builders'], ['builder' => 'relay-connection', 'builderConfig' => ['edgeType' => $edgeType]]);
183
                    }
184 19
                }
185
                break;
186 19
187 19
            case $classAnnotation instanceof GQL\Input:
188 19
                $gqlType = self::GQL_INPUT;
189 19
                $gqlName = $classAnnotation->name ?: self::suffixName($reflectionEntity->getShortName(), 'Input');
190 19
                if (!$preProcess) {
191
                    $gqlConfiguration = self::inputAnnotationToGQLConfiguration(
192
                        $classAnnotation, $classAnnotations, $properties, $reflectionEntity->getNamespaceName()
193 19
                    );
194
                }
195 19
                break;
196 19
197 19
            case $classAnnotation instanceof GQL\Scalar:
198 19
                $gqlType = self::GQL_SCALAR;
199 19
                if (!$preProcess) {
200
                    $gqlConfiguration = self::scalarAnnotationToGQLConfiguration(
201
                        $reflectionEntity->getName(), $classAnnotation, $classAnnotations
202 19
                    );
203
                }
204 19
                break;
205 19
206 19
            case $classAnnotation instanceof GQL\Enum:
207 19
                $gqlType = self::GQL_ENUM;
208 19
                if (!$preProcess) {
209
                    $gqlConfiguration = self::enumAnnotationToGQLConfiguration(
210
                        $classAnnotation, $classAnnotations, $reflectionEntity->getConstants()
211 19
                    );
212
                }
213 19
                break;
214 19
215 19
            case $classAnnotation instanceof GQL\Union:
216
                $gqlType = self::GQL_UNION;
217 19
                if (!$preProcess) {
218
                    $gqlConfiguration = self::unionAnnotationToGQLConfiguration(
219
                        $reflectionEntity->getName(), $classAnnotation, $classAnnotations, $methods
220 20
                    );
221 20
                }
222 19
                break;
223
224
            case $classAnnotation instanceof GQL\TypeInterface:
225 20
                $gqlType = self::GQL_INTERFACE;
226 20
                if (!$preProcess) {
227 1
                    $gqlConfiguration = self::typeInterfaceAnnotationToGQLConfiguration(
228
                        $classAnnotation, $classAnnotations, $properties, $methods, $reflectionEntity->getNamespaceName()
229 20
                    );
230
                }
231 20
                break;
232
233
            case $classAnnotation instanceof GQL\Provider:
234
                if ($preProcess) {
235 20
                    self::$providers[$reflectionEntity->getName()] = ['annotation' => $classAnnotation, 'methods' => $methods, 'annotations' => $classAnnotations];
236
                }
237
                break;
238 20
        }
239
240 20
        if (null !== $gqlType) {
241 20
            if (!$gqlName) {
242 20
                $gqlName = $classAnnotation->name ?: $reflectionEntity->getShortName();
0 ignored issues
show
Bug introduced by
The property name does not seem to exist on Overblog\GraphQLBundle\Annotation\Provider.
Loading history...
243 20
            }
244
245 20
            if ($preProcess) {
246 20
                if (isset(self::$classesMap[$gqlName])) {
247
                    throw new InvalidArgumentException(sprintf('The GraphQL type "%s" has already been registered in class "%s"', $gqlName, self::$classesMap[$gqlName]['class']));
248 20
                }
249 19
                self::$classesMap[$gqlName] = ['type' => $gqlType, 'class' => $reflectionEntity->getName()];
250 19
            } else {
251
                $gqlTypes = [$gqlName => $gqlConfiguration] + $gqlTypes;
252 19
            }
253
        }
254 20
255
        return $gqlTypes;
256 20
    }
257 20
258 19
    /**
259
     * @throws ReflectionException
260
     */
261 20
    private static function extractClassAnnotations(string $className): array
262
    {
263
        if (!isset(self::$classAnnotationsCache[$className])) {
264 20
            $annotationReader = self::getAnnotationReader();
265
            $reflectionEntity = new ReflectionClass($className); // @phpstan-ignore-line
266
            $classAnnotations = $annotationReader->getClassAnnotations($reflectionEntity);
267 20
268
            $properties = [];
269
            $reflectionClass = new ReflectionClass($className); // @phpstan-ignore-line
270
            do {
271
                foreach ($reflectionClass->getProperties() as $property) {
272
                    if (isset($properties[$property->getName()])) {
273
                        continue;
274
                    }
275
                    $properties[$property->getName()] = ['property' => $property, 'annotations' => $annotationReader->getPropertyAnnotations($property)];
276 20
                }
277 20
            } while ($reflectionClass = $reflectionClass->getParentClass());
278 20
279 20
            $methods = [];
280 20
            foreach ($reflectionEntity->getMethods() as $method) {
281
                $methods[$method->getName()] = ['method' => $method, 'annotations' => $annotationReader->getMethodAnnotations($method)];
282 20
            }
283 20
284 20
            self::$classAnnotationsCache[$className] = [$reflectionEntity, $classAnnotations, $properties, $methods];
285
        }
286 20
287 19
        return self::$classAnnotationsCache[$className];
288
    }
289
290 19
    private static function typeAnnotationToGQLConfiguration(
291 19
        ReflectionClass $reflectionEntity,
292
        GQL\Type $classAnnotation,
293 19
        string $gqlName,
294
        array $classAnnotations,
295
        array $properties,
296 20
        array $methods,
297
        array $configs
298
    ): array {
299 20
        $rootQueryType = $configs['definitions']['schema']['default']['query'] ?? null;
300
        $rootMutationType = $configs['definitions']['schema']['default']['mutation'] ?? null;
301 20
        $isRootQuery = ($rootQueryType && $gqlName === $rootQueryType);
302 20
        $isRootMutation = ($rootMutationType && $gqlName === $rootMutationType);
303 20
        $currentValue = ($isRootQuery || $isRootMutation) ? sprintf("service('%s')", self::formatNamespaceForExpression($reflectionEntity->getName())) : 'value';
304
305
        $gqlConfiguration = self::graphQLTypeConfigFromAnnotation($classAnnotation, $classAnnotations, $properties, $methods, $reflectionEntity->getNamespaceName(), $currentValue);
306
        $providerFields = self::getGraphQLFieldsFromProviders($reflectionEntity->getNamespaceName(), $isRootMutation ? 'Mutation' : 'Query', $gqlName, ($isRootQuery || $isRootMutation));
307 20
        $gqlConfiguration['config']['fields'] = $providerFields + $gqlConfiguration['config']['fields'];
308 20
309
        if ($classAnnotation instanceof GQL\Relay\Edge) {
310
            if (!$reflectionEntity->implementsInterface(EdgeInterface::class)) {
311 20
                throw new InvalidArgumentException(sprintf('The annotation @Edge on class "%s" can only be used on class implementing the EdgeInterface.', $reflectionEntity->getName()));
312
            }
313
            if (!isset($gqlConfiguration['config']['builders'])) {
314 20
                $gqlConfiguration['config']['builders'] = [];
315
            }
316 20
            array_unshift($gqlConfiguration['config']['builders'], ['builder' => 'relay-edge', 'builderConfig' => ['nodeType' => $classAnnotation->node]]);
317
        }
318 20
319 20
        return $gqlConfiguration;
320
    }
321 20
322 20
    private static function getAnnotationReader(): AnnotationReader
323
    {
324 20
        if (null === self::$annotationReader) {
325 19
            if (!class_exists('\\Doctrine\\Common\\Annotations\\AnnotationReader') ||
326
                !class_exists('\\Doctrine\\Common\\Annotations\\AnnotationRegistry')) {
327
                throw new RuntimeException('In order to use graphql annotation, you need to require doctrine annotations');
328 20
            }
329 19
330
            AnnotationRegistry::registerLoader('class_exists');
0 ignored issues
show
Deprecated Code introduced by
The function Doctrine\Common\Annotati...istry::registerLoader() has been deprecated: This method is deprecated and will be removed in doctrine/annotations 2.0. Annotations will be autoloaded in 2.0. ( Ignorable by Annotation )

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

330
            /** @scrutinizer ignore-deprecated */ AnnotationRegistry::registerLoader('class_exists');

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
331
            self::$annotationReader = new AnnotationReader();
332 20
        }
333
334 19
        return self::$annotationReader;
335 19
    }
336
337
    private static function graphQLTypeConfigFromAnnotation(GQL\Type $typeAnnotation, array $classAnnotations, array $properties, array $methods, string $namespace, string $currentValue): array
338 20
    {
339 20
        $typeConfiguration = [];
340 19
341
        $fields = self::getGraphQLFieldsFromAnnotations($namespace, $properties, false, false, $currentValue);
342
        $fields = self::getGraphQLFieldsFromAnnotations($namespace, $methods, false, true, $currentValue) + $fields;
343 20
344 20
        $typeConfiguration['fields'] = $fields;
345 19
        $typeConfiguration = self::getDescriptionConfiguration($classAnnotations) + $typeConfiguration;
346
347
        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...
348 20
            $typeConfiguration['interfaces'] = $typeAnnotation->interfaces;
349
        }
350
351
        if ($typeAnnotation->resolveField) {
352
            $typeConfiguration['resolveField'] = self::formatExpression($typeAnnotation->resolveField);
353
        }
354
355
        if ($typeAnnotation->builders && !empty($typeAnnotation->builders)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $typeAnnotation->builders of type Overblog\GraphQLBundle\Annotation\FieldsBuilder[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
356 19
            $typeConfiguration['builders'] = array_map(function ($fieldsBuilderAnnotation) {
357
                return ['builder' => $fieldsBuilderAnnotation->builder, 'builderConfig' => $fieldsBuilderAnnotation->builderConfig];
358 19
            }, $typeAnnotation->builders);
359
        }
360 19
361 19
        $publicAnnotation = self::getFirstAnnotationMatching($classAnnotations, GQL\IsPublic::class);
362
        if ($publicAnnotation) {
363 19
            $typeConfiguration['fieldsDefaultPublic'] = self::formatExpression($publicAnnotation->value);
364 19
        }
365
366 19
        $accessAnnotation = self::getFirstAnnotationMatching($classAnnotations, GQL\Access::class);
367
        if ($accessAnnotation) {
368 19
            $typeConfiguration['fieldsDefaultAccess'] = self::formatExpression($accessAnnotation->value);
369
        }
370
371
        return ['type' => $typeAnnotation->isRelay ? 'relay-mutation-payload' : 'object', 'config' => $typeConfiguration];
372
    }
373
374 19
    /**
375
     * Create a GraphQL Interface type configuration from annotations on properties.
376 19
     */
377 19
    private static function typeInterfaceAnnotationToGQLConfiguration(GQL\TypeInterface $interfaceAnnotation, array $classAnnotations, array $properties, array $methods, string $namespace): array
378
    {
379 19
        $interfaceConfiguration = [];
380 19
381
        $fields = self::getGraphQLFieldsFromAnnotations($namespace, $properties);
382 19
        $fields = self::getGraphQLFieldsFromAnnotations($namespace, $methods, false, true) + $fields;
383
384
        $interfaceConfiguration['fields'] = $fields;
385
        $interfaceConfiguration = self::getDescriptionConfiguration($classAnnotations) + $interfaceConfiguration;
386
387
        $interfaceConfiguration['resolveType'] = self::formatExpression($interfaceAnnotation->resolveType);
388 19
389
        return ['type' => 'interface', 'config' => $interfaceConfiguration];
390 19
    }
391
392 19
    /**
393 19
     * Create a GraphQL Input type configuration from annotations on properties.
394
     */
395
    private static function inputAnnotationToGQLConfiguration(GQL\Input $inputAnnotation, array $classAnnotations, array $properties, string $namespace): array
396 19
    {
397 19
        $inputConfiguration = [];
398 19
        $fields = self::getGraphQLFieldsFromAnnotations($namespace, $properties, true);
399
400
        $inputConfiguration['fields'] = $fields;
401
        $inputConfiguration = self::getDescriptionConfiguration($classAnnotations) + $inputConfiguration;
402 19
403
        return ['type' => $inputAnnotation->isRelay ? 'relay-mutation-input' : 'input-object', 'config' => $inputConfiguration];
404 19
    }
405
406
    /**
407
     * Get a GraphQL scalar configuration from given scalar annotation.
408
     */
409
    private static function scalarAnnotationToGQLConfiguration(string $className, GQL\Scalar $scalarAnnotation, array $classAnnotations): array
410 19
    {
411
        $scalarConfiguration = [];
412 19
413
        if ($scalarAnnotation->scalarType) {
414 19
            $scalarConfiguration['scalarType'] = self::formatExpression($scalarAnnotation->scalarType);
415
        } else {
416 19
            $scalarConfiguration = [
417
                'serialize' => [$className, 'serialize'],
418 19
                'parseValue' => [$className, 'parseValue'],
419 19
                'parseLiteral' => [$className, 'parseLiteral'],
420 19
            ];
421 19
        }
422
423 19
        $scalarConfiguration = self::getDescriptionConfiguration($classAnnotations) + $scalarConfiguration;
424 19
425
        return ['type' => 'custom-scalar', 'config' => $scalarConfiguration];
426
    }
427 19
428 19
    /**
429
     * Get a GraphQL Enum configuration from given enum annotation.
430
     */
431 19
    private static function enumAnnotationToGQLConfiguration(GQL\Enum $enumAnnotation, array $classAnnotations, array $constants): array
432
    {
433
        $enumValues = $enumAnnotation->values ? $enumAnnotation->values : [];
434 19
435 19
        $values = [];
436
437 19
        foreach ($constants as $name => $value) {
438
            $valueAnnotation = current(array_filter($enumValues, function ($enumValueAnnotation) use ($name) {
439
                return $enumValueAnnotation->name == $name;
440
            }));
441
            $valueConfig = [];
442
            $valueConfig['value'] = $value;
443 19
444
            if ($valueAnnotation && $valueAnnotation->description) {
445 19
                $valueConfig['description'] = $valueAnnotation->description;
446 19
            }
447
448 19
            if ($valueAnnotation && $valueAnnotation->deprecationReason) {
449 19
                $valueConfig['deprecationReason'] = $valueAnnotation->deprecationReason;
450
            }
451 19
452 19
            $values[$name] = $valueConfig;
453 19
        }
454 19
455
        $enumConfiguration = ['values' => $values];
456 19
        $enumConfiguration = self::getDescriptionConfiguration($classAnnotations) + $enumConfiguration;
457
458
        return ['type' => 'enum', 'config' => $enumConfiguration];
459 1
    }
460
461
    /**
462
     * Get a GraphQL Union configuration from given union annotation.
463 19
     */
464
    private static function unionAnnotationToGQLConfiguration(string $className, GQL\Union $unionAnnotation, array $classAnnotations, array $methods): array
465
    {
466
        $unionConfiguration = ['types' => $unionAnnotation->types];
467
        $unionConfiguration = self::getDescriptionConfiguration($classAnnotations) + $unionConfiguration;
468
469 20
        if ($unionAnnotation->resolveType) {
470
            $unionConfiguration['resolveType'] = self::formatExpression($unionAnnotation->resolveType);
471 20
        } else {
472 20
            if (isset($methods['resolveType'])) {
473 19
                $method = $methods['resolveType']['method'];
474 19
                if ($method->isStatic() && $method->isPublic()) {
475
                    $unionConfiguration['resolveType'] = self::formatExpression(sprintf("@=call('%s::%s', [service('overblog_graphql.type_resolver'), value], true)", self::formatNamespaceForExpression($className), 'resolveType'));
476 19
                } else {
477 19
                    throw new InvalidArgumentException(sprintf('The "resolveType()" method on class must be static and public. Or you must define a "resolveType" attribute on the @Union annotation.'));
478 19
                }
479
            } else {
480 19
                throw new InvalidArgumentException(sprintf('The annotation @Union has no "resolveType" attribute and the related class has no "resolveType()" public static method. You need to define of them.'));
481 19
            }
482 1
        }
483
484 19
        return ['type' => 'union', 'config' => $unionConfiguration];
485
    }
486
487 19
    /**
488 1
     * Create GraphQL fields configuration based on annotations.
489
     */
490
    private static function getGraphQLFieldsFromAnnotations(string $namespace, array $propertiesOrMethods, bool $isInput = false, bool $isMethod = false, string $currentValue = 'value', string $fieldAnnotationName = 'Field'): array
491
    {
492 19
        $fields = [];
493
        foreach ($propertiesOrMethods as $target => $config) {
494
            $annotations = $config['annotations'];
495
            $method = $isMethod ? $config['method'] : false;
496 19
497 19
            $fieldAnnotation = self::getFirstAnnotationMatching($annotations, sprintf('Overblog\GraphQLBundle\Annotation\%s', $fieldAnnotationName));
498 19
            $accessAnnotation = self::getFirstAnnotationMatching($annotations, GQL\Access::class);
499 19
            $publicAnnotation = self::getFirstAnnotationMatching($annotations, GQL\IsPublic::class);
500 19
501 19
            if (!$fieldAnnotation) {
502
                if ($accessAnnotation || $publicAnnotation) {
503
                    throw new InvalidArgumentException(sprintf('The annotations "@Access" and/or "@Visible" defined on "%s" are only usable in addition of annotation "@Field"', $target));
504
                }
505 19
                continue;
506
            }
507
508 19
            if ($isMethod && !$method->isPublic()) {
509
                throw new InvalidArgumentException(sprintf('The Annotation "@Field" can only be applied to public method. The method "%s" is not public.', $target));
510 19
            }
511 19
512
            // Ignore field with resolver when the type is an Input
513 19
            if ($fieldAnnotation->resolve && $isInput) {
514 19
                continue;
515
            }
516
517 19
            $fieldName = $target;
518
            $fieldType = $fieldAnnotation->type;
519 19
            $fieldConfiguration = [];
520 19
            if ($fieldType) {
521
                $resolvedType = self::resolveClassFromType($fieldType);
522 19
                if (null !== $resolvedType && $isInput && !in_array($resolvedType['type'], self::VALID_INPUT_TYPES)) {
523 19
                    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']));
524
                }
525 19
526
                $fieldConfiguration['type'] = $fieldType;
527
            }
528
529
            $fieldConfiguration = self::getDescriptionConfiguration($annotations, true) + $fieldConfiguration;
530
531 19
            if (!$isInput) {
532 19
                $args = self::getArgs($fieldAnnotation->args, $isMethod && !$fieldAnnotation->argsBuilder ? $method : null);
0 ignored issues
show
Bug introduced by
It seems like $isMethod && ! $fieldAnn...uilder ? $method : null can also be of type false; however, parameter $method of Overblog\GraphQLBundle\C...tationParser::getArgs() does only seem to accept ReflectionMethod|null, maybe add an additional type check? ( Ignorable by Annotation )

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

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