• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

overblog / GraphQLBundle / 8491707837

30 Mar 2024 03:26PM UTC coverage: 98.361% (-0.05%) from 98.411%
8491707837

push

github

web-flow
Merge pull request #1172 from sparklink-pro/master

Cleanup Input default value and Fix #1171

53 of 56 new or added lines in 7 files covered. (94.64%)

1 existing line in 1 file now uncovered.

4321 of 4393 relevant lines covered (98.36%)

39.59 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.56
/src/Config/Parser/MetadataParser/MetadataParser.php
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\Annotation\InputField;
11
use Overblog\GraphQLBundle\Config\Parser\MetadataParser\TypeGuesser\DocBlockTypeGuesser;
12
use Overblog\GraphQLBundle\Config\Parser\MetadataParser\TypeGuesser\DoctrineTypeGuesser;
13
use Overblog\GraphQLBundle\Config\Parser\MetadataParser\TypeGuesser\TypeGuessingException;
14
use Overblog\GraphQLBundle\Config\Parser\MetadataParser\TypeGuesser\TypeHintTypeGuesser;
15
use Overblog\GraphQLBundle\Config\Parser\PreParserInterface;
16
use Overblog\GraphQLBundle\Relay\Connection\ConnectionInterface;
17
use Overblog\GraphQLBundle\Relay\Connection\EdgeInterface;
18
use ReflectionClass;
19
use ReflectionClassConstant;
20
use ReflectionException;
21
use ReflectionMethod;
22
use ReflectionProperty;
23
use Reflector;
24
use RuntimeException;
25
use SplFileInfo;
26
use Symfony\Component\Config\Resource\FileResource;
27
use Symfony\Component\DependencyInjection\ContainerBuilder;
28
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
29

30
use function array_filter;
31
use function array_keys;
32
use function array_map;
33
use function array_unshift;
34
use function current;
35
use function file_get_contents;
36
use function implode;
37
use function in_array;
38
use function is_string;
39
use function preg_match;
40
use function sprintf;
41
use function str_replace;
42
use function strlen;
43
use function substr;
44
use function trim;
45

46
use const PHP_VERSION_ID;
47

