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
22:13
created

MetadataParser::scalarMetadataToGQLConfiguration()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

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