Scrutinizer GitHub App not installed

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

Install GitHub App

Passed
Pull Request — master (#710)
by Vincent
22:30
created

AnnotationParser::searchClassesMapBy()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 0
Metric Value
eloc 7
c 0
b 0
f 0
dl 0
loc 14
rs 9.6111
ccs 0
cts 0
cp 0
cc 5
nc 4
nop 2
crap 30
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Overblog\GraphQLBundle\Config\Parser;
6
7
use Doctrine\ORM\Mapping\Column;
8
use Doctrine\ORM\Mapping\JoinColumn;
9
use Doctrine\ORM\Mapping\ManyToMany;
10
use Doctrine\ORM\Mapping\ManyToOne;
11
use Doctrine\ORM\Mapping\OneToMany;
12
use Doctrine\ORM\Mapping\OneToOne;
13
use Exception;
14
use Overblog\GraphQLBundle\Annotation as GQL;
15
use Overblog\GraphQLBundle\Config\Parser\Annotation\GraphClass;
16
use Overblog\GraphQLBundle\Relay\Connection\ConnectionInterface;
17
use Overblog\GraphQLBundle\Relay\Connection\EdgeInterface;
18
use ReflectionException;
19
use ReflectionMethod;
20
use ReflectionNamedType;
21
use ReflectionProperty;
22
use Reflector;
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 current;
33
use function file_get_contents;
34
use function get_class;
35
use function implode;
36
use function in_array;
37
use function is_array;
38
use function is_string;
39
use function preg_match;
40
use function sprintf;
41
use function str_replace;
42
use function strlen;
43
use function strpos;
44
use function substr;
45
use function trim;
46
47
class AnnotationParser implements PreParserInterface
48
{
49
    private static array $classesMap = [];
50
    private static array $providers = [];
51
    private static array $doctrineMapping = [];
52
    private static array $graphClassCache = [];
53
54
    private const GQL_SCALAR = 'scalar';
55
    private const GQL_ENUM = 'enum';
56
    private const GQL_TYPE = 'type';
57
    private const GQL_INPUT = 'input';
58
    private const GQL_UNION = 'union';
59
    private const GQL_INTERFACE = 'interface';
60
61
    /**
62
     * @see https://facebook.github.io/graphql/draft/#sec-Input-and-Output-Types
63
     */
64
    private const VALID_INPUT_TYPES = [self::GQL_SCALAR, self::GQL_ENUM, self::GQL_INPUT];
65
    private const VALID_OUTPUT_TYPES = [self::GQL_SCALAR, self::GQL_TYPE, self::GQL_INTERFACE, self::GQL_UNION, self::GQL_ENUM];
66
67
    /**
68
     * {@inheritdoc}
69
     *
70
     * @throws InvalidArgumentException
71
     */
72
    public static function preParse(SplFileInfo $file, ContainerBuilder $container, array $configs = []): void
73
    {
74 20
        $container->setParameter('overblog_graphql_types.classes_map', self::processFile($file, $container, $configs, true));
75
    }
76 20
77 20
    /**
78
     * @throws InvalidArgumentException
79
     */
80
    public static function parse(SplFileInfo $file, ContainerBuilder $container, array $configs = []): array
81
    {
82 20
        return self::processFile($file, $container, $configs, false);
83
    }
84 20
85
    /**
86
     * @internal
87
     */
88
    public static function reset(): void
89
    {
90 58
        self::$classesMap = [];
91
        self::$providers = [];
92 58
        self::$graphClassCache = [];
93 58
    }
94 58
95 58
    /**
96 58
     * Process a file.
97
     *
98
     * @throws InvalidArgumentException
99
     * @throws ReflectionException
100
     */
101
    private static function processFile(SplFileInfo $file, ContainerBuilder $container, array $configs, bool $preProcess): array
102
    {
103
        self::$doctrineMapping = $configs['doctrine']['types_mapping'];
104 20
        $container->addResource(new FileResource($file->getRealPath()));
105
106 20
        try {
107 20
            $className = $file->getBasename('.php');
108
            if (preg_match('#namespace (.+);#', file_get_contents($file->getRealPath()), $matches)) {
109
                $className = trim($matches[1]).'\\'.$className;
110 20
            }
111 20
112 20
            $gqlTypes = [];
113
            $graphClass = self::getGraphClass($className);
114 20
115 20
            foreach ($graphClass->getAnnotations() as $classAnnotation) {
116
                $gqlTypes = self::classAnnotationsToGQLConfiguration(
117 20
                    $graphClass,
118 20
                    $classAnnotation,
119 20
                    $configs,
120
                    $gqlTypes,
121
                    $preProcess
122
                );
123
            }
124
125
            return $preProcess ? self::$classesMap : $gqlTypes;
126
        } catch (\InvalidArgumentException $e) {
127
            throw new InvalidArgumentException(sprintf('Failed to parse GraphQL annotations from file "%s".', $file), $e->getCode(), $e);
128
        }
129
    }
130 20
131 8
    private static function classAnnotationsToGQLConfiguration(
132 8
        GraphClass $graphClass,
133
        object $classAnnotation,
134
        array $configs,
135
        array $gqlTypes,
136 20
        bool $preProcess
137
    ): array {
138
        $gqlConfiguration = $gqlType = $gqlName = null;
139
140
        switch (true) {
141
            case $classAnnotation instanceof GQL\Type:
142
                $gqlType = self::GQL_TYPE;
143
                $gqlName = $classAnnotation->name ?: $graphClass->getShortName();
144
                if (!$preProcess) {
145
                    $gqlConfiguration = self::typeAnnotationToGQLConfiguration($graphClass, $classAnnotation, $gqlName, $configs);
146 20
147
                    if ($classAnnotation instanceof GQL\Relay\Connection) {
148
                        if (!$graphClass->implementsInterface(ConnectionInterface::class)) {
149 20
                            throw new InvalidArgumentException(sprintf('The annotation @Connection on class "%s" can only be used on class implementing the ConnectionInterface.', $graphClass->getName()));
150 20
                        }
151 20
152 20
                        if (!($classAnnotation->edge xor $classAnnotation->node)) {
153 20
                            throw new InvalidArgumentException(sprintf('The annotation @Connection on class "%s" is invalid. You must define the "edge" OR the "node" attribute.', $graphClass->getName()));
154 20
                        }
155
156
                        $edgeType = $classAnnotation->edge;
157 20
                        if (!$edgeType) {
158 19
                            $edgeType = sprintf('%sEdge', $gqlName);
159
                            $gqlTypes[$edgeType] = [
160
                                'type' => 'object',
161
                                'config' => [
162 19
                                    'builders' => [
163
                                        ['builder' => 'relay-edge', 'builderConfig' => ['nodeType' => $classAnnotation->node]],
164
                                    ],
165
                                ],
166 19
                            ];
167 19
                        }
168 19
                        if (!isset($gqlConfiguration['config']['builders'])) {
169 19
                            $gqlConfiguration['config']['builders'] = [];
170 19
                        }
171
                        array_unshift($gqlConfiguration['config']['builders'], ['builder' => 'relay-connection', 'builderConfig' => ['edgeType' => $edgeType]]);
172
                    }
173 19
                }
174
                break;
175
176
            case $classAnnotation instanceof GQL\Input:
177
                $gqlType = self::GQL_INPUT;
178 19
                $gqlName = $classAnnotation->name ?: self::suffixName($graphClass->getShortName(), 'Input');
179 19
                if (!$preProcess) {
180
                    $gqlConfiguration = self::inputAnnotationToGQLConfiguration($graphClass, $classAnnotation);
181 19
                }
182
                break;
183
184 20
            case $classAnnotation instanceof GQL\Scalar:
185
                $gqlType = self::GQL_SCALAR;
186 19
                if (!$preProcess) {
187 19
                    $gqlConfiguration = self::scalarAnnotationToGQLConfiguration($graphClass, $classAnnotation);
188 19
                }
189 19
                break;
190 19
191 19
            case $classAnnotation instanceof GQL\Enum:
192
                $gqlType = self::GQL_ENUM;
193
                if (!$preProcess) {
194 19
                    $gqlConfiguration = self::enumAnnotationToGQLConfiguration($graphClass, $classAnnotation);
195
                }
196 19
                break;
197 19
198 19
            case $classAnnotation instanceof GQL\Union:
199 19
                $gqlType = self::GQL_UNION;
200 19
                if (!$preProcess) {
201
                    $gqlConfiguration = self::unionAnnotationToGQLConfiguration($graphClass, $classAnnotation);
202
                }
203 19
                break;
204
205 19
            case $classAnnotation instanceof GQL\TypeInterface:
206 19
                $gqlType = self::GQL_INTERFACE;
207 19
                if (!$preProcess) {
208 19
                    $gqlConfiguration = self::typeInterfaceAnnotationToGQLConfiguration($graphClass, $classAnnotation);
209 19
                }
210
                break;
211
212 19
            case $classAnnotation instanceof GQL\Provider:
213
                if ($preProcess) {
214 19
                    self::$providers[] = ['metadata' => $graphClass, 'annotation' => $classAnnotation];
215 19
                }
216 19
                break;
217 19
        }
218 19
219
        if (null !== $gqlType) {
220
            if (!$gqlName) {
221 19
                $gqlName = $classAnnotation->name ?: $graphClass->getShortName();
0 ignored issues
show
Bug introduced by
The property name does not seem to exist on Overblog\GraphQLBundle\Annotation\Provider.
Loading history...
222
            }
223 19
224 19
            if ($preProcess) {
225 19
                if (isset(self::$classesMap[$gqlName])) {
226 19
                    throw new InvalidArgumentException(sprintf('The GraphQL type "%s" has already been registered in class "%s"', $gqlName, self::$classesMap[$gqlName]['class']));
227 19
                }
228
                self::$classesMap[$gqlName] = ['type' => $gqlType, 'class' => $graphClass->getName()];
229
            } else {
230 19
                $gqlTypes = [$gqlName => $gqlConfiguration] + $gqlTypes;
231
            }
232 19
        }
233 19
234 19
        return $gqlTypes;
235
    }
236 19
237
    /**
238
     * @throws ReflectionException
239 20
     */
240 20
    private static function getGraphClass(string $className): GraphClass
241 19
    {
242
        if (!isset(self::$graphClassCache[$className])) {
243
            self::$graphClassCache[$className] = new GraphClass($className);
244 20
        }
245 20
246 1
        return self::$graphClassCache[$className];
247
    }
248 20
249
    private static function typeAnnotationToGQLConfiguration(
250 20
        GraphClass $graphClass,
251
        GQL\Type $classAnnotation,
252
        string $gqlName,
253
        array $configs
254 20
    ): array {
255
        $isMutation = $isDefault = $isRoot = false;
256
        foreach ($configs['definitions']['schema'] as $schemaName => $schema) {
257
            $schemaQuery = $schema['query'] ?? null;
258
            $schemaMutation = $schema['mutation'] ?? null;
259
260 20
            if ($schemaQuery && $gqlName === $schemaQuery) {
261
                $isRoot = true;
262 20
                if ('default' == $schemaName) {
263 20
                    $isDefault = true;
264 20
                }
265 20
            } elseif ($schemaMutation && $gqlName === $schemaMutation) {
266
                $isMutation = true;
267 20
                $isRoot = true;
268 20
                if ('default' == $schemaName) {
269
                    $isDefault = true;
270 20
                }
271 19
            }
272 19
        }
273
274 19
        $currentValue = $isRoot ? sprintf("service('%s')", self::formatNamespaceForExpression($graphClass->getName())) : 'value';
275
276 20
        $gqlConfiguration = self::graphQLTypeConfigFromAnnotation($graphClass, $classAnnotation, $currentValue);
277
278 20
        $providerFields = self::getGraphQLFieldsFromProviders($graphClass, $isMutation ? GQL\Mutation::class : GQL\Query::class, $gqlName, $isDefault);
279 20
        $gqlConfiguration['config']['fields'] = array_merge($gqlConfiguration['config']['fields'], $providerFields);
280 19
281
        if ($classAnnotation instanceof GQL\Relay\Edge) {
282
            if (!$graphClass->implementsInterface(EdgeInterface::class)) {
283 20
                throw new InvalidArgumentException(sprintf('The annotation @Edge on class "%s" can only be used on class implementing the EdgeInterface.', $graphClass->getName()));
284
            }
285
            if (!isset($gqlConfiguration['config']['builders'])) {
286 20
                $gqlConfiguration['config']['builders'] = [];
287
            }
288
            array_unshift($gqlConfiguration['config']['builders'], ['builder' => 'relay-edge', 'builderConfig' => ['nodeType' => $classAnnotation->node]]);
289 20
        }
290
291
        return $gqlConfiguration;
292
    }
293
294
    private static function graphQLTypeConfigFromAnnotation(GraphClass $graphClass, GQL\Type $typeAnnotation, string $currentValue): array
295
    {
296
        $typeConfiguration = [];
297
        $fieldsFromProperties = self::getGraphQLTypeFieldsFromAnnotations($graphClass, $graphClass->getPropertiesExtended(), GQL\Field::class, $currentValue);
298 20
        $fieldsFromMethods = self::getGraphQLTypeFieldsFromAnnotations($graphClass, $graphClass->getMethods(), GQL\Field::class, $currentValue);
299 20
300 20
        $typeConfiguration['fields'] = array_merge($fieldsFromProperties, $fieldsFromMethods);
301 20
        $typeConfiguration = self::getDescriptionConfiguration($graphClass->getAnnotations()) + $typeConfiguration;
302 20
303
        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...
304 20
            $typeConfiguration['interfaces'] = $typeAnnotation->interfaces;
305 20
        } else {
306 20
            $typeConfiguration['interfaces'] = array_keys(self::searchClassesMapBy(function ($gqlType, $configuration) use ($graphClass) {
307
                ['class' => $interfaceClassName] = $configuration;
308 20
309 19
                $interfaceMetadata = self::getGraphClass($interfaceClassName);
310
                if ($interfaceMetadata->isInterface() && $graphClass->implementsInterface($interfaceMetadata->getName())) {
311
                    return true;
312 19
                }
313 19
314
                return $graphClass->isSubclassOf($interfaceClassName);
315 19
            }, self::GQL_INTERFACE));
316
        }
317
318 20
        if ($typeAnnotation->resolveField) {
319
            $typeConfiguration['resolveField'] = self::formatExpression($typeAnnotation->resolveField);
320
        }
321 20
322
        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...
323 20
            $typeConfiguration['builders'] = array_map(function ($fieldsBuilderAnnotation) {
324 20
                return ['builder' => $fieldsBuilderAnnotation->builder, 'builderConfig' => $fieldsBuilderAnnotation->builderConfig];
325 20
            }, $typeAnnotation->builders);
326
        }
327
328
        if ($typeAnnotation->isTypeOf) {
329 20
            $typeConfiguration['isTypeOf'] = $typeAnnotation->isTypeOf;
330 20
        }
331
332
        $publicAnnotation = self::getFirstAnnotationMatching($graphClass->getAnnotations(), GQL\IsPublic::class);
333 20
        if ($publicAnnotation) {
334
            $typeConfiguration['fieldsDefaultPublic'] = self::formatExpression($publicAnnotation->value);
335
        }
336 20
337
        $accessAnnotation = self::getFirstAnnotationMatching($graphClass->getAnnotations(), GQL\Access::class);
338 20
        if ($accessAnnotation) {
339
            $typeConfiguration['fieldsDefaultAccess'] = self::formatExpression($accessAnnotation->value);
340 20
        }
341 20
342
        return ['type' => $typeAnnotation->isRelay ? 'relay-mutation-payload' : 'object', 'config' => $typeConfiguration];
343 20
    }
