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 Timur
20:26
created

MetadataParser::typeMetadataToGQLConfiguration()   C

Complexity

Conditions 13
Paths 24

Size

Total Lines 46
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 13
eloc 26
c 1
b 0
f 0
nc 24
nop 4
dl 0
loc 46
rs 6.6166

How to fix   Complexity   

Long Method

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

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

Commonly applied refactorings include:

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

236
                if (self::$map->hasType(/** @scrutinizer ignore-type */ $gqlName)) {
Loading history...
237
                    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

237
                    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...
238
                }
239
                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

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

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

402
            $scalarConfiguration['scalarType'] = self::formatExpression(/** @scrutinizer ignore-type */ $scalarAnnotation->scalarType);
Loading history...
403
        } else {
404
            $scalarConfiguration = [
405
                'serialize' => [$reflectionClass->getName(), 'serialize'],
406
                'parseValue' => [$reflectionClass->getName(), 'parseValue'],
407
                'parseLiteral' => [$reflectionClass->getName(), 'parseLiteral'],
408
            ];
409
        }
410
411
        $scalarConfiguration = self::getDescriptionConfiguration(static::getMetadatas($reflectionClass)) + $scalarConfiguration;
412
413
        return ['type' => 'custom-scalar', 'config' => $scalarConfiguration];
414
    }
415
416
    /**
417
     * Get a GraphQL Enum configuration from given enum metadata.
418
     */
419
    private static function enumMetadataToGQLConfiguration(ReflectionClass $reflectionClass, Metadata\Enum $enumMetadata): array
420
    {
421
        $metadatas = static::getMetadatas($reflectionClass);
422
        $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

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

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