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
40:20 queued 15:17
created

MetadataParser::graphQLTypeConfigFromAnnotation()   B

Complexity

Conditions 10
Paths 128

Size

Total Lines 55
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 10
eloc 34
c 1
b 0
f 0
nc 128
nop 3
dl 0
loc 55
rs 7.4333

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

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

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

241
                    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...
242
                }
243
                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

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

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

415
            $scalarConfiguration['scalarType'] = self::formatExpression(/** @scrutinizer ignore-type */ $scalarAnnotation->scalarType);
Loading history...
416
        } else {
417
            $scalarConfiguration = [
418
                'serialize' => [$reflectionClass->getName(), 'serialize'],
419
                'parseValue' => [$reflectionClass->getName(), 'parseValue'],
420
                'parseLiteral' => [$reflectionClass->getName(), 'parseLiteral'],
421
            ];
422
        }
423
424
        $scalarConfiguration = self::getDescriptionConfiguration(static::getMetadatas($reflectionClass)) + $scalarConfiguration;
425
426
        return ['type' => 'custom-scalar', 'config' => $scalarConfiguration];
427
    }
428
429
    /**
430
     * Get a GraphQL Enum configuration from given enum metadata.
431
     *
432
     * @return array{type: 'enum', config: array}
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{type: 'enum', config: array} at position 4 could not be parsed: Unknown type name ''enum'' at position 4 in array{type: 'enum', config: array}.
Loading history...
433
     */
434
    private static function enumMetadataToGQLConfiguration(ReflectionClass $reflectionClass, Metadata\Enum $enumMetadata): array
435
    {
436
        $metadatas = static::getMetadatas($reflectionClass);
437
        $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

437
        $enumValues = array_merge(self::getMetadataMatching($metadatas, Metadata\EnumValue::class), /** @scrutinizer ignore-deprecated */ $enumMetadata->values);
Loading history...
438
439
        $values = [];
440
441
        foreach ($reflectionClass->getConstants() as $name => $value) {
442
            $reflectionConstant = new ReflectionClassConstant($reflectionClass->getName(), $name);
443
            $valueConfig = self::getDescriptionConfiguration(static::getMetadatas($reflectionConstant), true);
444
445
            $enumValueAnnotation = current(array_filter($enumValues, fn ($enumValueAnnotation) => $enumValueAnnotation->name === $name));
446
            $valueConfig['value'] = $value;
447
448
            if (false !== $enumValueAnnotation) {
449
                if (isset($enumValueAnnotation->description)) {
450
                    $valueConfig['description'] = $enumValueAnnotation->description;
451
                }
452
453
                if (isset($enumValueAnnotation->deprecationReason)) {
454
                    $valueConfig['deprecationReason'] = $enumValueAnnotation->deprecationReason;
455
                }
456
            }
457
458
            $values[$name] = $valueConfig;
459
        }
460
461
        $enumConfiguration = ['values' => $values];
462
        $enumConfiguration = self::getDescriptionConfiguration(static::getMetadatas($reflectionClass)) + $enumConfiguration;
463
464
        return ['type' => 'enum', 'config' => $enumConfiguration];
465
    }
466
467
    /**
468
     * Get a GraphQL Union configuration from given union metadata.
469
     *
470
     * @return array{type: 'union', config: array}
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{type: 'union', config: array} at position 4 could not be parsed: Unknown type name ''union'' at position 4 in array{type: 'union', config: array}.
Loading history...
471
     */
472
    private static function unionMetadataToGQLConfiguration(ReflectionClass $reflectionClass, Metadata\Union $unionMetadata): array
473
    {
474
        $unionConfiguration = [];
475
        if (!empty($unionMetadata->types)) {
476
            $unionConfiguration['types'] = $unionMetadata->types;
477
        } else {
478
            $types = array_keys(self::$map->searchClassesMapBy(function ($gqlType, $configuration) use ($reflectionClass) {
479
                $typeClassName = $configuration['class'];
480
                $typeMetadata = self::getClassReflection($typeClassName);
481
482
                if ($reflectionClass->isInterface() && $typeMetadata->implementsInterface($reflectionClass->getName())) {
483
                    return true;
484
                }
485
486
                return $typeMetadata->isSubclassOf($reflectionClass->getName());
487
            }, self::GQL_TYPE));
488
            sort($types);
489
            $unionConfiguration['types'] = $types;
490
        }
491
492
        $unionConfiguration = self::getDescriptionConfiguration(static::getMetadatas($reflectionClass)) + $unionConfiguration;
493
494
        if (isset($unionMetadata->resolveType)) {
495
            $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

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