48
abstract class MetadataParser implements PreParserInterface
49
{
50
    public const ANNOTATION_NAMESPACE = 'Overblog\GraphQLBundle\Annotation\\';
51
    public const METADATA_FORMAT = '%s';
52

53
    private static ClassesTypesMap $map;
54
    private static array $typeGuessers = [];
55
    private static array $providers = [];
56
    private static array $reflections = [];
57

58
    private const GQL_SCALAR = 'scalar';
59
    private const GQL_ENUM = 'enum';
60
    private const GQL_TYPE = 'type';
61
    private const GQL_INPUT = 'input';
62
    private const GQL_UNION = 'union';
63
    private const GQL_INTERFACE = 'interface';
64

65
    /**
66
     * @see https://facebook.github.io/graphql/draft/#sec-Input-and-Output-Types
67
     */
68
    private const VALID_INPUT_TYPES = [self::GQL_SCALAR, self::GQL_ENUM, self::GQL_INPUT];
69
    private const VALID_OUTPUT_TYPES = [self::GQL_SCALAR, self::GQL_TYPE, self::GQL_INTERFACE, self::GQL_UNION, self::GQL_ENUM];
70

71
    /**
72
     * {@inheritdoc}
73
     *
74
     * @throws InvalidArgumentException
75
     * @throws ReflectionException
76
     */
77
    public static function preParse(SplFileInfo $file, ContainerBuilder $container, array $configs = []): void
78
    {
79
        $container->setParameter('overblog_graphql_types.classes_map', self::processFile($file, $container, $configs, true));
52✔
80
    }
52✔
81

82
    /**
83
     * @throws InvalidArgumentException
84
     * @throws ReflectionException
85
     */
86
    public static function parse(SplFileInfo $file, ContainerBuilder $container, array $configs = []): array
87
    {
88
        return self::processFile($file, $container, $configs, false);
52✔
89
    }
90

91
    public static function finalize(ContainerBuilder $container): void
92
    {
93
        $parameter = 'overblog_graphql_types.interfaces_map';
43✔
94
        $value = $container->hasParameter($parameter) ? $container->getParameter($parameter) : [];
43✔
95
        foreach (self::$map->interfacesToArray() as $interface => $types) {
43✔
96
            /** @phpstan-ignore-next-line */
97
            if (!isset($value[$interface])) {
1✔
98
                /** @phpstan-ignore-next-line */
99
                $value[$interface] = [];
1✔
100
            }
101
            foreach ($types as $className => $typeName) {
1✔
102
                $value[$interface][$className] = $typeName;
1✔
103
            }
104
        }
105

106
        $container->setParameter('overblog_graphql_types.interfaces_map', $value);
43✔
107
    }
43✔
108

109
    /**
110
     * @internal
111
     */
112
    public static function reset(array $configs): void
113
    {
114
        self::$map = new ClassesTypesMap();
94✔
115
        self::$typeGuessers = [
94✔
116
            new DocBlockTypeGuesser(self::$map),
94✔
117
            new TypeHintTypeGuesser(self::$map),
94✔
118
            new DoctrineTypeGuesser(self::$map, $configs['doctrine']['types_mapping']),
94✔
119
        ];
120
        self::$providers = [];
94✔
121
        self::$reflections = [];
94✔
122
    }
94✔
123

124
    /**
125
     * Process a file.
126
     *
127
     * @throws InvalidArgumentException|ReflectionException|AnnotationException
128
     */
129
    private static function processFile(SplFileInfo $file, ContainerBuilder $container, array $configs, bool $preProcess): array
130
    {
131
        $container->addResource(new FileResource($file->getRealPath()));
52✔
132

133
        try {
134
            $className = $file->getBasename('.php');
52✔
135
            if (preg_match('#namespace (.+);#', file_get_contents($file->getRealPath()), $matches)) {
52✔
136
                $className = trim($matches[1]).'\\'.$className;
52✔
137
            }
138

139
            $gqlTypes = [];
52✔
140
            /** @phpstan-ignore-next-line */
141
            $reflectionClass = self::getClassReflection($className);
52✔
142

143
            foreach (static::getMetadatas($reflectionClass) as $classMetadata) {
52✔
144
                if ($classMetadata instanceof Meta) {
52✔
145
                    $gqlTypes = self::classMetadatasToGQLConfiguration(
52✔
146
                        $reflectionClass,
52✔
147
                        $classMetadata,
148
                        $configs,
149
                        $gqlTypes,
150
                        $preProcess
151
                    );
152
                }
153
            }
154

155
            return $preProcess ? self::$map->classesToArray() : $gqlTypes;
52✔
156
        } catch (ReflectionException $e) {
22✔
157
            return $gqlTypes;
2✔
158
        } catch (\InvalidArgumentException $e) {
20✔
159
            throw new InvalidArgumentException(sprintf('Failed to parse GraphQL metadata from file "%s".', $file), $e->getCode(), $e);
20✔
160
        }
161
    }
162

163
    /**
164
     * @return array<string,array>
165
     */
166
    private static function classMetadatasToGQLConfiguration(
167
        ReflectionClass $reflectionClass,
168
        Meta $classMetadata,
169
        array $configs,
170
        array $gqlTypes,
171
        bool $preProcess
172
    ): array {
173
        $gqlConfiguration = $gqlType = $gqlName = null;
52✔
174

175
        switch (true) {
176
            case $classMetadata instanceof Metadata\Type:
52✔
177
                $gqlType = self::GQL_TYPE;
52✔
178
                $gqlName = $classMetadata->name ?? $reflectionClass->getShortName();
52✔
179
                if (!$preProcess) {
52✔
180
                    $gqlConfiguration = self::typeMetadataToGQLConfiguration($reflectionClass, $classMetadata, $gqlName, $configs);
52✔
181

182
                    if ($classMetadata instanceof Metadata\Relay\Connection) {
52✔
183
                        if (!$reflectionClass->implementsInterface(ConnectionInterface::class)) {
50✔
184
                            throw new InvalidArgumentException(sprintf('The metadata %s on class "%s" can only be used on class implementing the ConnectionInterface.', self::formatMetadata('Connection'), $reflectionClass->getName()));
×
185
                        }
186

187
                        if (!(isset($classMetadata->edge) xor isset($classMetadata->node))) {
50✔
188
                            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()));
×
189
                        }
190

191
                        $edgeType = $classMetadata->edge ?? false;
50✔
192
                        if (!$edgeType) {
50✔
193
                            $edgeType = $gqlName.'Edge';
50✔
194
                            $gqlTypes[$edgeType] = [
50✔
195
                                'type' => 'object',
50✔
196
                                'config' => [
197
                                    'builders' => [
198
                                        ['builder' => 'relay-edge', 'builderConfig' => ['nodeType' => $classMetadata->node]],
50✔
199
                                    ],
200
                                ],
201
                            ];
202
                        }
203

204
                        if (!isset($gqlConfiguration['config']['builders'])) {
50✔
205
                            $gqlConfiguration['config']['builders'] = [];
50✔
206
                        }
207

208
                        array_unshift($gqlConfiguration['config']['builders'], ['builder' => 'relay-connection', 'builderConfig' => ['edgeType' => $edgeType]]);
50✔
209
                    }
210

211
                    $interfaces = $gqlConfiguration['config']['interfaces'] ?? [];
52✔
212
                    foreach ($interfaces as $interface) {
52✔
213
                        self::$map->addInterfaceType($interface, $gqlName, $reflectionClass->getName());
51✔
214
                    }
215
                }
216
                break;
52✔
217

218
            case $classMetadata instanceof Metadata\Input:
51✔
219
                $gqlType = self::GQL_INPUT;
50✔
220
                $gqlName = $classMetadata->name ?? self::suffixName($reflectionClass->getShortName(), 'Input');
50✔
221
                if (!$preProcess) {
50✔
222
                    $gqlConfiguration = self::inputMetadataToGQLConfiguration($reflectionClass, $classMetadata);
50✔
223
                }
224
                break;
50✔
225

226
            case $classMetadata instanceof Metadata\Scalar:
51✔
227
                $gqlType = self::GQL_SCALAR;
50✔
228
                if (!$preProcess) {
50✔
229
                    $gqlConfiguration = self::scalarMetadataToGQLConfiguration($reflectionClass, $classMetadata);
50✔
230
                }
231
                break;
50✔
232

233
            case $classMetadata instanceof Metadata\Enum:
51✔
234
                $gqlType = self::GQL_ENUM;
50✔
235
                if (!$preProcess) {
50✔
236
                    $gqlConfiguration = self::enumMetadataToGQLConfiguration($reflectionClass, $classMetadata);
50✔
237
                }
238
                break;
50✔
239

240
            case $classMetadata instanceof Metadata\Union:
51✔
241
                $gqlType = self::GQL_UNION;
50✔
242
                if (!$preProcess) {
50✔
243
                    $gqlConfiguration = self::unionMetadataToGQLConfiguration($reflectionClass, $classMetadata);
50✔
244
                }
245
                break;
50✔
246

247
            case $classMetadata instanceof Metadata\TypeInterface:
51✔
248
                $gqlType = self::GQL_INTERFACE;
51✔
249
                if (!$preProcess) {
51✔
250
                    $gqlName = !empty($classMetadata->name) ? $classMetadata->name : $reflectionClass->getShortName();
51✔
251
                    $gqlConfiguration = self::typeInterfaceMetadataToGQLConfiguration($reflectionClass, $classMetadata, $gqlName);
51✔
252
                }
253
                break;
51✔
254

255
            case $classMetadata instanceof Metadata\Provider:
51✔
256
                if ($preProcess) {
51✔
257
                    self::$providers[] = ['reflectionClass' => $reflectionClass, 'metadata' => $classMetadata];
51✔
258
                }
259

260
                return [];
51✔
261
        }
262