344 20
345
    /**
346 20
     * Create a GraphQL Interface type configuration from annotations on properties.
347 19
     */
348
    private static function typeInterfaceAnnotationToGQLConfiguration(GraphClass $graphClass, GQL\TypeInterface $interfaceAnnotation): array
349
    {
350 20
        $interfaceConfiguration = [];
351 19
352
        $fieldsFromProperties = self::getGraphQLTypeFieldsFromAnnotations($graphClass, $graphClass->getPropertiesExtended());
353
        $fieldsFromMethods = self::getGraphQLTypeFieldsFromAnnotations($graphClass, $graphClass->getMethods());
354 20
355
        $interfaceConfiguration['fields'] = array_merge($fieldsFromProperties, $fieldsFromMethods);
356 19
        $interfaceConfiguration = self::getDescriptionConfiguration($graphClass->getAnnotations()) + $interfaceConfiguration;
357 19
358
        $interfaceConfiguration['resolveType'] = self::formatExpression($interfaceAnnotation->resolveType);
359
360 20
        return ['type' => 'interface', 'config' => $interfaceConfiguration];
361 20
    }
362 19
363
    /**
364
     * Create a GraphQL Input type configuration from annotations on properties.
365 20
     */
366 20
    private static function inputAnnotationToGQLConfiguration(GraphClass $graphClass, GQL\Input $inputAnnotation): array
