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 (#801)
by Vincent
20:50
created

MetadataParser   F

Complexity

Total Complexity 169

Size/Duplication

Total Lines 895
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 423
c 1
b 0
f 0
dl 0
loc 895
rs 2
wmc 169

28 Methods

Rating   Name   Duplication   Size   Complexity  
F getTypeFieldConfigurationFromReflector() 0 114 30
A formatArgsForExpression() 0 8 2
B unionMetadataToGQLConfiguration() 0 38 8
A preParse() 0 3 1
A formatMetadata() 0 3 1
A getGraphQLTypeFieldsFromAnnotations() 0 9 2
A getMetadataMatching() 0 14 4
C typeMetadataToGQLConfiguration() 0 46 13
A enumMetadataToGQLConfiguration() 0 27 6
A getClassProperties() 0 13 4
F classMetadatasToGQLConfiguration() 0 107 25
A formatNamespaceForExpression() 0 3 1
A guessType() 0 14 3
F getGraphQLFieldsFromProviders() 0 78 23
B graphQLTypeConfigFromAnnotation() 0 55 10
A formatExpression() 0 3 2
A reset() 0 9 1
A inputMetadataToGQLConfiguration() 0 7 2
A scalarMetadataToGQLConfiguration() 0 17 2
A getDescriptionConfiguration() 0 16 4
A guessArgs() 0 21 4
A suffixName() 0 3 2
A typeInterfaceMetadataToGQLConfiguration() 0 13 1
A getClassReflection() 0 5 1
A parse() 0 3 1
A getFirstMetadataMatching() 0 5 1
A processFile() 0 28 6
B getGraphQLInputFieldsFromMetadatas() 0 47 9

How to fix   Complexity   

Complex Class

Complex classes like MetadataParser often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use MetadataParser, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Overblog\GraphQLBundle\Config\Parser\MetadataParser;
6
7
use Doctrine\Common\Annotations\AnnotationException;
8
use Overblog\GraphQLBundle\Annotation\Annotation as Meta;
9
use Overblog\GraphQLBundle\Annotation as Metadata;
10
use Overblog\GraphQLBundle\Config\Parser\MetadataParser\TypeGuesser\DoctrineTypeGuesser;
11
use Overblog\GraphQLBundle\Config\Parser\MetadataParser\TypeGuesser\TypeGuessingException;
12
use Overblog\GraphQLBundle\Config\Parser\MetadataParser\TypeGuesser\TypeHintTypeGuesser;
13
use Overblog\GraphQLBundle\Config\Parser\PreParserInterface;
14
use Overblog\GraphQLBundle\Relay\Connection\ConnectionInterface;
15
use Overblog\GraphQLBundle\Relay\Connection\EdgeInterface;
16
use ReflectionClass;
17
use ReflectionException;
18
use ReflectionMethod;
19
use ReflectionProperty;
20
use Reflector;
21
use RuntimeException;
22
use SplFileInfo;
23
use Symfony\Component\Config\Resource\FileResource;
24
use Symfony\Component\DependencyInjection\ContainerBuilder;
25
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
26
use function array_filter;
27
use function array_keys;
28
use function array_map;
29
use function array_unshift;
30
use function current;
31
use function file_get_contents;
32
use function implode;
33
use function in_array;
34
use function is_array;
35
use function is_string;
36
use function preg_match;
37
use function sprintf;
38
use function str_replace;
39
use function strlen;
40
use function substr;
41
use function trim;
42
43
abstract class MetadataParser implements PreParserInterface
44
{
45
    const ANNOTATION_NAMESPACE = 'Overblog\GraphQLBundle\Annotation\\';
46
    const METADATA_FORMAT = '%s';
47
48
    private static ClassesTypesMap $map;
49
    private static array $typeGuessers = [];
50
    private static array $providers = [];
51
    private static array $reflections = [];
52
53
    private const GQL_SCALAR = 'scalar';
54
    private const GQL_ENUM = 'enum';
55
    private const GQL_TYPE = 'type';
56
    private const GQL_INPUT = 'input';
57
    private const GQL_UNION = 'union';
58
    private const GQL_INTERFACE = 'interface';
59
60
    /**
61
     * @see https://facebook.github.io/graphql/draft/#sec-Input-and-Output-Types
62
     */
63
    private const VALID_INPUT_TYPES = [self::GQL_SCALAR, self::GQL_ENUM, self::GQL_INPUT];
64
    private const VALID_OUTPUT_TYPES = [self::GQL_SCALAR, self::GQL_TYPE, self::GQL_INTERFACE, self::GQL_UNION, self::GQL_ENUM];
65
66
    /**
67
     * {@inheritdoc}
68
     *
69
     * @throws InvalidArgumentException
70
     * @throws ReflectionException
71
     */
72
    public static function preParse(SplFileInfo $file, ContainerBuilder $container, array $configs = []): void
73
    {
74
        $container->setParameter('overblog_graphql_types.classes_map', self::processFile($file, $container, $configs, true));
75
    }
76
77
    /**
78
     * @throws InvalidArgumentException
79
     * @throws ReflectionException
80
     */
81
    public static function parse(SplFileInfo $file, ContainerBuilder $container, array $configs = []): array
82
    {
83
        return self::processFile($file, $container, $configs, false);
84
    }
85
86
    /**
87
     * @internal
88
     */
89
    public static function reset(array $configs): void
90
    {
91
        self::$map = new ClassesTypesMap();
92
        self::$typeGuessers = [
93
            new TypeHintTypeGuesser(self::$map),
94
            new DoctrineTypeGuesser(self::$map, $configs['doctrine']['types_mapping']),
95
        ];
96
        self::$providers = [];
97
        self::$reflections = [];
98
    }
99
100
    /**
101
     * Process a file.
102
     *
103
     * @throws InvalidArgumentException|ReflectionException|AnnotationException
104
     */
105
    private static function processFile(SplFileInfo $file, ContainerBuilder $container, array $configs, bool $preProcess): array
106
    {
107
        $container->addResource(new FileResource($file->getRealPath()));
108
109
        try {
110
            $className = $file->getBasename('.php');
111
            if (preg_match('#namespace (.+);#', file_get_contents($file->getRealPath()), $matches)) {
112
                $className = trim($matches[1]).'\\'.$className;
113
            }
114
115
            $gqlTypes = [];
116
            $reflectionClass = self::getClassReflection($className);
117
118
            foreach (static::getMetadatas($reflectionClass) as $classMetadata) {
119
                if ($classMetadata instanceof Meta) {
120
                    $gqlTypes = self::classMetadatasToGQLConfiguration(
121
                        $reflectionClass,
122
                        $classMetadata,
123
                        $configs,
124
                        $gqlTypes,
125
                        $preProcess
126
                    );
127
                }
128
            }
129
130
            return $preProcess ? self::$map->toArray() : $gqlTypes;
131
        } catch (\InvalidArgumentException $e) {
132
            throw new InvalidArgumentException(sprintf('Failed to parse GraphQL metadata from file "%s".', $file), $e->getCode(), $e);
133
        }
134
    }
135
136
    private static function classMetadatasToGQLConfiguration(
137
        ReflectionClass $reflectionClass,
138
        Meta $classMetadata,
139
        array $configs,
140
        array $gqlTypes,
141
        bool $preProcess
142
    ): array {
143
        $gqlConfiguration = $gqlType = $gqlName = null;
144
145
        switch (true) {
146
            case $classMetadata instanceof Metadata\Type:
147
                $gqlType = self::GQL_TYPE;
148
                $gqlName = $classMetadata->name ?? $reflectionClass->getShortName();
149
                if (!$preProcess) {
150
                    $gqlConfiguration = self::typeMetadataToGQLConfiguration($reflectionClass, $classMetadata, $gqlName, $configs);
151
152
                    if ($classMetadata instanceof Metadata\Relay\Connection) {
153
                        if (!$reflectionClass->implementsInterface(ConnectionInterface::class)) {
154
                            throw new InvalidArgumentException(sprintf('The metadata %s on class "%s" can only be used on class implementing the ConnectionInterface.', self::formatMetadata('Connection'), $reflectionClass->getName()));
155
                        }
156
157
                        if (!(isset($classMetadata->edge) xor isset($classMetadata->node))) {
158
                            throw new InvalidArgumentException(sprintf('The metadata %s on class "%s" is invalid. You must define either the "edge" OR the "node" attribute, but not both.', self::formatMetadata('Connection'), $reflectionClass->getName()));
159
                        }
160
161
                        $edgeType = $classMetadata->edge ?? false;
162
                        if (!$edgeType) {
163
                            $edgeType = $gqlName.'Edge';
164
                            $gqlTypes[$edgeType] = [
165
                                'type' => 'object',
166
                                'config' => [
167
                                    'builders' => [
168
                                        ['builder' => 'relay-edge', 'builderConfig' => ['nodeType' => $classMetadata->node]],
169
                                    ],
170
                                ],
171
                            ];
172
                        }
173
174
                        if (!isset($gqlConfiguration['config']['builders'])) {
175
                            $gqlConfiguration['config']['builders'] = [];
176
                        }
177
178
                        array_unshift($gqlConfiguration['config']['builders'], ['builder' => 'relay-connection', 'builderConfig' => ['edgeType' => $edgeType]]);
179
                    }
180
                }
181
                break;
182
183
            case $classMetadata instanceof Metadata\Input:
184
                $gqlType = self::GQL_INPUT;
185
                $gqlName = $classMetadata->name ?? self::suffixName($reflectionClass->getShortName(), 'Input');
186
                if (!$preProcess) {
187
                    $gqlConfiguration = self::inputMetadataToGQLConfiguration($reflectionClass, $classMetadata);
188
                }
189
                break;
190
191
            case $classMetadata instanceof Metadata\Scalar:
192
                $gqlType = self::GQL_SCALAR;
193
                if (!$preProcess) {
194
                    $gqlConfiguration = self::scalarMetadataToGQLConfiguration($reflectionClass, $classMetadata);
195
                }
196
                break;
197
198
            case $classMetadata instanceof Metadata\Enum:
199
                $gqlType = self::GQL_ENUM;
200
                if (!$preProcess) {
201
                    $gqlConfiguration = self::enumMetadataToGQLConfiguration($reflectionClass, $classMetadata);
202
                }
203
                break;
204
205
            case $classMetadata instanceof Metadata\Union:
206
                $gqlType = self::GQL_UNION;
207
                if (!$preProcess) {
208
                    $gqlConfiguration = self::unionMetadataToGQLConfiguration($reflectionClass, $classMetadata);
209
                }
210
                break;
211
212
            case $classMetadata instanceof Metadata\TypeInterface:
213
                $gqlType = self::GQL_INTERFACE;
214
                if (!$preProcess) {
215
                    $gqlConfiguration = self::typeInterfaceMetadataToGQLConfiguration($reflectionClass, $classMetadata);
216
                }
217
                break;
218
219
            case $classMetadata instanceof Metadata\Provider:
220
                if ($preProcess) {
221
                    self::$providers[] = ['reflectionClass' => $reflectionClass, 'metadata' => $classMetadata];
222
                }
223
224
                return [];
225
        }
226
227
        if (null !== $gqlType) {
228
            if (!$gqlName) {
229
                $gqlName = isset($classMetadata->name) ? $classMetadata->name : $reflectionClass->getShortName();
230
            }
231
232
            if ($preProcess) {
233
                if (self::$map->hasType($gqlName)) {
0 ignored issues
show
Bug introduced by
It seems like $gqlName can also be of type null; however, parameter $gqlType of Overblog\GraphQLBundle\C...ssesTypesMap::hasType() does only seem to accept string, 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

233
                if (self::$map->hasType(/** @scrutinizer ignore-type */ $gqlName)) {
Loading history...
234
                    throw new InvalidArgumentException(sprintf('The GraphQL type "%s" has already been registered in class "%s"', $gqlName, self::$map->getType($gqlName)['class']));
0 ignored issues
show
Bug introduced by
It seems like $gqlName can also be of type null; however, parameter $gqlType of Overblog\GraphQLBundle\C...ssesTypesMap::getType() does only seem to accept string, 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

234
                    throw new InvalidArgumentException(sprintf('The GraphQL type "%s" has already been registered in class "%s"', $gqlName, self::$map->getType(/** @scrutinizer ignore-type */ $gqlName)['class']));
Loading history...
235
                }
236
                self::$map->addClassType($gqlName, $reflectionClass->getName(), $gqlType);
0 ignored issues
show
Bug introduced by
It seems like $gqlName can also be of type null; however, parameter $typeName of Overblog\GraphQLBundle\C...ypesMap::addClassType() does only seem to accept string, 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

236
                self::$map->addClassType(/** @scrutinizer ignore-type */ $gqlName, $reflectionClass->getName(), $gqlType);
Loading history...
237
            } else {
238
                $gqlTypes = [$gqlName => $gqlConfiguration] + $gqlTypes;
239
            }
240
        }
241
242
        return $gqlTypes;
243
    }
244
245
    /**
246
     * @throws ReflectionException
247
     */
248
    private static function getClassReflection(string $className): ReflectionClass
249
    {
250
        self::$reflections[$className] ??= new ReflectionClass($className);
251
252
        return self::$reflections[$className];
253
    }
254
255
    private static function typeMetadataToGQLConfiguration(
256
        ReflectionClass $reflectionClass,
257
        Metadata\Type $classMetadata,
258
        string $gqlName,
259
        array $configs
260
    ): array {
261
        $isMutation = $isDefault = $isRoot = false;
262
        if (isset($configs['definitions']['schema'])) {
263
            $defaultSchemaName = isset($configs['definitions']['schema']['default']) ? 'default' : array_key_first($configs['definitions']['schema']);
264
            foreach ($configs['definitions']['schema'] as $schemaName => $schema) {
265
                $schemaQuery = $schema['query'] ?? null;
266
                $schemaMutation = $schema['mutation'] ?? null;
267
268
                if ($gqlName === $schemaQuery) {
269
                    $isRoot = true;
270
                    if ($defaultSchemaName === $schemaName) {
271
                        $isDefault = true;
272
                    }
273
                } elseif ($gqlName === $schemaMutation) {
274
                    $isMutation = true;
275
                    $isRoot = true;
276
                    if ($defaultSchemaName === $schemaName) {
277
                        $isDefault = true;
278
                    }
279
                }
280
            }
281
        }
282
283
        $currentValue = $isRoot ? sprintf("service('%s')", self::formatNamespaceForExpression($reflectionClass->getName())) : 'value';
284
285
        $gqlConfiguration = self::graphQLTypeConfigFromAnnotation($reflectionClass, $classMetadata, $currentValue);
286
287
        $providerFields = self::getGraphQLFieldsFromProviders($reflectionClass, $isMutation ? Metadata\Mutation::class : Metadata\Query::class, $gqlName, $isDefault);
288
        $gqlConfiguration['config']['fields'] = array_merge($gqlConfiguration['config']['fields'], $providerFields);
289
290
        if ($classMetadata instanceof Metadata\Relay\Edge) {
291
            if (!$reflectionClass->implementsInterface(EdgeInterface::class)) {
292
                throw new InvalidArgumentException(sprintf('The metadata %s on class "%s" can only be used on class implementing the EdgeInterface.', self::formatMetadata('Edge'), $reflectionClass->getName()));
293
            }
294
            if (!isset($gqlConfiguration['config']['builders'])) {
295
                $gqlConfiguration['config']['builders'] = [];
296
            }
297
            array_unshift($gqlConfiguration['config']['builders'], ['builder' => 'relay-edge', 'builderConfig' => ['nodeType' => $classMetadata->node]]);
298
        }
299
300
        return $gqlConfiguration;
301
    }
302
303
    private static function graphQLTypeConfigFromAnnotation(ReflectionClass $reflectionClass, Metadata\Type $typeAnnotation, string $currentValue): array
304
    {
305
        $typeConfiguration = [];
306
        $metadatas = static::getMetadatas($reflectionClass);
307
308
        $fieldsFromProperties = self::getGraphQLTypeFieldsFromAnnotations($reflectionClass, self::getClassProperties($reflectionClass), Metadata\Field::class, $currentValue);
309
        $fieldsFromMethods = self::getGraphQLTypeFieldsFromAnnotations($reflectionClass, $reflectionClass->getMethods(), Metadata\Field::class, $currentValue);
310
311
        $typeConfiguration['fields'] = array_merge($fieldsFromProperties, $fieldsFromMethods);
312
        $typeConfiguration = self::getDescriptionConfiguration($metadatas) + $typeConfiguration;
313
314
        if (!empty($typeAnnotation->interfaces)) {
315
            $typeConfiguration['interfaces'] = $typeAnnotation->interfaces;
316
        } else {
317
            $interfaces = array_keys(self::$map->searchClassesMapBy(function ($gqlType, $configuration) use ($reflectionClass) {
318
                ['class' => $interfaceClassName] = $configuration;
319
320
                $interfaceMetadata = self::getClassReflection($interfaceClassName);
321
                if ($interfaceMetadata->isInterface() && $reflectionClass->implementsInterface($interfaceMetadata->getName())) {
322
                    return true;
323
                }
324
325
                return $reflectionClass->isSubclassOf($interfaceClassName);
326
            }, self::GQL_INTERFACE));
327
328
            sort($interfaces);
329
            $typeConfiguration['interfaces'] = $interfaces;
330
        }
331
332
        if (isset($typeAnnotation->resolveField)) {
333
            $typeConfiguration['resolveField'] = self::formatExpression($typeAnnotation->resolveField);
0 ignored issues
show
Bug introduced by
It seems like $typeAnnotation->resolveField can also be of type null; however, parameter $expression of Overblog\GraphQLBundle\C...ser::formatExpression() does only seem to accept string, 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

333
            $typeConfiguration['resolveField'] = self::formatExpression(/** @scrutinizer ignore-type */ $typeAnnotation->resolveField);
Loading history...
334
        }
335
336
        $buildersAnnotations = array_merge(self::getMetadataMatching($metadatas, Metadata\FieldsBuilder::class), $typeAnnotation->builders);
337
        if (!empty($buildersAnnotations)) {
338
            $typeConfiguration['builders'] = array_map(function ($fieldsBuilderAnnotation) {
339
                return ['builder' => $fieldsBuilderAnnotation->builder, 'builderConfig' => $fieldsBuilderAnnotation->builderConfig];
340
            }, $buildersAnnotations);
341
        }
342
343
        if (isset($typeAnnotation->isTypeOf)) {
344
            $typeConfiguration['isTypeOf'] = $typeAnnotation->isTypeOf;
345
        }
346
347
        $publicMetadata = self::getFirstMetadataMatching($metadatas, Metadata\IsPublic::class);
348
        if (null !== $publicMetadata) {
349
            $typeConfiguration['fieldsDefaultPublic'] = self::formatExpression($publicMetadata->value);
350
        }
351
352
        $accessMetadata = self::getFirstMetadataMatching($metadatas, Metadata\Access::class);
353
        if (null !== $accessMetadata) {
354
            $typeConfiguration['fieldsDefaultAccess'] = self::formatExpression($accessMetadata->value);
355
        }
356
357
        return ['type' => $typeAnnotation->isRelay ? 'relay-mutation-payload' : 'object', 'config' => $typeConfiguration];
358
    }
359
360
    /**
361
     * Create a GraphQL Interface type configuration from metadatas on properties.
362
     */
363
    private static function typeInterfaceMetadataToGQLConfiguration(ReflectionClass $reflectionClass, Metadata\TypeInterface $interfaceAnnotation): array
364
    {
365
        $interfaceConfiguration = [];
366
367
        $fieldsFromProperties = self::getGraphQLTypeFieldsFromAnnotations($reflectionClass, self::getClassProperties($reflectionClass));
368
        $fieldsFromMethods = self::getGraphQLTypeFieldsFromAnnotations($reflectionClass, $reflectionClass->getMethods());
369
370
        $interfaceConfiguration['fields'] = array_merge($fieldsFromProperties, $fieldsFromMethods);
371
        $interfaceConfiguration = self::getDescriptionConfiguration(static::getMetadatas($reflectionClass)) + $interfaceConfiguration;
372
373
        $interfaceConfiguration['resolveType'] = self::formatExpression($interfaceAnnotation->resolveType);
374
375
        return ['type' => 'interface', 'config' => $interfaceConfiguration];
376
    }
377
378
    /**
379
     * Create a GraphQL Input type configuration from metadatas on properties.
380
     */
381
    private static function inputMetadataToGQLConfiguration(ReflectionClass $reflectionClass, Metadata\Input $inputAnnotation): array
382
    {
383
        $inputConfiguration = array_merge([
384
            'fields' => self::getGraphQLInputFieldsFromMetadatas($reflectionClass, self::getClassProperties($reflectionClass)),
385
        ], self::getDescriptionConfiguration(static::getMetadatas($reflectionClass)));
386
387
        return ['type' => $inputAnnotation->isRelay ? 'relay-mutation-input' : 'input-object', 'config' => $inputConfiguration];
388
    }
389
390
    /**
391
     * Get a GraphQL scalar configuration from given scalar metadata.
392
     */
393
    private static function scalarMetadataToGQLConfiguration(ReflectionClass $reflectionClass, Metadata\Scalar $scalarAnnotation): array
394
    {
395
        $scalarConfiguration = [];
396
397
        if (isset($scalarAnnotation->scalarType)) {
398
            $scalarConfiguration['scalarType'] = self::formatExpression($scalarAnnotation->scalarType);
0 ignored issues
show
Bug introduced by
It seems like $scalarAnnotation->scalarType can also be of type null; however, parameter $expression of Overblog\GraphQLBundle\C...ser::formatExpression() does only seem to accept string, 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

398
            $scalarConfiguration['scalarType'] = self::formatExpression(/** @scrutinizer ignore-type */ $scalarAnnotation->scalarType);
Loading history...
399
        } else {
400
            $scalarConfiguration = [
401
                'serialize' => [$reflectionClass->getName(), 'serialize'],
402
                'parseValue' => [$reflectionClass->getName(), 'parseValue'],
403
                'parseLiteral' => [$reflectionClass->getName(), 'parseLiteral'],
404
            ];
405
        }
406
407
        $scalarConfiguration = self::getDescriptionConfiguration(static::getMetadatas($reflectionClass)) + $scalarConfiguration;
408
409
        return ['type' => 'custom-scalar', 'config' => $scalarConfiguration];
410
    }
411
412
    /**
413
     * Get a GraphQL Enum configuration from given enum metadata.
414
     */
415
    private static function enumMetadataToGQLConfiguration(ReflectionClass $reflectionClass, Metadata\Enum $enumMetadata): array
416
    {
417
        $metadatas = static::getMetadatas($reflectionClass);
418
        $enumValues = array_merge(self::getMetadataMatching($metadatas, Metadata\EnumValue::class), $enumMetadata->values);
0 ignored issues
show
Deprecated Code introduced by
The property Overblog\GraphQLBundle\Annotation\Enum::$values has been deprecated. ( Ignorable by Annotation )

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

418
        $enumValues = array_merge(self::getMetadataMatching($metadatas, Metadata\EnumValue::class), /** @scrutinizer ignore-deprecated */ $enumMetadata->values);
Loading history...
419
420
        $values = [];
421
422
        foreach ($reflectionClass->getConstants() as $name => $value) {
423
            $valueAnnotation = current(array_filter($enumValues, fn ($enumValueAnnotation) => $enumValueAnnotation->name === $name));
424
            $valueConfig = [];
425
            $valueConfig['value'] = $value;
426
427
            if ($valueAnnotation && isset($valueAnnotation->description)) {
428
                $valueConfig['description'] = $valueAnnotation->description;
429
            }
430
431
            if ($valueAnnotation && isset($valueAnnotation->deprecationReason)) {
432
                $valueConfig['deprecationReason'] = $valueAnnotation->deprecationReason;
433
            }
434
435
            $values[$name] = $valueConfig;
436
        }
437
438
        $enumConfiguration = ['values' => $values];
439
        $enumConfiguration = self::getDescriptionConfiguration(static::getMetadatas($reflectionClass)) + $enumConfiguration;
440
441
        return ['type' => 'enum', 'config' => $enumConfiguration];
442
    }
443
444
    /**
445
     * Get a GraphQL Union configuration from given union metadata.
446
     */
447
    private static function unionMetadataToGQLConfiguration(ReflectionClass $reflectionClass, Metadata\Union $unionMetadata): array
448
    {
449
        $unionConfiguration = [];
450
        if (!empty($unionMetadata->types)) {
451
            $unionConfiguration['types'] = $unionMetadata->types;
452
        } else {
453
            $types = array_keys(self::$map->searchClassesMapBy(function ($gqlType, $configuration) use ($reflectionClass) {
454
                $typeClassName = $configuration['class'];
455
                $typeMetadata = self::getClassReflection($typeClassName);
456
457
                if ($reflectionClass->isInterface() && $typeMetadata->implementsInterface($reflectionClass->getName())) {
458
                    return true;
459
                }
460
461
                return $typeMetadata->isSubclassOf($reflectionClass->getName());
462
            }, self::GQL_TYPE));
463
            sort($types);
464
            $unionConfiguration['types'] = $types;
465
        }
466
467
        $unionConfiguration = self::getDescriptionConfiguration(static::getMetadatas($reflectionClass)) + $unionConfiguration;
468
469
        if (isset($unionMetadata->resolveType)) {
470
            $unionConfiguration['resolveType'] = self::formatExpression($unionMetadata->resolveType);
0 ignored issues
show
Bug introduced by
It seems like $unionMetadata->resolveType can also be of type null; however, parameter $expression of Overblog\GraphQLBundle\C...ser::formatExpression() does only seem to accept string, 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

470
            $unionConfiguration['resolveType'] = self::formatExpression(/** @scrutinizer ignore-type */ $unionMetadata->resolveType);
Loading history...
471
        } else {
472
            if ($reflectionClass->hasMethod('resolveType')) {
473
                $method = $reflectionClass->getMethod('resolveType');
474
                if ($method->isStatic() && $method->isPublic()) {
475
                    $unionConfiguration['resolveType'] = self::formatExpression(sprintf("@=call('%s::%s', [service('overblog_graphql.type_resolver'), value], true)", self::formatNamespaceForExpression($reflectionClass->getName()), 'resolveType'));
476
                } else {
477
                    throw new InvalidArgumentException(sprintf('The "resolveType()" method on class must be static and public. Or you must define a "resolveType" attribute on the %s metadata.', self::formatMetadata('Union')));
478
                }
479
            } else {
480
                throw new InvalidArgumentException(sprintf('The metadata %s has no "resolveType" attribute and the related class has no "resolveType()" public static method. You need to define of them.', self::formatMetadata('Union')));
481
            }
482
        }
483
484
        return ['type' => 'union', 'config' => $unionConfiguration];
485
    }
486
487
    /**
488
     * @phpstan-param ReflectionMethod|ReflectionProperty $reflector
489
     * @phpstan-param class-string<Metadata\Field> $fieldMetadataName
490
     *
491
     * @throws AnnotationException
492
     */
493
    private static function getTypeFieldConfigurationFromReflector(ReflectionClass $reflectionClass, Reflector $reflector, string $fieldMetadataName, string $currentValue = 'value'): array
494
    {
495
        /** @var ReflectionProperty|ReflectionMethod $reflector */
496
        $metadatas = static::getMetadatas($reflector);
497
498
        $fieldMetadata = self::getFirstMetadataMatching($metadatas, $fieldMetadataName);
499
        $accessMetadata = self::getFirstMetadataMatching($metadatas, Metadata\Access::class);
500
        $publicMetadata = self::getFirstMetadataMatching($metadatas, Metadata\IsPublic::class);
501
502
        if (null === $fieldMetadata) {
503
            if (null !== $accessMetadata || null !== $publicMetadata) {
504
                throw new InvalidArgumentException(sprintf('The metadatas %s and/or %s defined on "%s" are only usable in addition of metadata %s', self::formatMetadata('Access'), self::formatMetadata('Visible'), $reflector->getName(), self::formatMetadata('Field')));
505
            }
506
507
            return [];
508
        }
509
510
        if ($reflector instanceof ReflectionMethod && !$reflector->isPublic()) {
511
            throw new InvalidArgumentException(sprintf('The metadata %s can only be applied to public method. The method "%s" is not public.', self::formatMetadata('Field'), $reflector->getName()));
512
        }
513
514
        $fieldName = $reflector->getName();
515
        $fieldConfiguration = [];
516
517
        if (isset($fieldMetadata->type)) {
518
            $fieldConfiguration['type'] = $fieldMetadata->type;
519
        }
520
521
        $fieldConfiguration = self::getDescriptionConfiguration($metadatas, true) + $fieldConfiguration;
522
523
        $args = [];
524
525
        $argAnnotations = array_merge(self::getMetadataMatching($metadatas, Metadata\Arg::class), $fieldMetadata->args);
526
527
        foreach ($argAnnotations as $arg) {
528
            $args[$arg->name] = ['type' => $arg->type];
529
530
            if (isset($arg->description)) {
531
                $args[$arg->name]['description'] = $arg->description;
532
            }
533
534
            if (isset($arg->default)) {
535
                $args[$arg->name]['defaultValue'] = $arg->default;
536
            }
537
        }
538
539
        if (empty($argAnnotations) && $reflector instanceof ReflectionMethod) {
540
            $args = self::guessArgs($reflectionClass, $reflector);
541
        }
542
543
        if (!empty($args)) {
544
            $fieldConfiguration['args'] = $args;
545
        }
546
547
        $fieldName = $fieldMetadata->name ?? $fieldName;
548
549
        if (isset($fieldMetadata->resolve)) {
550
            $fieldConfiguration['resolve'] = self::formatExpression($fieldMetadata->resolve);
551
        } else {
552
            if ($reflector instanceof ReflectionMethod) {
553
                $fieldConfiguration['resolve'] = self::formatExpression(sprintf('call(%s.%s, %s)', $currentValue, $reflector->getName(), self::formatArgsForExpression($args)));
554
            } else {
555
                if ($fieldName !== $reflector->getName() || 'value' !== $currentValue) {
556
                    $fieldConfiguration['resolve'] = self::formatExpression(sprintf('%s.%s', $currentValue, $reflector->getName()));
557
                }
558
            }
559
        }
560
561
        if ($fieldMetadata->argsBuilder) {
562
            if (is_string($fieldMetadata->argsBuilder)) {
563
                $fieldConfiguration['argsBuilder'] = $fieldMetadata->argsBuilder;
564
            } elseif (is_array($fieldMetadata->argsBuilder)) {
565
                list($builder, $builderConfig) = $fieldMetadata->argsBuilder;
566
                $fieldConfiguration['argsBuilder'] = ['builder' => $builder, 'config' => $builderConfig];
567
            } else {
568
                throw new InvalidArgumentException(sprintf('The attribute "argsBuilder" on metadata %s defined on "%s" must be a string or an array where first index is the builder name and the second is the config.', static::formatMetadata($fieldMetadataName), $reflector->getName()));
569
            }
570
        }
571
572
        if ($fieldMetadata->fieldBuilder) {
573
            if (is_string($fieldMetadata->fieldBuilder)) {
574
                $fieldConfiguration['builder'] = $fieldMetadata->fieldBuilder;
575
            } elseif (is_array($fieldMetadata->fieldBuilder)) {
576
                list($builder, $builderConfig) = $fieldMetadata->fieldBuilder;
577
                $fieldConfiguration['builder'] = $builder;
578
                $fieldConfiguration['builderConfig'] = $builderConfig ?: [];
579
            } else {
580
                throw new InvalidArgumentException(sprintf('The attribute "fieldBuilder" on metadata %s defined on "%s" must be a string or an array where first index is the builder name and the second is the config.', static::formatMetadata($fieldMetadataName), $reflector->getName()));
581
            }
582
        } else {
583
            if (!isset($fieldMetadata->type)) {
584
                try {
585
                    $fieldConfiguration['type'] = self::guessType($reflectionClass, $reflector, self::VALID_OUTPUT_TYPES);
586
                } catch (TypeGuessingException $e) {
587
                    $error = sprintf('The attribute "type" on %s is missing on %s "%s" and cannot be auto-guessed from the following type guessers:'."\n%s\n", static::formatMetadata($fieldMetadataName), $reflector instanceof ReflectionProperty ? 'property' : 'method', $reflector->getName(), $e->getMessage());
588
589
                    throw new InvalidArgumentException($error);
590
                }
591
            }
592
        }
593
594
        if ($accessMetadata) {
595
            $fieldConfiguration['access'] = self::formatExpression($accessMetadata->value);
596
        }
597
598
        if ($publicMetadata) {
599
            $fieldConfiguration['public'] = self::formatExpression($publicMetadata->value);
600
        }
601
602
        if (isset($fieldMetadata->complexity)) {
603
            $fieldConfiguration['complexity'] = self::formatExpression($fieldMetadata->complexity);
604
        }
605
606
        return [$fieldName => $fieldConfiguration];
607
    }
608
609
    /**
610
     * Create GraphQL input fields configuration based on metadatas.
611
     *
612
     * @param ReflectionProperty[] $reflectors
613
     *
614
     * @throws AnnotationException
615
     */
616
    private static function getGraphQLInputFieldsFromMetadatas(ReflectionClass $reflectionClass, array $reflectors): array
617
    {
618
        $fields = [];
619
620
        foreach ($reflectors as $reflector) {
621
            $metadatas = static::getMetadatas($reflector);
622
623
            /** @var Metadata\Field|null $fieldMetadata */
624
            $fieldMetadata = self::getFirstMetadataMatching($metadatas, Metadata\Field::class);
625
626
            // No field metadata found
627
            if (null === $fieldMetadata) {
628
                continue;
629
            }
630
631
            // Ignore field with resolver when the type is an Input
632
            if (isset($fieldMetadata->resolve)) {
633
                continue;
634
            }
635
636
            $fieldName = $reflector->getName();
637
            if (isset($fieldMetadata->type)) {
638
                $fieldType = $fieldMetadata->type;
639
            } else {
640
                try {
641
                    $fieldType = self::guessType($reflectionClass, $reflector, self::VALID_INPUT_TYPES);
642
                } catch (TypeGuessingException $e) {
643
                    throw new InvalidArgumentException(sprintf('The attribute "type" on %s is missing on property "%s" and cannot be auto-guessed from the following type guessers:'."\n%s\n", self::formatMetadata(Metadata\Field::class), $reflector->getName(), $e->getMessage()));
644
                }
645
            }
646
            $fieldConfiguration = [];
647
            if ($fieldType) {
648
                // Resolve a PHP class from a GraphQL type
649
                $resolvedType = self::$map->getType($fieldType);
650
                // We found a type but it is not allowed
651
                if (null !== $resolvedType && !in_array($resolvedType['type'], self::VALID_INPUT_TYPES)) {
652
                    throw new InvalidArgumentException(sprintf('The type "%s" on "%s" is a "%s" not valid on an Input %s. Only Input, Scalar and Enum are allowed.', $fieldType, $reflector->getName(), $resolvedType['type'], self::formatMetadata('Field')));
653
                }
654
655
                $fieldConfiguration['type'] = $fieldType;
656
            }
657
658
            $fieldConfiguration = array_merge(self::getDescriptionConfiguration($metadatas, true), $fieldConfiguration);
659
            $fields[$fieldName] = $fieldConfiguration;
660
        }
661
662
        return $fields;
663
    }
664
665
    /**
666
     * Create GraphQL type fields configuration based on metadatas.
667
     *
668
     * @phpstan-param class-string<Metadata\Field> $fieldMetadataName
669
     *
670
     * @param ReflectionProperty[]|ReflectionMethod[] $reflectors
671
     *
672
     * @throws AnnotationException
673
     */
674
    private static function getGraphQLTypeFieldsFromAnnotations(ReflectionClass $reflectionClass, array $reflectors, string $fieldMetadataName = Metadata\Field::class, string $currentValue = 'value'): array
675
    {
676
        $fields = [];
677
678
        foreach ($reflectors as $reflector) {
679
            $fields = array_merge($fields, self::getTypeFieldConfigurationFromReflector($reflectionClass, $reflector, $fieldMetadataName, $currentValue));
680
        }
681
682
        return $fields;
683
    }
684
685
    /**
686
     * @phpstan-param class-string<Metadata\Query|Metadata\Mutation> $expectedMetadata
687
     *
688
     * Return fields config from Provider methods.
689
     * Loop through configured provider and extract fields targeting the targetType.
690
     */
691
    private static function getGraphQLFieldsFromProviders(ReflectionClass $reflectionClass, string $expectedMetadata, string $targetType, bool $isDefaultTarget = false): array
692
    {
693
        $fields = [];
694
        foreach (self::$providers as ['reflectionClass' => $providerReflection, 'metadata' => $providerMetadata]) {
695
            $defaultAccessAnnotation = self::getFirstMetadataMatching(static::getMetadatas($providerReflection), Metadata\Access::class);
696
            $defaultIsPublicAnnotation = self::getFirstMetadataMatching(static::getMetadatas($providerReflection), Metadata\IsPublic::class);
697
698
            $defaultAccess = $defaultAccessAnnotation ? self::formatExpression($defaultAccessAnnotation->value) : false;
699
            $defaultIsPublic = $defaultIsPublicAnnotation ? self::formatExpression($defaultIsPublicAnnotation->value) : false;
700
701
            $methods = [];
702
            // First found the methods matching the targeted type
703
            foreach ($providerReflection->getMethods() as $method) {
704
                $metadatas = static::getMetadatas($method);
705
706
                $metadata = self::getFirstMetadataMatching($metadatas, [Metadata\Mutation::class, Metadata\Query::class]);
707
                if (null === $metadata) {
708
                    continue;
709
                }
710
711
                // TODO: Remove old property check in 1.1
712
                $metadataTargets = $metadata->targetTypes ?? $metadata->targetType ?? null;
713
714
                if (null === $metadataTargets) {
715
                    if ($metadata instanceof Metadata\Mutation && isset($providerMetadata->targetMutationTypes)) {
716
                        $metadataTargets = $providerMetadata->targetMutationTypes;
717
                    } elseif ($metadata instanceof Metadata\Query && isset($providerMetadata->targetQueryTypes)) {
718
                        $metadataTargets = $providerMetadata->targetQueryTypes;
719
                    }
720
                }
721
722
                if (null === $metadataTargets) {
723
                    if ($isDefaultTarget) {
724
                        $metadataTargets = [$targetType];
725
                        if (!$metadata instanceof $expectedMetadata) {
726
                            continue;
727
                        }
728
                    } else {
729
                        continue;
730
                    }
731
                }
732
733
                if (!in_array($targetType, $metadataTargets)) {
734
                    continue;
735
                }
736
737
                if (!$metadata instanceof $expectedMetadata) {
738
                    if (Metadata\Mutation::class === $expectedMetadata) {
739
                        $message = sprintf('The provider "%s" try to add a query field on type "%s" (through %s on method "%s") but "%s" is a mutation.', $providerReflection->getName(), $targetType, self::formatMetadata('Query'), $method->getName(), $targetType);
740
                    } else {
741
                        $message = sprintf('The provider "%s" try to add a mutation on type "%s" (through %s on method "%s") but "%s" is not a mutation.', $providerReflection->getName(), $targetType, self::formatMetadata('Mutation'), $method->getName(), $targetType);
742
                    }
743
744
                    throw new InvalidArgumentException($message);
745
                }
746
                $methods[$method->getName()] = $method;
747
            }
748
749
            $currentValue = sprintf("service('%s')", self::formatNamespaceForExpression($providerReflection->getName()));
750
            $providerFields = self::getGraphQLTypeFieldsFromAnnotations($reflectionClass, $methods, $expectedMetadata, $currentValue);
751
            foreach ($providerFields as $fieldName => $fieldConfig) {
752
                if (isset($providerMetadata->prefix)) {
753
                    $fieldName = sprintf('%s%s', $providerMetadata->prefix, $fieldName);
754
                }
755
756
                if ($defaultAccess && !isset($fieldConfig['access'])) {
757
                    $fieldConfig['access'] = $defaultAccess;
758
                }
759
760
                if ($defaultIsPublic && !isset($fieldConfig['public'])) {
761
                    $fieldConfig['public'] = $defaultIsPublic;
762
                }
763
764
                $fields[$fieldName] = $fieldConfig;
765
            }
766
        }
767
768
        return $fields;
769
    }
770
771
    /**
772
     * Get the config for description & deprecation reason.
773
     */
774
    private static function getDescriptionConfiguration(array $metadatas, bool $withDeprecation = false): array
775
    {
776
        $config = [];
777
        $descriptionAnnotation = self::getFirstMetadataMatching($metadatas, Metadata\Description::class);
778
        if (null !== $descriptionAnnotation) {
779
            $config['description'] = $descriptionAnnotation->value;
780
        }
781
782
        if ($withDeprecation) {
783
            $deprecatedAnnotation = self::getFirstMetadataMatching($metadatas, Metadata\Deprecated::class);
784
            if (null !== $deprecatedAnnotation) {
785
                $config['deprecationReason'] = $deprecatedAnnotation->value;
786
            }
787
        }
788
789
        return $config;
790
    }
791
792
    /**
793
     * Format an array of args to a list of arguments in an expression.
794
     */
795
    private static function formatArgsForExpression(array $args): string
796
    {
797
        $mapping = [];
798
        foreach ($args as $name => $config) {
799
            $mapping[] = sprintf('%s: "%s"', $name, $config['type']);
800
        }
801
802
        return sprintf('arguments({%s}, args)', implode(', ', $mapping));
803
    }
804
805
    /**
806
     * Format a namespace to be used in an expression (double escape).
807
     */
808
    private static function formatNamespaceForExpression(string $namespace): string
809
    {
810
        return str_replace('\\', '\\\\', $namespace);
811
    }
812
813
    /**
814
     * Get the first metadata matching given class.
815
     *
816
     * @phpstan-template T of object
817
     * @phpstan-param class-string<T>|class-string<T>[] $metadataClasses
818
     * @phpstan-return T|null
819
     *
820
     * @return object|null
821
     */
822
    private static function getFirstMetadataMatching(array $metadatas, $metadataClasses)
823
    {
824
        $metas = self::getMetadataMatching($metadatas, $metadataClasses);
825
826
        return array_shift($metas);
827
    }
828
829
    /**
830
     * Return the metadata matching given class
831
     *
832
     * @phpstan-template T of object
833
     * @phpstan-param class-string<T>|class-string<T>[] $metadataClasses
834
     *
835
     * @return array
836
     */
837
    private static function getMetadataMatching(array $metadatas, $metadataClasses)
838
    {
839
        if (is_string($metadataClasses)) {
840
            $metadataClasses = [$metadataClasses];
841
        }
842
843
        return array_filter($metadatas, function ($metadata) use ($metadataClasses) {
844
            foreach ($metadataClasses as $metadataClass) {
845
                if ($metadata instanceof $metadataClass) {
846
                    return true;
847
                }
848
            }
849
850
            return false;
851
        });
852
    }
853
854
    /**
855
     * Format an expression (ie. add "@=" if not set).
856
     */
857
    private static function formatExpression(string $expression): string
858
    {
859
        return '@=' === substr($expression, 0, 2) ? $expression : sprintf('@=%s', $expression);
860
    }
861
862
    /**
863
     * Suffix a name if it is not already.
864
     */
865
    private static function suffixName(string $name, string $suffix): string
866
    {
867
        return substr($name, -strlen($suffix)) === $suffix ? $name : sprintf('%s%s', $name, $suffix);
868
    }
869
870
    /**
871
     * Try to guess a GraphQL type using configured type guessers
872
     *
873
     * @throws RuntimeException
874
     */
875
    private static function guessType(ReflectionClass $reflectionClass, Reflector $reflector, array $filterGraphQLTypes = []): string
876
    {
877
        $errors = [];
878
        foreach (self::$typeGuessers as $typeGuesser) {
879
            try {
880
                $type = $typeGuesser->guessType($reflectionClass, $reflector, $filterGraphQLTypes);
881
882
                return $type;
883
            } catch (TypeGuessingException $exception) {
884
                $errors[] = sprintf('[%s] %s', $typeGuesser->getName(), $exception->getMessage());
885
            }
886
        }
887
888
        throw new TypeGuessingException(join("\n", $errors));
889
    }
890
891
    /**
892
     * Transform a method arguments from reflection to a list of GraphQL argument.
893
     */
894
    private static function guessArgs(ReflectionClass $reflectionClass, ReflectionMethod $method): array
895
    {
896
        $arguments = [];
897
        foreach ($method->getParameters() as $index => $parameter) {
898
            try {
899
                $gqlType = self::guessType($reflectionClass, $parameter, self::VALID_INPUT_TYPES);
900
            } catch (TypeGuessingException $exception) {
901
                throw new InvalidArgumentException(sprintf('Argument n°%s "$%s" on method "%s" cannot be auto-guessed from the following type guessers:'."\n%s\n", $index + 1, $parameter->getName(), $method->getName(), $exception->getMessage()));
902
            }
903
904
            $argumentConfig = [];
905
            if ($parameter->isDefaultValueAvailable()) {
906
                $argumentConfig['defaultValue'] = $parameter->getDefaultValue();
907
            }
908
909
            $argumentConfig['type'] = $gqlType;
910
911
            $arguments[$parameter->getName()] = $argumentConfig;
912
        }
913
914
        return $arguments;
915
    }
916
917
    private static function getClassProperties(ReflectionClass $reflectionClass)
918
    {
919
        $properties = [];
920
        do {
921
            foreach ($reflectionClass->getProperties() as $property) {
922
                if (isset($properties[$property->getName()])) {
923
                    continue;
924
                }
925
                $properties[$property->getName()] = $property;
926
            }
927
        } while ($reflectionClass = $reflectionClass->getParentClass());
928
929
        return $properties;
930
    }
931
932
    protected static function formatMetadata(string $className): string
933
    {
934
        return sprintf(static::METADATA_FORMAT, str_replace(self::ANNOTATION_NAMESPACE, '', $className));
935
    }
936
937
    abstract protected static function getMetadatas(Reflector $reflector): array;
938
}
939