263
        if (null !== $gqlType) {
52✔
264
            if (!$gqlName) {
52✔
265
                $gqlName = !empty($classMetadata->name) ? $classMetadata->name : $reflectionClass->getShortName();
51✔
266
            }
267

268
            if ($preProcess) {
52✔
269
                if (self::$map->hasType($gqlName)) {
52✔
270
                    throw new InvalidArgumentException(sprintf('The GraphQL type "%s" has already been registered in class "%s"', $gqlName, self::$map->getType($gqlName)['class']));
2✔
271
                }
272
                self::$map->addClassType($gqlName, $reflectionClass->getName(), $gqlType);
52✔
273
            } else {
274
                $gqlTypes = [$gqlName => $gqlConfiguration] + $gqlTypes;
52✔
275
            }
276
        }
277

278
        return $gqlTypes;
52✔
279
    }
280

281
    /**
282
     * @throws ReflectionException
283
     *
284
     * @phpstan-param class-string $className
285
     */
286
    private static function getClassReflection(string $className): ReflectionClass
287
    {
288
        self::$reflections[$className] ??= new ReflectionClass($className);
52✔
289

290
        return self::$reflections[$className];
52✔
291
    }
292

293
    private static function typeMetadataToGQLConfiguration(
294
        ReflectionClass $reflectionClass,
295
        Metadata\Type $classMetadata,
296
        string $gqlName,
297
        array $configs
298
    ): array {
299
        $isMutation = $isDefault = $isRoot = false;
52✔
300
        if (isset($configs['definitions']['schema'])) {
52✔
301
            $defaultSchemaName = isset($configs['definitions']['schema']['default']) ? 'default' : array_key_first($configs['definitions']['schema']);
51✔
302
            foreach ($configs['definitions']['schema'] as $schemaName => $schema) {
51✔
303
                $schemaQuery = $schema['query'] ?? null;
51✔
304
                $schemaMutation = $schema['mutation'] ?? null;
51✔
305
                $schemaSubscription = $schema['subscription'] ?? null;
51✔
306

307
                if ($gqlName === $schemaQuery) {
51✔
308
                    $isRoot = true;
51✔
309
                    if ($defaultSchemaName === $schemaName) {
51✔
310
                        $isDefault = true;
51✔
311
                    }
312
                } elseif ($gqlName === $schemaMutation) {
51✔
313
                    $isMutation = true;
51✔
314
                    $isRoot = true;
51✔
315
                    if ($defaultSchemaName === $schemaName) {
51✔
316
                        $isDefault = true;
51✔
317
                    }
318
                } elseif ($gqlName === $schemaSubscription) {
51✔
319
                    $isRoot = true;
×
320
                }
321
            }
322
        }
323

324
        $currentValue = $isRoot ? sprintf("service('%s')", self::formatNamespaceForExpression($reflectionClass->getName())) : 'value';
52✔
325

326
        $gqlConfiguration = self::graphQLTypeConfigFromAnnotation($reflectionClass, $classMetadata, $currentValue);
52✔
327

328
        $providerFields = self::getGraphQLFieldsFromProviders($reflectionClass, $isMutation ? Metadata\Mutation::class : Metadata\Query::class, $gqlName, $isDefault);
52✔
329
        $gqlConfiguration['config']['fields'] = array_merge($gqlConfiguration['config']['fields'], $providerFields);
52✔
330

331
        if ($classMetadata instanceof Metadata\Relay\Edge) {
52✔
332
            if (!$reflectionClass->implementsInterface(EdgeInterface::class)) {
50✔
333
                throw new InvalidArgumentException(sprintf('The metadata %s on class "%s" can only be used on class implementing the EdgeInterface.', self::formatMetadata('Edge'), $reflectionClass->getName()));
×
334
            }
335
            if (!isset($gqlConfiguration['config']['builders'])) {
50✔
336
                $gqlConfiguration['config']['builders'] = [];
50✔
337
            }
338
            array_unshift($gqlConfiguration['config']['builders'], ['builder' => 'relay-edge', 'builderConfig' => ['nodeType' => $classMetadata->node]]);
50✔
339
        }
340

341
        return $gqlConfiguration;
52✔
342
    }
343

344
    /**
345
     * @return array{type: 'relay-mutation-payload'|'object', config: array}
346
     */
347
    private static function graphQLTypeConfigFromAnnotation(ReflectionClass $reflectionClass, Metadata\Type $typeAnnotation, string $currentValue): array