367 19
    {
368
        $inputConfiguration = array_merge([
369
            'fields' => self::getGraphQLInputFieldsFromAnnotations($graphClass, $graphClass->getPropertiesExtended()),
370 20
        ], self::getDescriptionConfiguration($graphClass->getAnnotations()));
371
372
        return ['type' => $inputAnnotation->isRelay ? 'relay-mutation-input' : 'input-object', 'config' => $inputConfiguration];
373
    }
374
375
    /**
376 19
     * Get a GraphQL scalar configuration from given scalar annotation.
377
     */
378 19
    private static function scalarAnnotationToGQLConfiguration(GraphClass $graphClass, GQL\Scalar $scalarAnnotation): array
379
    {
380 19
        $scalarConfiguration = [];
381 19
382
        if ($scalarAnnotation->scalarType) {
383 19
            $scalarConfiguration['scalarType'] = self::formatExpression($scalarAnnotation->scalarType);
384 19
        } else {
385
            $scalarConfiguration = [
386 19
                'serialize' => [$graphClass->getName(), 'serialize'],
387
                'parseValue' => [$graphClass->getName(), 'parseValue'],
388 19
                'parseLiteral' => [$graphClass->getName(), 'parseLiteral'],
389
            ];
390
        }
391
392
        $scalarConfiguration = self::getDescriptionConfiguration($graphClass->getAnnotations()) + $scalarConfiguration;
393
394 19
        return ['type' => 'custom-scalar', 'config' => $scalarConfiguration];
395
    }
