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
25:11
created

MetadataParser::unionMetadataToGQLConfiguration()   B

Complexity

Conditions 8
Paths 8

Size

Total Lines 38
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 8
eloc 26
c 1
b 0
f 0
nc 8
nop 2
dl 0
loc 38
rs 8.4444
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
            $reflectionClass = self::getClassReflection($className);
119
120
            foreach (static::getMetadatas($reflectionClass) as $classMetadata) {
121
                if ($classMetadata instanceof Meta) {
122
                    $gqlTypes = self::classMetadatasToGQLConfiguration(
123
                        $reflectionClass,
124
                        $classMetadata,
125
                        $configs,
126
                        $gqlTypes,
127
                        $preProcess
128
                    );
129
                }
130
            }
131
132
            return $preProcess ? self::$map->toArray() : $gqlTypes;
133
        } catch (\InvalidArgumentException $e) {
134
            throw new InvalidArgumentException(sprintf('Failed to parse GraphQL metadata from file "%s".', $file), $e->getCode(), $e);
135
        }
136
    }
137
138
    private static function classMetadatasToGQLConfiguration(
139
        ReflectionClass $reflectionClass,
140
        Meta $classMetadata,
141
        array $configs,
142
        array $gqlTypes,
143
        bool $preProcess
144
    ): array {
145
        $gqlConfiguration = $gqlType = $gqlName = null;
146
147
        switch (true) {
148
            case $classMetadata instanceof Metadata\Type:
149
                $gqlType = self::GQL_TYPE;
150
                $gqlName = $classMetadata->name ?? $reflectionClass->getShortName();
151
                if (!$preProcess) {
152
                    $gqlConfiguration = self::typeMetadataToGQLConfiguration($reflectionClass, $classMetadata, $gqlName, $configs);
153
154
                    if ($classMetadata instanceof Metadata\Relay\Connection) {
155
                        if (!$reflectionClass->implementsInterface(ConnectionInterface::class)) {
156
                            throw new InvalidArgumentException(sprintf('The metadata %s on class "%s" can only be used on class implementing the ConnectionInterface.', self::formatMetadata('Connection'), $reflectionClass->getName()));
157
                        }
158
159
                        if (!(isset($classMetadata->edge) xor isset($classMetadata->node))) {
160
                            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()));
161
                        }
162
163
                        $edgeType = $classMetadata->edge ?? false;
164
                        if (!$edgeType) {
165
                            $edgeType = $gqlName.'Edge';
166
                            $gqlTypes[$edgeType] = [
167
                                'type' => 'object',
168
                                'config' => [
169
                                    'builders' => [
170
                                        ['builder' => 'relay-edge', 'builderConfig' => ['nodeType' => $classMetadata->node]],
171
                                    ],
172
                                ],
173
                            ];
174
                        }
175
176
                        if (!isset($gqlConfiguration['config']['builders'])) {
177
                            $gqlConfiguration['config']['builders'] = [];
178
                        }
179
180
                        array_unshift($gqlConfiguration['config']['builders'], ['builder' => 'relay-connection', 'builderConfig' => ['edgeType' => $edgeType]]);
181
                    }
182
                }
183
                break;
184
185
            case $classMetadata instanceof Metadata\Input:
186
                $gqlType = self::GQL_INPUT;
187
                $gqlName = $classMetadata->name ?? self::suffixName($reflectionClass->getShortName(), 'Input');
188
                if (!$preProcess) {
189
                    $gqlConfiguration = self::inputMetadataToGQLConfiguration($reflectionClass, $classMetadata);
190
                }
191
                break;
192
193
            case $classMetadata instanceof Metadata\Scalar:
194
                $gqlType = self::GQL_SCALAR;
195
                if (!$preProcess) {
196
                    $gqlConfiguration = self::scalarMetadataToGQLConfiguration($reflectionClass, $classMetadata);
197
                }
198
                break;
199
200
            case $classMetadata instanceof Metadata\Enum:
201
                $gqlType = self::GQL_ENUM;
202
                if (!$preProcess) {
203
                    $gqlConfiguration = self::enumMetadataToGQLConfiguration($reflectionClass, $classMetadata);
204
                }
205
                break;
206
207
            case $classMetadata instanceof Metadata\Union:
208
                $gqlType = self::GQL_UNION;
209
                if (!$preProcess) {
210
                    $gqlConfiguration = self::unionMetadataToGQLConfiguration($reflectionClass, $classMetadata);
211
                }
212
                break;
213
214
            case $classMetadata instanceof Metadata\TypeInterface:
215
                $gqlType = self::GQL_INTERFACE;
216
                if (!$preProcess) {
217
                    $gqlConfiguration = self::typeInterfaceMetadataToGQLConfiguration($reflectionClass, $classMetadata);
218
                }
219
                break;
220
221
            case $classMetadata instanceof Metadata\Provider:
222
                if ($preProcess) {
223
                    self::$providers[] = ['reflectionClass' => $reflectionClass, 'metadata' => $classMetadata];
224
                }
225
226
                return [];
227
        }
228
229
        if (null !== $gqlType) {
230
            if (!$gqlName) {
231
                $gqlName = isset($classMetadata->name) ? $classMetadata->name : $reflectionClass->getShortName();
232
            }
233
234
            if ($preProcess) {
235
                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

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

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

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

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

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

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

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