348
    {
349
        $typeConfiguration = [];
52✔
350
        $metadatas = static::getMetadatas($reflectionClass);
52✔
351

352
        $fieldsFromProperties = self::getGraphQLTypeFieldsFromAnnotations($reflectionClass, self::getClassProperties($reflectionClass), Metadata\Field::class, $currentValue);
52✔
353
        $fieldsFromMethods = self::getGraphQLTypeFieldsFromAnnotations($reflectionClass, $reflectionClass->getMethods(), Metadata\Field::class, $currentValue);
52✔
354

355
        $typeConfiguration['fields'] = array_merge($fieldsFromProperties, $fieldsFromMethods);
52✔
356
        $typeConfiguration = self::getDescriptionConfiguration($metadatas) + $typeConfiguration;
52✔
357

358
        if (!empty($typeAnnotation->interfaces)) {
52✔
359
            $typeConfiguration['interfaces'] = $typeAnnotation->interfaces;
50✔
360
        } else {
361
            $interfaces = array_keys(self::$map->searchClassesMapBy(function ($gqlType, $configuration) use ($reflectionClass) {
52✔
362
                ['class' => $interfaceClassName] = $configuration;
51✔
363

364
                $interfaceMetadata = self::getClassReflection($interfaceClassName);
51✔
365
                if ($interfaceMetadata->isInterface() && $reflectionClass->implementsInterface($interfaceMetadata->getName())) {
51✔
366
                    return true;
50✔
367
                }
368

369
                return $reflectionClass->isSubclassOf($interfaceClassName);
51✔
370
            }, self::GQL_INTERFACE));
52✔
371

372
            sort($interfaces);
52✔
373
            $typeConfiguration['interfaces'] = $interfaces;
52✔
374
        }
375

376
        if (isset($typeAnnotation->resolveField)) {
52✔
377
            $typeConfiguration['resolveField'] = self::formatExpression($typeAnnotation->resolveField);
50✔
378
        }
379

380
        $buildersAnnotations = self::getMetadataMatching($metadatas, Metadata\FieldsBuilder::class);
52✔
381
        if (!empty($buildersAnnotations)) {
52✔
382
            $typeConfiguration['builders'] = array_map(fn ($fieldsBuilderAnnotation) => ['builder' => $fieldsBuilderAnnotation->name, 'builderConfig' => $fieldsBuilderAnnotation->config], $buildersAnnotations);
50✔
383
        }
384

385
        if (isset($typeAnnotation->isTypeOf)) {
52✔
386
            $typeConfiguration['isTypeOf'] = $typeAnnotation->isTypeOf;
50✔
387
        }
388

389
        $publicMetadata = self::getFirstMetadataMatching($metadatas, Metadata\IsPublic::class);
52✔
390
        if (null !== $publicMetadata) {
52✔
391
            $typeConfiguration['fieldsDefaultPublic'] = self::formatExpression($publicMetadata->value);
50✔
392
        }
393

394
        $accessMetadata = self::getFirstMetadataMatching($metadatas, Metadata\Access::class);
52✔
395
        if (null !== $accessMetadata) {
52✔
396
            $typeConfiguration['fieldsDefaultAccess'] = self::formatExpression($accessMetadata->value);
50✔
397
        }
398

399
        return ['type' => $typeAnnotation->isRelay ? 'relay-mutation-payload' : 'object', 'config' => $typeConfiguration];
52✔
400
    }
401

402
    /**
403
     * Create a GraphQL Interface type configuration from metadatas on properties.
404
     *
405
     * @return array{type: 'interface', config: array}
406
     */
407
    private static function typeInterfaceMetadataToGQLConfiguration(ReflectionClass $reflectionClass, Metadata\TypeInterface $interfaceAnnotation, string $gqlName): array
408
    {
409
        $interfaceConfiguration = [];
51✔
410

411
        $fieldsFromProperties = self::getGraphQLTypeFieldsFromAnnotations($reflectionClass, self::getClassProperties($reflectionClass));
51✔
412
        $fieldsFromMethods = self::getGraphQLTypeFieldsFromAnnotations($reflectionClass, $reflectionClass->getMethods());
51✔
413

414
        $interfaceConfiguration['fields'] = array_merge($fieldsFromProperties, $fieldsFromMethods);
51✔
415
        $interfaceConfiguration = self::getDescriptionConfiguration(static::getMetadatas($reflectionClass)) + $interfaceConfiguration;
51✔
416

417
        if (isset($interfaceAnnotation->resolveType)) {
51✔
418
            $interfaceConfiguration['resolveType'] = self::formatExpression($interfaceAnnotation->resolveType);
50✔
419
        } else {
420
            // Try to use default interface resolver type
421
            $interfaceConfiguration['resolveType'] = self::formatExpression(sprintf("service('overblog_graphql.interface_type_resolver').resolveType('%s', value)", $gqlName));
51✔
422
        }
423

424
        return ['type' => 'interface', 'config' => $interfaceConfiguration];
51✔
425
    }
426

427
    /**
428
     * Create a GraphQL Input type configuration from metadatas on properties.
429
     *
430
     * @return array{type: 'relay-mutation-input'|'input-object', config: array}
431
     */
432
    private static function inputMetadataToGQLConfiguration(ReflectionClass $reflectionClass, Metadata\Input $inputAnnotation): array
433
    {
434
        $inputConfiguration = array_merge([
50✔
435
            'fields' => self::getGraphQLInputFieldsFromMetadatas($reflectionClass, self::getClassProperties($reflectionClass)),
50✔
436
        ], self::getDescriptionConfiguration(static::getMetadatas($reflectionClass), true));
50✔
437

438
        return ['type' => $inputAnnotation->isRelay ? 'relay-mutation-input' : 'input-object', 'config' => $inputConfiguration];
50✔
439
    }
440

441
    /**
442
     * Get a GraphQL scalar configuration from given scalar metadata.
443
     *
444
     * @return array{type: 'custom-scalar', config: array}
445
     */
446
    private static function scalarMetadataToGQLConfiguration(ReflectionClass $reflectionClass, Metadata\Scalar $scalarAnnotation): array
447
    {
448
        $scalarConfiguration = [];
50✔
449

450
        if (isset($scalarAnnotation->scalarType)) {
50✔
451
            $scalarConfiguration['scalarType'] = self::formatExpression($scalarAnnotation->scalarType);
50✔
452
        } else {
453
            $scalarConfiguration = [
50✔
454
                'serialize' => [$reflectionClass->getName(), 'serialize'],
50✔
455
                'parseValue' => [$reflectionClass->getName(), 'parseValue'],
50✔
456
                'parseLiteral' => [$reflectionClass->getName(), 'parseLiteral'],
50✔
457
            ];
458
        }
459

460
        $scalarConfiguration = self::getDescriptionConfiguration(static::getMetadatas($reflectionClass)) + $scalarConfiguration;
50✔
461

462
        return ['type' => 'custom-scalar', 'config' => $scalarConfiguration];
50✔
463
    }
464

465
    /**
466
     * Get a GraphQL Enum configuration from given enum metadata.
467
     *
468
     * @return array{type: 'enum', config: array}
469
     */
470
    private static function enumMetadataToGQLConfiguration(ReflectionClass $reflectionClass, Metadata\Enum $enumMetadata): array