396 19
397 19
    /**
398
     * Get a GraphQL Enum configuration from given enum annotation.
399 19
     */
400 19
    private static function enumAnnotationToGQLConfiguration(GraphClass $graphClass, GQL\Enum $enumAnnotation): array
401
    {
402 19
        $enumValues = $enumAnnotation->values ? $enumAnnotation->values : [];
403
404
        $values = [];
405
406
        foreach ($graphClass->getConstants() as $name => $value) {
407
            $valueAnnotation = current(array_filter($enumValues, function ($enumValueAnnotation) use ($name) {
408 19
                return $enumValueAnnotation->name == $name;
409
            }));
410 19
            $valueConfig = [];
411
            $valueConfig['value'] = $value;
412 19
413 19
            if ($valueAnnotation && $valueAnnotation->description) {
414
                $valueConfig['description'] = $valueAnnotation->description;
415
            }
416 19
417 19
            if ($valueAnnotation && $valueAnnotation->deprecationReason) {
418 19
                $valueConfig['deprecationReason'] = $valueAnnotation->deprecationReason;
419
            }
420
421
            $values[$name] = $valueConfig;
422 19
        }
423
424 19
        $enumConfiguration = ['values' => $values];
425
        $enumConfiguration = self::getDescriptionConfiguration($graphClass->getAnnotations()) + $enumConfiguration;
426
427
        return ['type' => 'enum', 'config' => $enumConfiguration];
428
    }
