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 — 0.14 (#841)
by Jérémiah
02:54
created

MetadataParser::enumMetadataToGQLConfiguration()   A

Complexity

Conditions 5
Paths 6

Size

Total Lines 31
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 5

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 5
eloc 17
c 2
b 0
f 0
nc 6
nop 2
dl 0
loc 31
ccs 18
cts 18
cp 1
crap 5
rs 9.3888
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
    public const ANNOTATION_NAMESPACE = 'Overblog\GraphQLBundle\Annotation\\';
48
    public 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 52
    public static function preParse(SplFileInfo $file, ContainerBuilder $container, array $configs = []): void
75
    {
76 52
        $container->setParameter('overblog_graphql_types.classes_map', self::processFile($file, $container, $configs, true));
77 52
    }
78
79
    /**
80
     * @throws InvalidArgumentException
81
     * @throws ReflectionException
82
     */
83 52
    public static function parse(SplFileInfo $file, ContainerBuilder $container, array $configs = []): array
84
    {
85 52
        return self::processFile($file, $container, $configs, false);
86
    }
87
88
    /**
89
     * @internal
90
     */
91 92
    public static function reset(array $configs): void
92
    {
93 92
        self::$map = new ClassesTypesMap();
94 92
        self::$typeGuessers = [
95 92
            new DocBlockTypeGuesser(self::$map),
96 92
            new TypeHintTypeGuesser(self::$map),
97 92
            new DoctrineTypeGuesser(self::$map, $configs['doctrine']['types_mapping']),
98
        ];
99 92
        self::$providers = [];
100 92
        self::$reflections = [];
101 92
    }
102
103
    /**
104
     * Process a file.
105
     *
106
     * @throws InvalidArgumentException|ReflectionException|AnnotationException
107
     */
108 52
    private static function processFile(SplFileInfo $file, ContainerBuilder $container, array $configs, bool $preProcess): array
109
    {
110 52
        $container->addResource(new FileResource($file->getRealPath()));
111
112
        try {
113 52
            $className = $file->getBasename('.php');
114 52
            if (preg_match('#namespace (.+);#', file_get_contents($file->getRealPath()), $matches)) {
115 52
                $className = trim($matches[1]).'\\'.$className;
116
            }
117
118 52
            $gqlTypes = [];
119
            /** @phpstan-ignore-next-line */
120 52
            $reflectionClass = self::getClassReflection($className);
121
122 52
            foreach (static::getMetadatas($reflectionClass) as $classMetadata) {
123 52
                if ($classMetadata instanceof Meta) {
124 52
                    $gqlTypes = self::classMetadatasToGQLConfiguration(
125 52
                        $reflectionClass,
126
                        $classMetadata,
127
                        $configs,
128
                        $gqlTypes,
129
                        $preProcess
130
                    );
131
                }
132
            }
133
134 52
            return $preProcess ? self::$map->toArray() : $gqlTypes;
135 20
        } catch (\InvalidArgumentException $e) {
136 20
            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 52
    private static function classMetadatasToGQLConfiguration(
144
        ReflectionClass $reflectionClass,
145
        Meta $classMetadata,
146
        array $configs,
147
        array $gqlTypes,
148
        bool $preProcess
149
    ): array {
150 52
        $gqlConfiguration = $gqlType = $gqlName = null;
151
152
        switch (true) {
153 52
            case $classMetadata instanceof Metadata\Type:
154 52
                $gqlType = self::GQL_TYPE;
155 52
                $gqlName = $classMetadata->name ?? $reflectionClass->getShortName();
156 52
                if (!$preProcess) {
157 52
                    $gqlConfiguration = self::typeMetadataToGQLConfiguration($reflectionClass, $classMetadata, $gqlName, $configs);
158
159 52
                    if ($classMetadata instanceof Metadata\Relay\Connection) {
160 51
                        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 51
                        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 51
                        $edgeType = $classMetadata->edge ?? false;
169 51
                        if (!$edgeType) {
170 51
                            $edgeType = $gqlName.'Edge';
171 51
                            $gqlTypes[$edgeType] = [
172 51
                                'type' => 'object',
173
                                'config' => [
174
                                    'builders' => [
175 51
                                        ['builder' => 'relay-edge', 'builderConfig' => ['nodeType' => $classMetadata->node]],
176
                                    ],
177
                                ],
178
                            ];
179
                        }
180
181 51
                        if (!isset($gqlConfiguration['config']['builders'])) {
182 51
                            $gqlConfiguration['config']['builders'] = [];
183
                        }
184
185 51
                        array_unshift($gqlConfiguration['config']['builders'], ['builder' => 'relay-connection', 'builderConfig' => ['edgeType' => $edgeType]]);
186
                    }
187
                }
188 52
                break;
189
190 51
            case $classMetadata instanceof Metadata\Input:
191 51
                $gqlType = self::GQL_INPUT;
192 51
                $gqlName = $classMetadata->name ?? self::suffixName($reflectionClass->getShortName(), 'Input');
193 51
                if (!$preProcess) {
194 51
                    $gqlConfiguration = self::inputMetadataToGQLConfiguration($reflectionClass, $classMetadata);
195
                }
196 51
                break;
197
198 51
            case $classMetadata instanceof Metadata\Scalar:
199 51
                $gqlType = self::GQL_SCALAR;
200 51
                if (!$preProcess) {
201 51
                    $gqlConfiguration = self::scalarMetadataToGQLConfiguration($reflectionClass, $classMetadata);
202
                }
203 51
                break;
204
205 51
            case $classMetadata instanceof Metadata\Enum:
206 51
                $gqlType = self::GQL_ENUM;
207 51
                if (!$preProcess) {
208 51
                    $gqlConfiguration = self::enumMetadataToGQLConfiguration($reflectionClass, $classMetadata);
209
                }
210 51
                break;
211
212 51
            case $classMetadata instanceof Metadata\Union:
213 51
                $gqlType = self::GQL_UNION;
214 51
                if (!$preProcess) {
215 51
                    $gqlConfiguration = self::unionMetadataToGQLConfiguration($reflectionClass, $classMetadata);
216
                }
217 51
                break;
218
219 51
            case $classMetadata instanceof Metadata\TypeInterface:
220 51
                $gqlType = self::GQL_INTERFACE;
221 51
                if (!$preProcess) {
222 51
                    $gqlConfiguration = self::typeInterfaceMetadataToGQLConfiguration($reflectionClass, $classMetadata);
223
                }
224 51
                break;
225
226 51
            case $classMetadata instanceof Metadata\Provider:
227 51
                if ($preProcess) {
228 51
                    self::$providers[] = ['reflectionClass' => $reflectionClass, 'metadata' => $classMetadata];
229
                }
230
231 51
                return [];
232
        }
233
234 52
        if (null !== $gqlType) {
235 52
            if (!$gqlName) {
236 51
                $gqlName = !empty($classMetadata->name) ? $classMetadata->name : $reflectionClass->getShortName();
237
            }
238
239 52
            if ($preProcess) {
240 52
                if (self::$map->hasType($gqlName)) {
241 2
                    throw new InvalidArgumentException(sprintf('The GraphQL type "%s" has already been registered in class "%s"', $gqlName, self::$map->getType($gqlName)['class']));
242
                }
243 52
                self::$map->addClassType($gqlName, $reflectionClass->getName(), $gqlType);
244
            } else {
245 52
                $gqlTypes = [$gqlName => $gqlConfiguration] + $gqlTypes;
246
            }
247
        }
248
249 52
        return $gqlTypes;
250
    }
251
252
    /**
253
     * @throws ReflectionException
254
     * @phpstan-param class-string $className
255
     */
256 52
    private static function getClassReflection(string $className): ReflectionClass
257
    {
258 52
        self::$reflections[$className] ??= new ReflectionClass($className);
259
260 52
        return self::$reflections[$className];
261
    }
262
263 52
    private static function typeMetadataToGQLConfiguration(
264
        ReflectionClass $reflectionClass,
265
        Metadata\Type $classMetadata,
266
        string $gqlName,
267
        array $configs
268
    ): array {
269 52
        $isMutation = $isDefault = $isRoot = false;
270 52
        if (isset($configs['definitions']['schema'])) {
271 51
            $defaultSchemaName = isset($configs['definitions']['schema']['default']) ? 'default' : array_key_first($configs['definitions']['schema']);
272 51
            foreach ($configs['definitions']['schema'] as $schemaName => $schema) {
273 51
                $schemaQuery = $schema['query'] ?? null;
274 51
                $schemaMutation = $schema['mutation'] ?? null;
275
276 51
                if ($gqlName === $schemaQuery) {
277 51
                    $isRoot = true;
278 51
                    if ($defaultSchemaName === $schemaName) {
279 51
                        $isDefault = true;
280
                    }
281 51
                } elseif ($gqlName === $schemaMutation) {
282 51
                    $isMutation = true;
283 51
                    $isRoot = true;
284 51
                    if ($defaultSchemaName === $schemaName) {
285 51
                        $isDefault = true;
286
                    }
287
                }
288
            }
289
        }
290
291 52
        $currentValue = $isRoot ? sprintf("service('%s')", self::formatNamespaceForExpression($reflectionClass->getName())) : 'value';
292
293 52
        $gqlConfiguration = self::graphQLTypeConfigFromAnnotation($reflectionClass, $classMetadata, $currentValue);
294
295 52
        $providerFields = self::getGraphQLFieldsFromProviders($reflectionClass, $isMutation ? Metadata\Mutation::class : Metadata\Query::class, $gqlName, $isDefault);
296 52
        $gqlConfiguration['config']['fields'] = array_merge($gqlConfiguration['config']['fields'], $providerFields);
297
298 52
        if ($classMetadata instanceof Metadata\Relay\Edge) {
299 51
            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 51
            if (!isset($gqlConfiguration['config']['builders'])) {
303 51
                $gqlConfiguration['config']['builders'] = [];
304
            }
305 51
            array_unshift($gqlConfiguration['config']['builders'], ['builder' => 'relay-edge', 'builderConfig' => ['nodeType' => $classMetadata->node]]);
306
        }
307
308 52
        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 52
    private static function graphQLTypeConfigFromAnnotation(ReflectionClass $reflectionClass, Metadata\Type $typeAnnotation, string $currentValue): array
315
    {
316 52
        $typeConfiguration = [];
317 52
        $metadatas = static::getMetadatas($reflectionClass);
318
319 52
        $fieldsFromProperties = self::getGraphQLTypeFieldsFromAnnotations($reflectionClass, self::getClassProperties($reflectionClass), Metadata\Field::class, $currentValue);
320 52
        $fieldsFromMethods = self::getGraphQLTypeFieldsFromAnnotations($reflectionClass, $reflectionClass->getMethods(), Metadata\Field::class, $currentValue);
321
322 52
        $typeConfiguration['fields'] = array_merge($fieldsFromProperties, $fieldsFromMethods);
323 52
        $typeConfiguration = self::getDescriptionConfiguration($metadatas) + $typeConfiguration;
324
325 52
        if (!empty($typeAnnotation->interfaces)) {
326 51
            $typeConfiguration['interfaces'] = $typeAnnotation->interfaces;
327
        } else {
328 52
            $interfaces = array_keys(self::$map->searchClassesMapBy(function ($gqlType, $configuration) use ($reflectionClass) {
329 51
                ['class' => $interfaceClassName] = $configuration;
330
331 51
                $interfaceMetadata = self::getClassReflection($interfaceClassName);
332 51
                if ($interfaceMetadata->isInterface() && $reflectionClass->implementsInterface($interfaceMetadata->getName())) {
333 51
                    return true;
334
                }
335
336 51
                return $reflectionClass->isSubclassOf($interfaceClassName);
337 52
            }, self::GQL_INTERFACE));
338
339 52
            sort($interfaces);
340 52
            $typeConfiguration['interfaces'] = $interfaces;
341
        }
342
343 52
        if (isset($typeAnnotation->resolveField)) {
344 51
            $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 52
        $buildersAnnotations = array_merge(self::getMetadataMatching($metadatas, Metadata\FieldsBuilder::class), $typeAnnotation->builders);
348 52
        if (!empty($buildersAnnotations)) {
349 51
            $typeConfiguration['builders'] = array_map(fn ($fieldsBuilderAnnotation) => ['builder' => $fieldsBuilderAnnotation->name, 'builderConfig' => $fieldsBuilderAnnotation->config], $buildersAnnotations);
350
        }
351
352 52
        if (isset($typeAnnotation->isTypeOf)) {
353 51
            $typeConfiguration['isTypeOf'] = $typeAnnotation->isTypeOf;
354
        }
355
356 52
        $publicMetadata = self::getFirstMetadataMatching($metadatas, Metadata\IsPublic::class);
357 52
        if (null !== $publicMetadata) {
358 51
            $typeConfiguration['fieldsDefaultPublic'] = self::formatExpression($publicMetadata->value);
359
        }
360
361 52
        $accessMetadata = self::getFirstMetadataMatching($metadatas, Metadata\Access::class);
362 52
        if (null !== $accessMetadata) {
363 51
            $typeConfiguration['fieldsDefaultAccess'] = self::formatExpression($accessMetadata->value);
364
        }
365
366 52
        return ['type' => $typeAnnotation->isRelay ? 'relay-mutation-payload' : 'object', 'config' => $typeConfiguration];
367
    }
368
369
    /**
370
     * Create a GraphQL Interface type configuration from metadatas on properties.
371
     *
372
     * @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...
373
     */
374 51
    private static function typeInterfaceMetadataToGQLConfiguration(ReflectionClass $reflectionClass, Metadata\TypeInterface $interfaceAnnotation): array
375
    {
376 51
        $interfaceConfiguration = [];
377
378 51
        $fieldsFromProperties = self::getGraphQLTypeFieldsFromAnnotations($reflectionClass, self::getClassProperties($reflectionClass));
379 51
        $fieldsFromMethods = self::getGraphQLTypeFieldsFromAnnotations($reflectionClass, $reflectionClass->getMethods());
380
381 51
        $interfaceConfiguration['fields'] = array_merge($fieldsFromProperties, $fieldsFromMethods);
382 51
        $interfaceConfiguration = self::getDescriptionConfiguration(static::getMetadatas($reflectionClass)) + $interfaceConfiguration;
383
384 51
        $interfaceConfiguration['resolveType'] = self::formatExpression($interfaceAnnotation->resolveType);
385
386 51
        return ['type' => 'interface', 'config' => $interfaceConfiguration];
387
    }
388
389
    /**
390
     * Create a GraphQL Input type configuration from metadatas on properties.
391
     *
392
     * @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...
393
     */
394 51
    private static function inputMetadataToGQLConfiguration(ReflectionClass $reflectionClass, Metadata\Input $inputAnnotation): array
395
    {
396 51
        $inputConfiguration = array_merge([
397 51
            'fields' => self::getGraphQLInputFieldsFromMetadatas($reflectionClass, self::getClassProperties($reflectionClass)),
398 51
        ], self::getDescriptionConfiguration(static::getMetadatas($reflectionClass)));
399
400 51
        return ['type' => $inputAnnotation->isRelay ? 'relay-mutation-input' : 'input-object', 'config' => $inputConfiguration];
401
    }
402
403
    /**
404
     * Get a GraphQL scalar configuration from given scalar metadata.
405
     *
406
     * @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...
407
     */
408 51
    private static function scalarMetadataToGQLConfiguration(ReflectionClass $reflectionClass, Metadata\Scalar $scalarAnnotation): array
409
    {
410 51
        $scalarConfiguration = [];
411
412 51
        if (isset($scalarAnnotation->scalarType)) {
413 51
            $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

413
            $scalarConfiguration['scalarType'] = self::formatExpression(/** @scrutinizer ignore-type */ $scalarAnnotation->scalarType);
Loading history...
414
        } else {
415 51
            $scalarConfiguration = [
416 51
                'serialize' => [$reflectionClass->getName(), 'serialize'],
417 51
                'parseValue' => [$reflectionClass->getName(), 'parseValue'],
418 51
                'parseLiteral' => [$reflectionClass->getName(), 'parseLiteral'],
419
            ];
420
        }
421
422 51
        $scalarConfiguration = self::getDescriptionConfiguration(static::getMetadatas($reflectionClass)) + $scalarConfiguration;
423
424 51
        return ['type' => 'custom-scalar', 'config' => $scalarConfiguration];
425
    }
426
427
    /**
428
     * Get a GraphQL Enum configuration from given enum metadata.
429
     *
430
     * @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...
431
     */
432 51
    private static function enumMetadataToGQLConfiguration(ReflectionClass $reflectionClass, Metadata\Enum $enumMetadata): array
433
    {
434 51
        $metadatas = static::getMetadatas($reflectionClass);
435 51
        $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

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

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