471
    {
472
        $metadatas = static::getMetadatas($reflectionClass);
50✔
473
        $enumValues = self::getMetadataMatching($metadatas, Metadata\EnumValue::class);
50✔
474
        $isPhpEnum = PHP_VERSION_ID >= 80100 && $reflectionClass->isEnum();
50✔
475
        $values = [];
50✔
476

477
        foreach ($reflectionClass->getConstants() as $name => $value) {
50✔
478
            $reflectionConstant = new ReflectionClassConstant($reflectionClass->getName(), $name);
50✔
479
            $valueConfig = self::getDescriptionConfiguration(static::getMetadatas($reflectionConstant), true);
50✔
480

481
            $enumValueAnnotation = current(array_filter($enumValues, fn ($enumValueAnnotation) => $enumValueAnnotation->name === $name));
50✔
482
            $valueConfig['value'] = $isPhpEnum ? $value->name : $value;
50✔
483

484
            if (false !== $enumValueAnnotation) {
50✔
485
                if (isset($enumValueAnnotation->description)) {
25✔
486
                    $valueConfig['description'] = $enumValueAnnotation->description;
25✔
487
                }
488

489
                if (isset($enumValueAnnotation->deprecationReason)) {
25✔
490
                    $valueConfig['deprecationReason'] = $enumValueAnnotation->deprecationReason;
25✔
491
                }
492
            }
493

494
            $values[$name] = $valueConfig;
50✔
495
        }
496

497
        $enumConfiguration = ['values' => $values];
50✔
498
        $enumConfiguration = self::getDescriptionConfiguration(static::getMetadatas($reflectionClass)) + $enumConfiguration;
50✔
499
        if ($isPhpEnum) {
50✔
500
            $enumConfiguration['enumClass'] = $reflectionClass->getName();
50✔
501
        }
502

503
        return ['type' => 'enum', 'config' => $enumConfiguration];
50✔
504
    }
505

506
    /**
507
     * Get a GraphQL Union configuration from given union metadata.
508
     *
509
     * @return array{type: 'union', config: array}
510
     */
511
    private static function unionMetadataToGQLConfiguration(ReflectionClass $reflectionClass, Metadata\Union $unionMetadata): array
512
    {
513
        $unionConfiguration = [];
50✔
514
        if (!empty($unionMetadata->types)) {
50✔
515
            $unionConfiguration['types'] = $unionMetadata->types;
50✔
516
        } else {
517
            $types = array_keys(self::$map->searchClassesMapBy(function ($gqlType, $configuration) use ($reflectionClass) {
50✔
518
                $typeClassName = $configuration['class'];
50✔
519
                $typeMetadata = self::getClassReflection($typeClassName);
50✔
520

521
                if ($reflectionClass->isInterface() && $typeMetadata->implementsInterface($reflectionClass->getName())) {
50✔
522
                    return true;
50✔
523
                }
524

525
                return $typeMetadata->isSubclassOf($reflectionClass->getName());
50✔
526
            }, self::GQL_TYPE));
50✔
527
            sort($types);
50✔
528
            $unionConfiguration['types'] = $types;
50✔
529
        }
530

531
        $unionConfiguration = self::getDescriptionConfiguration(static::getMetadatas($reflectionClass)) + $unionConfiguration;
50✔
532

533
        if (isset($unionMetadata->resolveType)) {
50✔
534
            $unionConfiguration['resolveType'] = self::formatExpression($unionMetadata->resolveType);
50✔
535
        } else {
536
            if ($reflectionClass->hasMethod('resolveType')) {
50✔
537
                $method = $reflectionClass->getMethod('resolveType');
50✔
538
                if ($method->isStatic() && $method->isPublic()) {
50✔
539
                    $unionConfiguration['resolveType'] = self::formatExpression(sprintf("@=call('%s::%s', [service('overblog_graphql.type_resolver'), value], true)", self::formatNamespaceForExpression($reflectionClass->getName()), 'resolveType'));
50✔
540
                } else {
541
                    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')));
50✔
542
                }
543
            } else {
544
                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')));
2✔
545
            }
546
        }
547

548
        return ['type' => 'union', 'config' => $unionConfiguration];
50✔
549
    }
550

551
    /**
552
     * @phpstan-param ReflectionMethod|ReflectionProperty $reflector
553
     * @phpstan-param class-string<Metadata\Field> $fieldMetadataName
554
     *
555
     * @return array<string,array>
556
     *
557
     * @throws AnnotationException
558
     */
559
    private static function getTypeFieldConfigurationFromReflector(ReflectionClass $reflectionClass, Reflector $reflector, string $fieldMetadataName, string $currentValue = 'value'): array
560
    {
561
        /** @var ReflectionProperty|ReflectionMethod $reflector */
562
        $metadatas = static::getMetadatas($reflector);
51✔
563

564
        $fieldMetadata = self::getFirstMetadataMatching($metadatas, $fieldMetadataName);
51✔
565
        $accessMetadata = self::getFirstMetadataMatching($metadatas, Metadata\Access::class);
51✔
566
        $publicMetadata = self::getFirstMetadataMatching($metadatas, Metadata\IsPublic::class);
51✔
567

568
        if (null === $fieldMetadata) {
51✔
569
            if (null !== $accessMetadata || null !== $publicMetadata) {
50✔
570
                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')));
2✔
571
            }
572

573
            return [];
50✔
574
        }
575

576
        if ($reflector instanceof ReflectionMethod && !$reflector->isPublic()) {
51✔
577
            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()));
2✔
578
        }
579

580
        $fieldName = $reflector->getName();
51✔
581
        $fieldConfiguration = [];
51✔
582

583
        if (isset($fieldMetadata->type)) {
51✔
584
            $fieldConfiguration['type'] = $fieldMetadata->type;
51✔
585
        }
586

587
        $fieldConfiguration = self::getDescriptionConfiguration($metadatas, true) + $fieldConfiguration;
51✔
588

589
        $args = [];
51✔
590

591
        /** @var Metadata\Arg[] $argAnnotations */
592
        $argAnnotations = self::getMetadataMatching($metadatas, Metadata\Arg::class);
51✔
593