429
430 19
    /**
431
     * Get a GraphQL Union configuration from given union annotation.
432 19
     */
433
    private static function unionAnnotationToGQLConfiguration(GraphClass $graphClass, GQL\Union $unionAnnotation): array
434 19
    {
435
        $unionConfiguration = [];
436 19
        if ($unionAnnotation->types) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $unionAnnotation->types 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...
437
            $unionConfiguration['types'] = $unionAnnotation->types;
438 19
        } else {
439 19
            $unionConfiguration['types'] = array_keys(self::searchClassesMapBy(function ($gqlType, $configuration) use ($graphClass) {
440 19
                $typeClassName = $configuration['class'];
0 ignored issues
show
Unused Code introduced by
The assignment to $typeClassName is dead and can be removed.
Loading history...
441 19
                ['class' => $typeClassName] = $configuration;
442
443 19
                $typeMetadata = self::getGraphClass($typeClassName);
444 19
445
                if ($graphClass->isInterface() && $typeMetadata->implementsInterface($graphClass->getName())) {
446
                    return true;
447 19
                }
448 19
449
                return $typeMetadata->isSubclassOf($graphClass->getName());
450
            }, self::GQL_TYPE));
451 19
        }
452
453
        $unionConfiguration = self::getDescriptionConfiguration($graphClass->getAnnotations()) + $unionConfiguration;
454 19
455 19
        if ($unionAnnotation->resolveType) {
456
            $unionConfiguration['resolveType'] = self::formatExpression($unionAnnotation->resolveType);
457 19
        } else {
458
            if ($graphClass->hasMethod('resolveType')) {
459
                $method = $graphClass->getMethod('resolveType');
460
                if ($method->isStatic() && $method->isPublic()) {
461
                    $unionConfiguration['resolveType'] = self::formatExpression(sprintf("@=call('%s::%s', [service('overblog_graphql.type_resolver'), value], true)", self::formatNamespaceForExpression($graphClass->getName()), 'resolveType'));
462
                } else {
463 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.'));
464
                }
465 19
            } else {
466 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.'));
467
            }
468 19
        }
469 19
470
        return ['type' => 'union', 'config' => $unionConfiguration];
471 19
    }
472 19
473 19
    /**
474 19
     * @param ReflectionMethod|ReflectionProperty $reflector
475
     */
476 19
    private static function getTypeFieldConfigurationFromReflector(GraphClass $graphClass, Reflector $reflector, string $fieldAnnotationName = GQL\Field::class, string $currentValue = 'value'): array
477
    {
478
        $annotations = $graphClass->getAnnotations($reflector);
479 1
480
        $fieldAnnotation = self::getFirstAnnotationMatching($annotations, $fieldAnnotationName);
481
        $accessAnnotation = self::getFirstAnnotationMatching($annotations, GQL\Access::class);
482
        $publicAnnotation = self::getFirstAnnotationMatching($annotations, GQL\IsPublic::class);
483 19
484
        $isMethod = $reflector instanceof ReflectionMethod;
485
486
        if (!$fieldAnnotation) {
487
            if ($accessAnnotation || $publicAnnotation) {
488
                throw new InvalidArgumentException(sprintf('The annotations "@Access" and/or "@Visible" defined on "%s" are only usable in addition of annotation "@Field"', $reflector->getName()));
489 20
            }
490
491 20
            return [];
492 20
        }
493 19
494 19
        if ($isMethod && !$reflector->isPublic()) {
495
            throw new InvalidArgumentException(sprintf('The Annotation "@Field" can only be applied to public method. The method "%s" is not public.', $reflector->getName()));
496 19
        }
497 19
498 19
        $fieldName = $reflector->getName();
499
        $fieldType = $fieldAnnotation->type;
500 19
        $fieldConfiguration = [];
501 19
        if ($fieldType) {
502 1
            $fieldConfiguration['type'] = $fieldType;
503
        }
504 19
505
        $fieldConfiguration = self::getDescriptionConfiguration($annotations, true) + $fieldConfiguration;
506
507 19
        $args = self::getArgs($fieldAnnotation->args, $isMethod && !$fieldAnnotation->argsBuilder ? $reflector : null);
508 1
509
        if (!empty($args)) {
510
            $fieldConfiguration['args'] = $args;
511
        }
512 19
513
        $fieldName = $fieldAnnotation->name ?: $fieldName;
514
515
        if ($fieldAnnotation->resolve) {
516 19
            $fieldConfiguration['resolve'] = self::formatExpression($fieldAnnotation->resolve);
517 19
        } else {
518 19
            if ($isMethod) {
519 19
                $fieldConfiguration['resolve'] = self::formatExpression(sprintf('call(%s.%s, %s)', $currentValue, $reflector->getName(), self::formatArgsForExpression($args)));
520 19
            } else {
521 19
                if ($fieldName !== $reflector->getName() || 'value' !== $currentValue) {
522
                    $fieldConfiguration['resolve'] = self::formatExpression(sprintf('%s.%s', $currentValue, $reflector->getName()));
523
                }
524
            }
525 19
        }
526
527
        if ($fieldAnnotation->argsBuilder) {
528 19
            if (is_string($fieldAnnotation->argsBuilder)) {
529
                $fieldConfiguration['argsBuilder'] = $fieldAnnotation->argsBuilder;
530 19
            } elseif (is_array($fieldAnnotation->argsBuilder)) {
531 19
                list($builder, $builderConfig) = $fieldAnnotation->argsBuilder;
532
                $fieldConfiguration['argsBuilder'] = ['builder' => $builder, 'config' => $builderConfig];
533 19
            } else {
534 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, $reflector->getName()));
535
            }