594
        foreach ($argAnnotations as $arg) {
51✔
595
            $args[$arg->name] = ['type' => $arg->type];
50✔
596

597
            if (isset($arg->description)) {
50✔
598
                $args[$arg->name]['description'] = $arg->description;
50✔
599
            }
600

601
            if (isset($arg->defaultValue)) {
50✔
602
                $args[$arg->name]['defaultValue'] = $arg->defaultValue;
50✔
603
            } elseif (isset($arg->default)) {
50✔
NEW
604
                @trigger_error(sprintf('%s %s %s', 'overblog/graphql-bundle', '1.3', 'The "default" attribute on @GQL\Arg or #GQL\Arg is deprecated, use "defaultValue" instead.'), E_USER_DEPRECATED);
×
UNCOV
605
                $args[$arg->name]['defaultValue'] = $arg->default;
×
606
            }
607
        }
608

609
        if ($reflector instanceof ReflectionMethod) {
51✔
610
            $args = self::guessArgs($reflectionClass, $reflector, $args);
51✔
611
        }
612

613
        if (!empty($args)) {
51✔
614
            $fieldConfiguration['args'] = $args;
50✔
615
        }
616

617
        $fieldName = $fieldMetadata->name ?? $fieldName;
51✔
618

619
        if (isset($fieldMetadata->resolve)) {
51✔
620
            $fieldConfiguration['resolve'] = self::formatExpression($fieldMetadata->resolve);
50✔
621
        } else {
622
            if ($reflector instanceof ReflectionMethod) {
51✔
623
                $fieldConfiguration['resolve'] = self::formatExpression(sprintf('call(%s.%s, %s)', $currentValue, $reflector->getName(), self::formatArgsForExpression($args)));
51✔
624
            } else {
625
                if ($fieldName !== $reflector->getName() || 'value' !== $currentValue) {
51✔
626
                    $fieldConfiguration['resolve'] = self::formatExpression(sprintf('%s.%s', $currentValue, $reflector->getName()));
1✔
627
                }
628
            }
629
        }
630

631
        $argsBuilder = self::getFirstMetadataMatching($metadatas, Metadata\ArgsBuilder::class);
51✔
632
        if ($argsBuilder) {
51✔
633
            $fieldConfiguration['argsBuilder'] = ['builder' => $argsBuilder->name, 'config' => $argsBuilder->config];
50✔
634
        }
635
        $fieldBuilder = self::getFirstMetadataMatching($metadatas, Metadata\FieldBuilder::class);
51✔
636
        if ($fieldBuilder) {
51✔
637
            $fieldConfiguration['builder'] = $fieldBuilder->name;
50✔
638
            $fieldConfiguration['builderConfig'] = $fieldBuilder->config;
50✔
639
        } else {
640
            if (!isset($fieldMetadata->type)) {
51✔
641
                try {
642
                    $fieldConfiguration['type'] = self::guessType($reflectionClass, $reflector, self::VALID_OUTPUT_TYPES);
51✔
643
                } catch (TypeGuessingException $e) {
6✔
644
                    $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());
6✔
645

646
                    throw new InvalidArgumentException($error);
6✔
647
                }
648
            }
649
        }
650

651
        if ($accessMetadata) {
51✔
652
            $fieldConfiguration['access'] = self::formatExpression($accessMetadata->value);
50✔
653
        }
654

655
        if ($publicMetadata) {
51✔
656
            $fieldConfiguration['public'] = self::formatExpression($publicMetadata->value);
50✔
657
        }
658

659
        if (isset($fieldMetadata->complexity)) {
51✔
660
            $fieldConfiguration['complexity'] = self::formatExpression($fieldMetadata->complexity);
50✔
661
        }
662

663
        return [$fieldName => $fieldConfiguration];
51✔
664
    }
665

666
    /**
667
     * Create GraphQL input fields configuration based on metadatas.
668
     *
669
     * @param ReflectionProperty[] $reflectors
670
     *
671
     * @return array<string,array>
672
     *
673
     * @throws AnnotationException
674
     */
675
    private static function getGraphQLInputFieldsFromMetadatas(ReflectionClass $reflectionClass, array $reflectors): array
676
    {
677
        $fields = [];
50✔
678

679
        foreach ($reflectors as $reflector) {
50✔
680
            $metadatas = static::getMetadatas($reflector);
50✔
681

682
            /** @var Metadata\Field|null $fieldMetadata */
683
            $fieldMetadata = self::getFirstMetadataMatching($metadatas, Metadata\Field::class);
50✔
684

685
            // No field metadata found
686
            if (null === $fieldMetadata) {
50✔
687
                continue;
50✔
688
            }
689

690
            // Ignore field with resolver when the type is an Input
691
            if (isset($fieldMetadata->resolve)) {
50✔
692
                continue;
50✔
693
            }
694

695
            $fieldName = $reflector->getName();
50✔
696
            if (isset($fieldMetadata->type)) {
50✔
697
                $fieldType = $fieldMetadata->type;
50✔
698
            } else {
699
                try {
700
                    $fieldType = self::guessType($reflectionClass, $reflector, self::VALID_INPUT_TYPES);
50✔
701
                } catch (TypeGuessingException $e) {
×
702
                    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()));
×
703
                }
704
            }
705
            $fieldConfiguration = [];
50✔
706
            if ($fieldType) {
50✔
707
                // Resolve a PHP class from a GraphQL type
708
                $resolvedType = self::$map->getType($fieldType);
50✔
709
                // We found a type but it is not allowed
710
                if (null !== $resolvedType && !in_array($resolvedType['type'], self::VALID_INPUT_TYPES)) {
50✔
711
                    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')));
×
712
                }
713

714
                $fieldConfiguration['type'] = $fieldType;
50✔
715
            }
716

717
            if ($fieldMetadata instanceof InputField && null !== $fieldMetadata->defaultValue) {
50✔
718
                $fieldConfiguration['defaultValue'] = $fieldMetadata->defaultValue;
50✔
719
            } elseif ($reflector->hasDefaultValue() && null !== $reflector->getDefaultValue()) {
50✔
720
                $fieldConfiguration['defaultValue'] = $reflector->getDefaultValue();
50✔
721
            }
722

723
            $fieldConfiguration = array_merge(self::getDescriptionConfiguration($metadatas, true), $fieldConfiguration);