536
        }
537 19
538
        if ($fieldAnnotation->fieldBuilder) {
539 19
            if (is_string($fieldAnnotation->fieldBuilder)) {
540 19
                $fieldConfiguration['builder'] = $fieldAnnotation->fieldBuilder;
541
            } elseif (is_array($fieldAnnotation->fieldBuilder)) {
542 19
                list($builder, $builderConfig) = $fieldAnnotation->fieldBuilder;
543 19
                $fieldConfiguration['builder'] = $builder;
544
                $fieldConfiguration['builderConfig'] = $builderConfig ?: [];
545 19
            } else {
546
                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, $reflector->getName()));
547
            }
548
        } else {
549
            if (!$fieldType) {
550
                if ($isMethod) {
551 19
                    if ($reflector->hasReturnType()) {
0 ignored issues
show
Bug introduced by
The method hasReturnType() does not exist on Reflector. It seems like you code against a sub-type of Reflector such as ReflectionFunction or ReflectionFunctionAbstract or ReflectionMethod. ( Ignorable by Annotation )

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

551
                    if ($reflector->/** @scrutinizer ignore-call */ hasReturnType()) {
Loading history...
552 19
                        try {
553
                            $fieldConfiguration['type'] = self::resolveGraphQLTypeFromReflectionType($reflector->getReturnType(), self::VALID_OUTPUT_TYPES);
0 ignored issues
show
Bug introduced by
The method getReturnType() does not exist on Reflector. It seems like you code against a sub-type of Reflector such as ReflectionFunction or ReflectionFunctionAbstract or ReflectionMethod. ( Ignorable by Annotation )

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

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