50✔
724
            $fields[$fieldName] = $fieldConfiguration;
50✔
725
        }
726

727
        return $fields;
50✔
728
    }
729

730
    /**
731
     * Create GraphQL type fields configuration based on metadatas.
732
     *
733
     * @phpstan-param class-string<Metadata\Field> $fieldMetadataName
734
     *
735
     * @param ReflectionProperty[]|ReflectionMethod[] $reflectors
736
     *
737
     * @throws AnnotationException
738
     */
739
    private static function getGraphQLTypeFieldsFromAnnotations(ReflectionClass $reflectionClass, array $reflectors, string $fieldMetadataName = Metadata\Field::class, string $currentValue = 'value'): array
740
    {
741
        $fields = [];
52✔
742

743
        foreach ($reflectors as $reflector) {
52✔
744
            $fields = array_merge($fields, self::getTypeFieldConfigurationFromReflector($reflectionClass, $reflector, $fieldMetadataName, $currentValue));
51✔
745
        }
746

747
        return $fields;
52✔
748
    }
749

750
    /**
751
     * @phpstan-param class-string<Metadata\Query|Metadata\Mutation> $expectedMetadata
752
     *
753
     * Return fields config from Provider methods.
754
     * Loop through configured provider and extract fields targeting the targetType.
755
     *
756
     * @return array<string,array>
757
     */
758
    private static function getGraphQLFieldsFromProviders(ReflectionClass $reflectionClass, string $expectedMetadata, string $targetType, bool $isDefaultTarget = false): array
759
    {
760
        $fields = [];
52✔
761
        foreach (self::$providers as ['reflectionClass' => $providerReflection, 'metadata' => $providerMetadata]) {
52✔
762
            $defaultAccessAnnotation = self::getFirstMetadataMatching(static::getMetadatas($providerReflection), Metadata\Access::class);
51✔
763
            $defaultIsPublicAnnotation = self::getFirstMetadataMatching(static::getMetadatas($providerReflection), Metadata\IsPublic::class);
51✔
764

765
            $defaultAccess = $defaultAccessAnnotation ? self::formatExpression($defaultAccessAnnotation->value) : false;
51✔
766
            $defaultIsPublic = $defaultIsPublicAnnotation ? self::formatExpression($defaultIsPublicAnnotation->value) : false;
51✔
767

768
            $methods = [];
51✔
769
            // First found the methods matching the targeted type
770
            foreach ($providerReflection->getMethods() as $method) {
51✔
771
                $metadatas = static::getMetadatas($method);
51✔
772

773
                $metadata = self::getFirstMetadataMatching($metadatas, [Metadata\Mutation::class, Metadata\Query::class]);
51✔
774
                if (null === $metadata) {
51✔
775
                    continue;
×
776
                }
777

778
                // TODO: Remove old property check in 1.1
779
                $metadataTargets = $metadata->targetTypes ?? null;
51✔
780

781
                if (null === $metadataTargets) {
51✔
782
                    if ($metadata instanceof Metadata\Mutation && isset($providerMetadata->targetMutationTypes)) {
51✔
783
                        $metadataTargets = $providerMetadata->targetMutationTypes;
50✔
784
                    } elseif ($metadata instanceof Metadata\Query && isset($providerMetadata->targetQueryTypes)) {
51✔
785
                        $metadataTargets = $providerMetadata->targetQueryTypes;
50✔
786
                    }
787
                }
788

789
                if (null === $metadataTargets) {
51✔
790
                    if ($isDefaultTarget) {
51✔
791
                        $metadataTargets = [$targetType];
51✔
792
                        if (!$metadata instanceof $expectedMetadata) {
51✔
793
                            continue;
51✔
794
                        }
795
                    } else {
796
                        continue;
51✔
797
                    }
798
                }
799

800
                if (!in_array($targetType, $metadataTargets)) {
51✔
801
                    continue;
50✔
802
                }
803

804
                if (!$metadata instanceof $expectedMetadata) {
51✔
805
                    if (Metadata\Mutation::class === $expectedMetadata) {
4✔
806
                        $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);
2✔
807
                    } else {
808
                        $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);
2✔
809
                    }
810

811
                    throw new InvalidArgumentException($message);
4✔
812
                }
813
                $methods[$method->getName()] = $method;
51✔
814
            }
815

816
            $currentValue = sprintf("service('%s')", self::formatNamespaceForExpression($providerReflection->getName()));
51✔
817
            $providerFields = self::getGraphQLTypeFieldsFromAnnotations($reflectionClass, $methods, $expectedMetadata, $currentValue);
51✔
818
            foreach ($providerFields as $fieldName => $fieldConfig) {
51✔
819
                if (isset($providerMetadata->prefix)) {
51✔
820
                    $fieldName = sprintf('%s%s', $providerMetadata->prefix, $fieldName);
50✔
821
                }
822

823
                if ($defaultAccess && !isset($fieldConfig['access'])) {
51✔
824
                    $fieldConfig['access'] = $defaultAccess;
50✔
825
                }
826

827
                if ($defaultIsPublic && !isset($fieldConfig['public'])) {
51✔
828
                    $fieldConfig['public'] = $defaultIsPublic;
50✔
829
                }
830

831
                $fields[$fieldName] = $fieldConfig;
51✔
832
            }
833
        }
834

835
        return $fields;
52✔
836
    }
837

838
    /**
839
     * Get the config for description & deprecation reason.
840
     *
841
     * @return array<'description'|'deprecationReason',string>
842
     */
843
    private static function getDescriptionConfiguration(array $metadatas, bool $withDeprecation = false): array
844
    {
845
        $config = [];
52✔
846
        $descriptionAnnotation = self::getFirstMetadataMatching($metadatas, Metadata\Description::class);
52✔
847
        if (null !== $descriptionAnnotation) {
52✔
848
            $config['description'] = $descriptionAnnotation->value;
50✔
849
        }
850

851
        if ($withDeprecation) {
52✔
852
            $deprecatedAnnotation = self::getFirstMetadataMatching($metadatas, Metadata\Deprecated::class);
51✔
853
            if (null !== $deprecatedAnnotation) {
51✔
854
                $config['deprecationReason'] = $deprecatedAnnotation->value;
50✔
855
            }
856
        }
857

858
        return $config;
52✔
859
    }
860

861
    /**
862
     * Format an array of args to a list of arguments in an expression.
863
     */
864
    private static function formatArgsForExpression(array $args): string
865
    {
866
        $mapping = [];
51✔
867
        foreach ($args as $name => $config) {
51✔
868
            $mapping[] = sprintf('%s: "%s"', $name, $config['type']);
50✔
869
        }
870

871
        return sprintf('arguments({%s}, args)', implode(', ', $mapping));
51✔
872
    }
873

874
    /**
875
     * Format a namespace to be used in an expression (double escape).
876
     */
877
    private static function formatNamespaceForExpression(string $namespace): string
878
    {
879
        return str_replace('\\', '\\\\', $namespace);
51✔
880
    }
881

882
    /**
883
     * Get the first metadata matching given class.
884
     *
885
     * @phpstan-template T of object
886
     *
887
     * @phpstan-param class-string<T>|class-string<T>[] $metadataClasses
888
     *
889
     * @phpstan-return T|null
890
     *
891
     * @return object|null
892
     */
893
    private static function getFirstMetadataMatching(array $metadatas, $metadataClasses)
894
    {
895
        $metas = self::getMetadataMatching($metadatas, $metadataClasses);
52✔
896

897
        return array_shift($metas);
52✔
898
    }
899

900
    /**
901
     * Return the metadata matching given class
902
     *
903
     * @phpstan-template T of object
904
     *
905
     * @phpstan-param class-string<T>|class-string<T>[] $metadataClasses
906
     *
907
     * @return array
908
     */
909
    private static function getMetadataMatching(array $metadatas, $metadataClasses)
910
    {
911
        if (is_string($metadataClasses)) {
52✔
912
            $metadataClasses = [$metadataClasses];
52✔
913
        }
914

915
        return array_values(array_filter($metadatas, function ($metadata) use ($metadataClasses) {
52✔
916
            foreach ($metadataClasses as $metadataClass) {
52✔
917
                if ($metadata instanceof $metadataClass) {
52✔
918
                    return true;
51✔
919
                }
920
            }
921

922
            return false;
52✔
923
        }));
52✔
924
    }
925

926
    /**
927
     * Format an expression (ie. add "@=" if not set).
928
     */
929
    private static function formatExpression(string $expression): string
930
    {
931
        return '@=' === substr($expression, 0, 2) ? $expression : sprintf('@=%s', $expression);
51✔
932
    }
933

934
    /**
935
     * Suffix a name if it is not already.
936
     */
937
    private static function suffixName(string $name, string $suffix): string
938
    {
939
        return substr($name, -strlen($suffix)) === $suffix ? $name : sprintf('%s%s', $name, $suffix);
50✔
940
    }
941

942
    /**
943
     * Try to guess a GraphQL type using configured type guessers
944
     *
945
     * @throws RuntimeException
946
     */
947
    private static function guessType(ReflectionClass $reflectionClass, Reflector $reflector, array $filterGraphQLTypes = []): string
948
    {
949
        $errors = [];
51✔
950
        foreach (self::$typeGuessers as $typeGuesser) {
51✔
951
            if (!$typeGuesser->supports($reflector)) {
51✔
952
                continue;
50✔
953
            }
954
            try {
955
                $type = $typeGuesser->guessType($reflectionClass, $reflector, $filterGraphQLTypes);
51✔
956

957
                return $type;
51✔
958
            } catch (TypeGuessingException $exception) {
51✔
959
                $errors[] = sprintf('[%s] %s', $typeGuesser->getName(), $exception->getMessage());
51✔
960
            }
961
        }
962

963
        throw new TypeGuessingException(implode("\n", $errors));
8✔
964
    }
965

966
    /**
967
     * Transform a method arguments from reflection to a list of GraphQL argument.
968
     */
969
    private static function guessArgs(
970
        ReflectionClass $reflectionClass,
971
        ReflectionMethod $method,
972
        array $arguments,
973
    ): array {
974
        foreach ($method->getParameters() as $index => $parameter) {
51✔
975
            if (array_key_exists($parameter->getName(), $arguments)) {
50✔
976
                continue;
50✔
977
            }
978

979
            try {
980
                $gqlType = self::guessType($reflectionClass, $parameter, self::VALID_INPUT_TYPES);
50✔
981
            } catch (TypeGuessingException $exception) {
2✔
982
                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()));
2✔
983
            }
984

985
            $argumentConfig = [];
50✔
986
            if ($parameter->isDefaultValueAvailable()) {
50✔
987
                $argumentConfig['defaultValue'] = $parameter->getDefaultValue();
50✔
988
            }
989

990
            $argumentConfig['type'] = $gqlType;
50✔
991

992
            $arguments[$parameter->getName()] = $argumentConfig;
50✔
993
        }
994

995
        return $arguments;
51✔
996
    }
997

998
    /**
999
     * @return ReflectionProperty[]
1000
     */
1001
    private static function getClassProperties(ReflectionClass $reflectionClass): array
1002
    {
1003
        $properties = [];
52✔
1004
        do {
1005
            foreach ($reflectionClass->getProperties() as $property) {
52✔
1006
                if (isset($properties[$property->getName()])) {
51✔
1007
                    continue;
51✔
1008
                }
1009
                $properties[$property->getName()] = $property;
51✔
1010
            }
1011
        } while ($reflectionClass = $reflectionClass->getParentClass());
52✔
1012

1013
        return $properties;
52✔
1014
    }
1015

1016
    protected static function formatMetadata(string $className): string
1017
    {
1018
        return sprintf(static::METADATA_FORMAT, str_replace(self::ANNOTATION_NAMESPACE, '', $className));
16✔
1019
    }
1020

1021
    abstract protected static function getMetadatas(Reflector $reflector): array;
1022
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc