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

overblog / GraphQLBundle / 21449656252

28 Jan 2026 05:58PM UTC coverage: 98.372% (-0.2%) from 98.563%
21449656252

Pull #1228

github

web-flow
Merge fd68937f1 into 1881475b6
Pull Request #1228: adding symfony8 to supported list

10 of 19 new or added lines in 2 files covered. (52.63%)

4 existing lines in 1 file now uncovered.

4533 of 4608 relevant lines covered (98.37%)

39.24 hits per line

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

94.79
/src/Generator/TypeBuilder.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Overblog\GraphQLBundle\Generator;
6

7
use Composer\InstalledVersions;
8
use GraphQL\Language\AST\NodeKind;
9
use GraphQL\Language\Parser;
10
use GraphQL\Type\Definition\InputObjectType;
11
use GraphQL\Type\Definition\InterfaceType;
12
use GraphQL\Type\Definition\ObjectType;
13
use GraphQL\Type\Definition\ResolveInfo;
14
use GraphQL\Type\Definition\Type;
15
use GraphQL\Type\Definition\UnionType;
16
use Murtukov\PHPCodeGenerator\ArrowFunction;
17
use Murtukov\PHPCodeGenerator\Closure;
18
use Murtukov\PHPCodeGenerator\Config;
19
use Murtukov\PHPCodeGenerator\ConverterInterface;
20
use Murtukov\PHPCodeGenerator\GeneratorInterface;
21
use Murtukov\PHPCodeGenerator\Instance;
22
use Murtukov\PHPCodeGenerator\Literal;
23
use Murtukov\PHPCodeGenerator\PhpFile;
24
use Murtukov\PHPCodeGenerator\Utils;
25
use Overblog\GraphQLBundle\Definition\ConfigProcessor;
26
use Overblog\GraphQLBundle\Definition\GraphQLServices;
27
use Overblog\GraphQLBundle\Definition\Resolver\AliasedInterface;
28
use Overblog\GraphQLBundle\Definition\Type\CustomScalarType;
29
use Overblog\GraphQLBundle\Definition\Type\GeneratedTypeInterface;
30
use Overblog\GraphQLBundle\Definition\Type\PhpEnumType;
31
use Overblog\GraphQLBundle\Error\ResolveErrors;
32
use Overblog\GraphQLBundle\ExpressionLanguage\ExpressionLanguage as EL;
33
use Overblog\GraphQLBundle\Generator\Converter\ExpressionConverter;
34
use Overblog\GraphQLBundle\Generator\Exception\GeneratorException;
35
use Overblog\GraphQLBundle\Validator\InputValidator;
36
use ReflectionClass;
37
use Symfony\Component\Validator\Constraints\Choice;
38
use Symfony\Component\Validator\Constraints\Video;
39

40
use function array_map;
41
use function class_exists;
42
use function count;
43
use function explode;
44
use function in_array;
45
use function is_array;
46
use function is_callable;
47
use function is_string;
48
use function key;
49
use function ltrim;
50
use function reset;
51
use function rtrim;
52
use function strtolower;
53
use function substr;
54

55
/**
56
 * Service that exposes a single method `build` called for each GraphQL
57
 * type config to build a PhpFile object.
58
 *
59
 * {@link https://github.com/murtukov/php-code-generator}
60
 *
61
 * It's responsible for building all GraphQL types (object, input-object,
62
 * interface, union, enum and custom-scalar).
63
 *
64
 * Every method with prefix 'build' has a render example in it's PHPDoc.
65
 */
66
final class TypeBuilder
67
{
68
    private const CONSTRAINTS_NAMESPACE = 'Symfony\Component\Validator\Constraints';
69
    private const DOCBLOCK_TEXT = 'THIS FILE WAS GENERATED AND SHOULD NOT BE EDITED MANUALLY.';
70
    private const BUILT_IN_TYPES = [Type::STRING, Type::INT, Type::FLOAT, Type::BOOLEAN, Type::ID];
71

72
    private const EXTENDS = [
73
        'object' => ObjectType::class,
74
        'input-object' => InputObjectType::class,
75
        'interface' => InterfaceType::class,
76
        'union' => UnionType::class,
77
        'enum' => PhpEnumType::class,
78
        'custom-scalar' => CustomScalarType::class,
79
    ];
80

81
    private ExpressionConverter $expressionConverter;
82
    private PhpFile $file;
83
    private string $namespace;
84
    private array $config;
85
    private string $type;
86
    private string $currentField;
87
    private string $gqlServices = '$'.TypeGenerator::GRAPHQL_SERVICES;
88
    private bool $isSymfony74Plus;
89

90
    public function __construct(ExpressionConverter $expressionConverter, string $namespace)
91
    {
92
        $this->expressionConverter = $expressionConverter;
163✔
93
        $this->namespace = $namespace;
163✔
94

95
        // Register additional converter in the php code generator
96
        Config::registerConverter($expressionConverter, ConverterInterface::TYPE_STRING);
163✔
97
        $this->isSymfony74Plus = class_exists(Video::class);
163✔
98

99
    }
100

101
    /**
102
     * @param array{
103
     *     name:          string,
104
     *     class_name:    string,
105
     *     fields:        array,
106
     *     description?:  string,
107
     *     interfaces?:   array,
108
     *     resolveType?:  string,
109
     *     validation?:   array,
110
     *     types?:        array,
111
     *     values?:       array,
112
     *     serialize?:    callable,
113
     *     parseValue?:   callable,
114
     *     parseLiteral?: callable,
115
     * } $config
116
     *
117
     * @throws GeneratorException
118
     */
119
    public function build(array $config, string $type): PhpFile
120
    {
121
        // This values should be accessible from every method
122
        $this->config = $config;
45✔
123
        $this->type = $type;
45✔
124

125
        $this->file = PhpFile::new()->setNamespace($this->namespace);
45✔
126

127
        $class = $this->file->createClass($config['class_name'])
45✔
128
            ->setFinal()
45✔
129
            ->setExtends(static::EXTENDS[$type])
45✔
130
            ->addImplements(GeneratedTypeInterface::class, AliasedInterface::class)
45✔
131
            ->addConst('NAME', $config['name'])
45✔
132
            ->setDocBlock(static::DOCBLOCK_TEXT);
45✔
133

134
        $class->emptyLine();
45✔
135

136
        $class->createConstructor()
45✔
137
            ->addArgument('configProcessor', ConfigProcessor::class)
45✔
138
            ->addArgument(TypeGenerator::GRAPHQL_SERVICES, GraphQLServices::class)
45✔
139
            ->append('$config = ', $this->buildConfig($config))
45✔
140
            ->emptyLine()
45✔
141
            ->append('parent::__construct($configProcessor->process($config))');
45✔
142

143
        $class->createMethod('getAliases', 'public')
42✔
144
            ->setStatic()
42✔
145
            ->setReturnType('array')
42✔
146
            ->setDocBlock('{@inheritdoc}')
42✔
147
            ->append('return [self::NAME]');
42✔
148

149
        return $this->file;
42✔
150
    }
151

152
    /**
153
     * Converts a native GraphQL type string into the `webonyx/graphql-php`
154
     * type literal. References to user-defined types are converted into
155
     * TypeResolver method call and wrapped into a closure.
156
     *
157
     * Render examples:
158
     *
159
     *  -   "String"   -> Type::string()
160
     *  -   "String!"  -> Type::nonNull(Type::string())
161
     *  -   "[String!] -> Type::listOf(Type::nonNull(Type::string()))
162
     *  -   "[Post]"   -> Type::listOf($services->getType('Post'))
163
     *
164
     * @return GeneratorInterface|string
165
     */
166
    private function buildType(string $typeDefinition)
167
    {
168
        $typeNode = Parser::parseType($typeDefinition);
45✔
169

170
        $isReference = false;
45✔
171
        $type = $this->wrapTypeRecursive($typeNode, $isReference);
45✔
172

173
        if ($isReference) {
45✔
174
            // References to other types should be wrapped in a closure
175
            // for performance reasons
176
            return ArrowFunction::new($type);
35✔
177
        }
178

179
        return $type;
45✔
180
    }
181

182
    /**
183
     * Used by {@see buildType}.
184
     *
185
     * @param mixed $typeNode
186
     *
187
     * @return Literal|string
188
     */
189
    private function wrapTypeRecursive($typeNode, bool &$isReference)
190
    {
191
        switch ($typeNode->kind) {
45✔
192
            case NodeKind::NON_NULL_TYPE:
1✔
193
                $innerType = $this->wrapTypeRecursive($typeNode->type, $isReference);
42✔
194
                $type = Literal::new("Type::nonNull($innerType)");
42✔
195
                $this->file->addUse(Type::class);
42✔
196
                break;
42✔
197
            case NodeKind::LIST_TYPE:
1✔
198
                $innerType = $this->wrapTypeRecursive($typeNode->type, $isReference);
28✔
199
                $type = Literal::new("Type::listOf($innerType)");
28✔
200
                $this->file->addUse(Type::class);
28✔
201
                break;
28✔
202
            default: // NodeKind::NAMED_TYPE
203
                if (in_array($typeNode->name->value, static::BUILT_IN_TYPES)) {
45✔
204
                    $name = strtolower($typeNode->name->value);
45✔
205
                    $type = Literal::new("Type::$name()");
45✔
206
                    $this->file->addUse(Type::class);
45✔
207
                } else {
208
                    $name = $typeNode->name->value;
35✔
209
                    $type = "$this->gqlServices->getType('$name')";
35✔
210
                    $isReference = true;
35✔
211
                }
212
                break;
45✔
213
        }
214

215
        return $type;
45✔
216
    }
217

218
    /**
219
     * Builds a config array compatible with webonyx/graphql-php type system. The content
220
     * of the array depends on the GraphQL type that is currently being generated.
221
     *
222
     * Render example (object):
223
     *
224
     *      [
225
     *          'name' => self::NAME,
226
     *          'description' => 'Root query type',
227
     *          'fields' => fn() => [
228
     *              'posts' => {@see buildField},
229
     *              'users' => {@see buildField},
230
     *               ...
231
     *           ],
232
     *           'interfaces' => fn() => [
233
     *               $services->getType('PostInterface'),
234
     *               ...
235
     *           ],
236
     *           'resolveField' => {@see buildResolveField},
237
     *      ]
238
     *
239
     * Render example (input-object):
240
     *
241
     *      [
242
     *          'name' => self::NAME,
243
     *          'description' => 'Some description.',
244
     *          'validation' => {@see buildValidationRules}
245
     *          'fields' => fn() => [
246
     *              {@see buildField},
247
     *               ...
248
     *           ],
249
     *      ]
250
     *
251
     * Render example (interface)
252
     *
253
     *      [
254
     *          'name' => self::NAME,
255
     *          'description' => 'Some description.',
256
     *          'fields' => fn() => [
257
     *              {@see buildField},
258
     *               ...
259
     *           ],
260
     *          'resolveType' => {@see buildResolveType},
261
     *      ]
262
     *
263
     * Render example (union):
264
     *
265
     *      [
266
     *          'name' => self::NAME,
267
     *          'description' => 'Some description.',
268
     *          'types' => fn() => [
269
     *              $services->getType('Photo'),
270
     *              ...
271
     *          ],
272
     *          'resolveType' => {@see buildResolveType},
273
     *      ]
274
     *
275
     * Render example (custom-scalar):
276
     *
277
     *      [
278
     *          'name' => self::NAME,
279
     *          'description' => 'Some description'
280
     *          'serialize' => {@see buildScalarCallback},
281
     *          'parseValue' => {@see buildScalarCallback},
282
     *          'parseLiteral' => {@see buildScalarCallback},
283
     *      ]
284
     *
285
     * Render example (enum):
286
     *
287
     *      [
288
     *          'name' => self::NAME,
289
     *          'values' => [
290
     *              'PUBLISHED' => ['value' => 1],
291
     *              'DRAFT' => ['value' => 2],
292
     *              'STANDBY' => [
293
     *                  'value' => 3,
294
     *                  'description' => 'Waiting for validation',
295
     *              ],
296
     *              ...
297
     *          ],
298
     *      ]
299
     *
300
     * @throws GeneratorException
301
     */
302
    private function buildConfig(array $config): Collection
303
    {
304
        // Convert to an object for a better readability
305
        $c = (object) $config;
45✔
306

307
        $configLoader = Collection::assoc();
45✔
308
        $configLoader->addItem('name', new Literal('self::NAME'));
45✔
309

310
        if (isset($c->description)) {
45✔
311
            $configLoader->addItem('description', $c->description);
42✔
312
        }
313

314
        // only by input-object types (for class level validation)
315
        if (isset($c->validation)) {
45✔
316
            $configLoader->addItem('validation', $this->buildValidationRules($c->validation));
1✔
317
        }
318

319
        // only by object, input-object and interface types
320
        if (!empty($c->fields)) {
45✔
321
            $configLoader->addItem('fields', ArrowFunction::new(
45✔
322
                Collection::map($c->fields, [$this, 'buildField'])
45✔
323
            ));
45✔
324
        }
325

326
        if (!empty($c->interfaces)) {
42✔
327
            $items = array_map(fn ($type) => "$this->gqlServices->getType('$type')", $c->interfaces);
9✔
328
            $configLoader->addItem('interfaces', ArrowFunction::new(Collection::numeric($items, true)));
9✔
329
        }
330

331
        if (!empty($c->types)) {
42✔
332
            $items = array_map(fn ($type) => "$this->gqlServices->getType('$type')", $c->types);
2✔
333
            $configLoader->addItem('types', ArrowFunction::new(Collection::numeric($items, true)));
2✔
334
        }
335

336
        if (isset($c->resolveType)) {
42✔
337
            $configLoader->addItem('resolveType', $this->buildResolveType($c->resolveType));
6✔
338
        }
339

340
        if (isset($c->isTypeOf)) {
42✔
341
            $configLoader->addItem('isTypeOf', $this->buildIsTypeOf($c->isTypeOf));
2✔
342
        }
343

344
        if (isset($c->resolveField)) {
42✔
345
            $configLoader->addItem('resolveField', $this->buildResolve($c->resolveField));
4✔
346
        }
347

348
        // only by enum types
349
        if (isset($c->values)) {
42✔
350
            $configLoader->addItem('values', Collection::assoc($c->values));
10✔
351
        }
352
        if (isset($c->enumClass)) {
42✔
353
            $configLoader->addItem('enumClass', $c->enumClass);
2✔
354
        }
355

356
        // only by custom-scalar types
357
        if ('custom-scalar' === $this->type) {
42✔
358
            if (isset($c->scalarType)) {
6✔
359
                $configLoader->addItem('scalarType', $c->scalarType);
2✔
360
            }
361

362
            if (isset($c->serialize)) {
6✔
363
                $configLoader->addItem('serialize', $this->buildScalarCallback($c->serialize, 'serialize'));
4✔
364
            }
365

366
            if (isset($c->parseValue)) {
6✔
367
                $configLoader->addItem('parseValue', $this->buildScalarCallback($c->parseValue, 'parseValue'));
4✔
368
            }
369

370
            if (isset($c->parseLiteral)) {
6✔
371
                $configLoader->addItem('parseLiteral', $this->buildScalarCallback($c->parseLiteral, 'parseLiteral'));
4✔
372
            }
373
        }
374

375
        return $configLoader;
42✔
376
    }
377

378
    /**
379
     * Builds an arrow function that calls a static method.
380
     *
381
     * Render example:
382
     *
383
     *      fn() => MyClassName::myMethodName(...\func_get_args())
384
     *
385
     * @param callable $callback - a callable string or a callable array
386
     *
387
     * @return ArrowFunction
388
     *
389
     * @throws GeneratorException
390
     */
391
    private function buildScalarCallback($callback, string $fieldName)
392
    {
393
        if (!is_callable($callback)) {
4✔
UNCOV
394
            throw new GeneratorException("Value of '$fieldName' is not callable.");
×
395
        }
396

397
        $closure = new ArrowFunction();
4✔
398

399
        if (!is_string($callback)) {
4✔
400
            [$class, $method] = $callback;
4✔
401
        } else {
402
            [$class, $method] = explode('::', $callback);
2✔
403
        }
404

405
        $className = Utils::resolveQualifier($class);
4✔
406

407
        if ($className === $this->config['class_name']) {
4✔
408
            // Create an alias if name of serializer is same as type name
409
            $className = 'Base'.$className;
2✔
410
            $this->file->addUse($class, $className);
2✔
411
        } else {
412
            $this->file->addUse($class);
2✔
413
        }
414

415
        $closure->setExpression(Literal::new("$className::$method(...\\func_get_args())"));
4✔
416

417
        return $closure;
4✔
418
    }
419

420
    /**
421
     * Builds a resolver closure that contains the compiled result of user-defined
422
     * expression and optionally the validation logic.
423
     *
424
     * Render example (no expression language):
425
     *
426
     *      function ($value, $args, $context, $info) use ($services) {
427
     *          return "Hello, World!";
428
     *      }
429
     *
430
     * Render example (with expression language):
431
     *
432
     *      function ($value, $args, $context, $info) use ($services) {
433
     *          return $services->mutation("my_resolver", $args);
434
     *      }
435
     *
436
     * Render example (with validation):
437
     *
438
     *      function ($value, $args, $context, $info) use ($services) {
439
     *          $validator = $services->createInputValidator(...func_get_args());
440
     *          return $services->mutation("create_post", $validator]);
441
     *      }
442
     *
443
     * Render example (with validation, but errors are injected into the user-defined resolver):
444
     * {@link https://github.com/overblog/GraphQLBundle/blob/master/docs/validation/index.md#injecting-errors}
445
     *
446
     *      function ($value, $args, $context, $info) use ($services) {
447
     *          $errors = new ResolveErrors();
448
     *          $validator = $services->createInputValidator(...func_get_args());
449
     *
450
     *          $errors->setValidationErrors($validator->validate(null, false))
451
     *
452
     *          return $services->mutation("create_post", $errors);
453
     *      }
454
     *
455
     * @param mixed $resolve
456
     *
457
     * @throws GeneratorException
458
     */
459
    private function buildResolve($resolve, ?array $groups = null): GeneratorInterface
460
    {
461
        if (is_callable($resolve) && is_array($resolve)) {
42✔
462
            return Collection::numeric($resolve);
4✔
463
        }
464

465
        // TODO: before creating an input validator, check if any validation rules are defined
466
        if (EL::isStringWithTrigger($resolve)) {
41✔
467
            $closure = Closure::new()
40✔
468
                ->addArguments('value', 'args', 'context', 'info')
40✔
469
                ->bindVar(TypeGenerator::GRAPHQL_SERVICES);
40✔
470

471
            $injectValidator = EL::expressionContainsVar('validator', $resolve);
40✔
472

473
            if ($this->configContainsValidation()) {
40✔
474
                $injectErrors = EL::expressionContainsVar('errors', $resolve);
6✔
475

476
                if ($injectErrors) {
6✔
477
                    $closure->append('$errors = ', Instance::new(ResolveErrors::class));
1✔
478
                }
479

480
                $closure->append('$validator = ', "$this->gqlServices->createInputValidator(...func_get_args())");
6✔
481

482
                // If auto-validation on or errors are injected
483
                if (!$injectValidator || $injectErrors) {
6✔
484
                    if (!empty($groups)) {
3✔
485
                        $validationGroups = Collection::numeric($groups);
1✔
486
                    } else {
487
                        $validationGroups = 'null';
3✔
488
                    }
489

490
                    $closure->emptyLine();
3✔
491

492
                    if ($injectErrors) {
3✔
493
                        $closure->append('$errors->setValidationErrors($validator->validate(', $validationGroups, ', false))');
1✔
494
                    } else {
495
                        $closure->append('$validator->validate(', $validationGroups, ')');
3✔
496
                    }
497

498
                    $closure->emptyLine();
6✔
499
                }
500
            } elseif ($injectValidator) {
36✔
501
                throw new GeneratorException('Unable to inject an instance of the InputValidator. No validation constraints provided. Please remove the "validator" argument from the list of dependencies of your resolver or provide validation configs.');
1✔
502
            }
503

504
            $closure->append('return ', $this->expressionConverter->convert($resolve));
39✔
505

506
            return $closure;
39✔
507
        }
508

509
        return ArrowFunction::new($resolve);
24✔
510
    }
511

512
    /**
513
     * Checks if given config contains any validation rules.
514
     */
515
    private function configContainsValidation(): bool
516
    {
517
        $fieldConfig = $this->config['fields'][$this->currentField];
40✔
518

519
        if (!empty($fieldConfig['validation'])) {
40✔
520
            return true;
1✔
521
        }
522

523
        foreach ($fieldConfig['args'] ?? [] as $argConfig) {
40✔
524
            if (!empty($argConfig['validation'])) {
36✔
525
                return true;
6✔
526
            }
527
        }
528

529
        return false;
36✔
530
    }
531

532
    /**
533
     * Render example:
534
     *
535
     *      [
536
     *          'link' => {@see normalizeLink}
537
     *          'cascade' => [
538
     *              'groups' => ['my_group'],
539
     *          ],
540
     *          'constraints' => {@see buildConstraints}
541
     *      ]
542
     *
543
     * If only constraints provided, uses {@see buildConstraints} directly.
544
     *
545
     * @param array{
546
     *     constraints: array,
547
     *     link: string,
548
     *     cascade: array
549
     * } $config
550
     *
551
     * @throws GeneratorException
552
     */
553
    private function buildValidationRules(array $config): GeneratorInterface
554
    {
555
        // Convert to object for better readability
556
        $c = (object) $config;
5✔
557

558
        $array = Collection::assoc();
5✔
559

560
        if (!empty($c->link)) {
5✔
561
            if (!str_contains($c->link, '::')) {
1✔
562
                // e.g. App\Entity\Droid
563
                $array->addItem('link', $c->link);
1✔
564
            } else {
565
                // e.g. App\Entity\Droid::$id
566
                $array->addItem('link', Collection::numeric($this->normalizeLink($c->link)));
1✔
567
            }
568
        }
569

570
        if (isset($c->cascade)) {
5✔
571
            // If there are only constarainst, use short syntax
572
            if (empty($c->cascade['groups'])) {
3✔
573
                $this->file->addUse(InputValidator::class);
3✔
574

575
                return Literal::new('InputValidator::CASCADE');
3✔
576
            }
577
            $array->addItem('cascade', $c->cascade['groups']);
1✔
578
        }
579

580
        if (!empty($c->constraints)) {
5✔
581
            // If there are only constarainst, use short syntax
582
            if (0 === $array->count()) {
5✔
583
                return $this->buildConstraints($c->constraints);
5✔
584
            }
585
            $array->addItem('constraints', $this->buildConstraints($c->constraints));
1✔
586
        }
587

588
        return $array;
1✔
589
    }
590

591
    /**
592
     * Builds a closure or a numeric multiline array with Symfony Constraint
593
     * instances. The array is used by {@see InputValidator} during requests.
594
     *
595
     * Render example (array):
596
     *
597
     *      [
598
     *          new NotNull(),
599
     *          new Length([
600
     *              'min' => 5,
601
     *              'max' => 10
602
     *          ]),
603
     *          ...
604
     *      ]
605
     *
606
     * Render example (in a closure):
607
     *
608
     *      fn() => [
609
     *          new NotNull(),
610
     *          new Length([
611
     *              'min' => 5,
612
     *              'max' => 10
613
     *          ]),
614
     *          ...
615
     *      ]
616
     *
617
     * @return ArrowFunction|Collection
618
     *
619
     * @throws GeneratorException
620
     */
621
    private function buildConstraints(array $constraints = [], bool $inClosure = true)
622
    {
623
        $result = Collection::numeric()->setMultiline();
5✔
624

625
        foreach ($constraints as $wrapper) {
5✔
626
            $name = key($wrapper);
5✔
627
            $args = reset($wrapper);
5✔
628

629
            if (str_contains($name, '\\')) {
5✔
630
                // Custom constraint
631
                $fqcn = ltrim($name, '\\');
2✔
632
                $instance = Instance::new("@\\$fqcn");
2✔
633
            } else {
634
                // Symfony constraint
635
                $fqcn = static::CONSTRAINTS_NAMESPACE."\\$name";
5✔
636
                $this->file->addUse(static::CONSTRAINTS_NAMESPACE.' as SymfonyConstraints');
5✔
637
                $instance = Instance::new("@SymfonyConstraints\\$name");
5✔
638
            }
639

640
            if (!class_exists($fqcn)) {
5✔
641
                throw new GeneratorException("Constraint class '$fqcn' doesn't exist.");
1✔
642
            }
643

644
            if (is_array($args)) {
4✔
645
                $reflectionClass = new ReflectionClass($fqcn);
2✔
646
                $constructor = $reflectionClass->getConstructor();
2✔
647

648
                if ($this->isSymfony74Plus && isset($args[0]) !== false && $fqcn === Choice::class) {
2✔
649
                    // Handle Choice constraint in Symfony 7.4+
NEW
650
                    $args = ['choices'=>$args];
×
651
                }
652

653
                /*
654
                 * In Symfony 7.4+, we should not pass an array, but split up parameters in different arguments.
655
                 */
656
                $inlineParameters = $this->isSymfony74Plus && isset($args[0]) === false;
2✔
657
                if (null !== $constructor && $inlineParameters === true) {
2✔
NEW
658
                    $parameters = $constructor->getParameters();
×
NEW
659
                    foreach ($parameters as $parameter) {
×
NEW
660
                        $name = $parameter->getName();
×
NEW
661
                        if (isset($args[$name])) {
×
NEW
662
                            $instance->addArgument($args[$name]);
×
NEW
663
                        } elseif ($parameter->isDefaultValueAvailable()) {
×
NEW
664
                            $instance->addArgument($parameter->getDefaultValue());
×
665
                        } else {
NEW
666
                            throw new GeneratorException("Constraint '$fqcn' requires argument '$name'.");
×
667
                        }
668
                    }
669
                }
670

671
                if (isset($args[0]) && is_array($args[0]) && false === $inlineParameters) {
2✔
672
                    // Nested instance
673
                    $instance->addArgument($this->buildConstraints($args, false));
1✔
674
                } elseif (isset($args['constraints'][0]) && is_array($args['constraints'][0]) && false === $inlineParameters) {
2✔
675
                    // Nested instance with "constraints" key (full syntax)
676
                    $options = [
1✔
677
                        'constraints' => $this->buildConstraints($args['constraints'], false),
1✔
678
                    ];
1✔
679

680
                    // Check for additional options
681
                    foreach ($args as $key => $option) {
1✔
682
                        if ('constraints' === $key) {
1✔
683
                            continue;
1✔
684
                        }
685
                        $options[$key] = $option;
1✔
686
                    }
687

688
                    $instance->addArgument($options);
1✔
689
                } elseif (false === $inlineParameters) {
2✔
690
                    // Numeric or Assoc array?
691
                    $instance->addArgument(isset($args[0]) ? $args : Collection::assoc($args));
2✔
692
                }
693
            } elseif (null !== $args) {
4✔
694
                $instance->addArgument($args);
1✔
695
            }
696

697
            $result->push($instance);
4✔
698
        }
699

700
        if ($inClosure) {
4✔
701
            return ArrowFunction::new($result);
4✔
702
        }
703

704
        return $result; // @phpstan-ignore-line
1✔
705
    }
706

707
    /**
708
     * Render example:
709
     *
710
     *      [
711
     *          'type' => {@see buildType},
712
     *          'description' => 'Some description.',
713
     *          'deprecationReason' => 'This field will be removed soon.',
714
     *          'args' => fn() => [
715
     *              {@see buildArg},
716
     *              {@see buildArg},
717
     *               ...
718
     *           ],
719
     *          'resolve' => {@see buildResolve},
720
     *          'complexity' => {@see buildComplexity},
721
     *      ]
722
     *
723
     * @param array{
724
     *     type:              string,
725
     *     resolve?:          string,
726
     *     description?:      string,
727
     *     args?:             array,
728
     *     complexity?:       string,
729
     *     deprecatedReason?: string,
730
     *     validation?:       array,
731
     * } $fieldConfig
732
     *
733
     * @internal
734
     *
735
     * @return GeneratorInterface|Collection|string
736
     *
737
     * @throws GeneratorException
738
     */
739
    public function buildField(array $fieldConfig, string $fieldname)
740
    {
741
        $this->currentField = $fieldname;
45✔
742

743
        // Convert to object for better readability
744
        $c = (object) $fieldConfig;
45✔
745

746
        // TODO(any): modify `InputValidator` and `TypeDecoratorListener` to support it before re-enabling this
747
        // see https://github.com/overblog/GraphQLBundle/issues/973
748
        // If there is only 'type', use shorthand
749
        /*if (1 === count($fieldConfig) && isset($c->type)) {
750
            return $this->buildType($c->type);
751
        }*/
752

753
        $field = Collection::assoc()
45✔
754
            ->addItem('type', $this->buildType($c->type));
45✔
755

756
        // only for object types
757
        if (isset($c->resolve)) {
45✔
758
            if (isset($c->validation)) {
42✔
759
                $field->addItem('validation', $this->buildValidationRules($c->validation));
1✔
760
            }
761
            $field->addItem('resolve', $this->buildResolve($c->resolve, $fieldConfig['validationGroups'] ?? null));
42✔
762
        }
763

764
        if (isset($c->deprecationReason)) {
44✔
765
            $field->addItem('deprecationReason', $c->deprecationReason);
2✔
766
        }
767

768
        if (isset($c->description)) {
44✔
769
            $field->addItem('description', $c->description);
42✔
770
        }
771

772
        if (!empty($c->args)) {
44✔
773
            $field->addItem('args', Collection::map($c->args, [$this, 'buildArg'], false));
36✔
774
        }
775

776
        if (isset($c->complexity)) {
42✔
777
            $field->addItem('complexity', $this->buildComplexity($c->complexity));
5✔
778
        }
779

780
        if (isset($c->public)) {
42✔
781
            $field->addItem('public', $this->buildPublic($c->public));
3✔
782
        }
783

784
        if (isset($c->access)) {
42✔
785
            $field->addItem('access', $this->buildAccess($c->access));
4✔
786
        }
787

788
        if (!empty($c->access) && is_string($c->access) && EL::expressionContainsVar('object', $c->access)) {
42✔
789
            $field->addItem('useStrictAccess', false);
2✔
790
        }
791

792
        if ('input-object' === $this->type) {
42✔
793
            if (property_exists($c, 'defaultValue')) {
17✔
794
                $field->addItem('defaultValue', $c->defaultValue);
4✔
795
            }
796

797
            if (isset($c->validation)) {
17✔
798
                $field->addItem('validation', $this->buildValidationRules($c->validation));
3✔
799
            }
800
        }
801

802
        return $field;
42✔
803
    }
804

805
    /**
806
     * Render example:
807
     * <code>
808
     *  [
809
     *      'name' => 'username',
810
     *      'type' => {@see buildType},
811
     *      'description' => 'Some fancy description.',
812
     *      'defaultValue' => 'admin',
813
     *  ]
814
     * </code>
815
     *
816
     * @param array{
817
     *     type: string,
818
     *     description?: string,
819
     *     defaultValue?: string
820
     * } $argConfig
821
     *
822
     * @internal
823
     *
824
     * @throws GeneratorException
825
     */
826
    public function buildArg(array $argConfig, string $argName): Collection
827
    {
828
        // Convert to object for better readability
829
        $c = (object) $argConfig;
36✔
830

831
        $arg = Collection::assoc()
36✔
832
            ->addItem('name', $argName)
36✔
833
            ->addItem('type', $this->buildType($c->type));
36✔
834

835
        if (isset($c->description)) {
36✔
836
            $arg->addIfNotEmpty('description', $c->description);
3✔
837
        }
838

839
        if (property_exists($c, 'defaultValue')) {
36✔
840
            $arg->addItem('defaultValue', $c->defaultValue);
3✔
841
        }
842

843
        if (!empty($c->validation)) {
36✔
844
            if (in_array($c->type, self::BUILT_IN_TYPES) && isset($c->validation['cascade'])) {
6✔
845
                throw new GeneratorException('Cascade validation cannot be applied to built-in types.');
1✔
846
            }
847

848
            $arg->addIfNotEmpty('validation', $this->buildValidationRules($c->validation));
5✔
849
        }
850

851
        return $arg;
34✔
852
    }
853

854
    /**
855
     * Builds a closure or an arrow function, depending on whether the `args` param is provided.
856
     *
857
     * Render example (closure):
858
     *
859
     *      function ($value, $arguments) use ($services) {
860
     *          $args = $services->get('argumentFactory')->create($arguments);
861
     *          return ($args['age'] + 5);
862
     *      }
863
     *
864
     * Render example (arrow function):
865
     *
866
     *      fn($childrenComplexity) => ($childrenComplexity + 20);
867
     *
868
     * @param mixed $complexity
869
     *
870
     * @return Closure|mixed
871
     */
872
    private function buildComplexity($complexity)
873
    {
874
        if (EL::isStringWithTrigger($complexity)) {
5✔
875
            $expression = $this->expressionConverter->convert($complexity);
4✔
876

877
            if (EL::expressionContainsVar('args', $complexity)) {
4✔
878
                return Closure::new()
4✔
879
                    ->addArgument('childrenComplexity')
4✔
880
                    ->addArgument('arguments', '', [])
4✔
881
                    ->bindVar(TypeGenerator::GRAPHQL_SERVICES)
4✔
882
                    ->append('$args = ', "$this->gqlServices->get('argumentFactory')->create(\$arguments)")
4✔
883
                    ->append('return ', $expression)
4✔
884
                ;
4✔
885
            }
886

887
            $arrow = ArrowFunction::new(is_string($expression) ? new Literal($expression) : $expression);
4✔
888

889
            if (EL::expressionContainsVar('childrenComplexity', $complexity)) {
4✔
890
                $arrow->addArgument('childrenComplexity');
4✔
891
            }
892

893
            return $arrow;
4✔
894
        }
895

896
        return new ArrowFunction(0);
1✔
897
    }
898

899
    /**
900
     * Builds an arrow function from a string with an expression prefix,
901
     * otherwise just returns the provided value back untouched.
902
     *
903
     * Render example (if expression):
904
     *
905
     *      fn($fieldName, $typeName = self::NAME) => ($fieldName == "name")
906
     *
907
     * @param mixed $public
908
     *
909
     * @return ArrowFunction|mixed
910
     */
911
    private function buildPublic($public)
912
    {
913
        if (EL::isStringWithTrigger($public)) {
3✔
914
            $expression = $this->expressionConverter->convert($public);
2✔
915
            $arrow = ArrowFunction::new(Literal::new($expression));
2✔
916

917
            if (EL::expressionContainsVar('fieldName', $public)) {
2✔
918
                $arrow->addArgument('fieldName');
2✔
919
            }
920

921
            if (EL::expressionContainsVar('typeName', $public)) {
2✔
922
                $arrow->addArgument('fieldName');
2✔
923
                $arrow->addArgument('typeName', '', new Literal('self::NAME'));
2✔
924
            }
925

926
            return $arrow;
2✔
927
        }
928

929
        return $public;
1✔
930
    }
931

932
    /**
933
     * Builds an arrow function from a string with an expression prefix,
934
     * otherwise just returns the provided value back untouched.
935
     *
936
     * Render example (if expression):
937
     *
938
     *      fn($value, $args, $context, $info, $object) => $services->get('private_service')->hasAccess()
939
     *
940
     * @param mixed $access
941
     *
942
     * @return ArrowFunction|mixed
943
     */
944
    private function buildAccess($access)
945
    {
946
        if (EL::isStringWithTrigger($access)) {
4✔
947
            $expression = $this->expressionConverter->convert($access);
4✔
948

949
            return ArrowFunction::new()
4✔
950
                ->addArguments('value', 'args', 'context', 'info', 'object')
4✔
951
                ->setExpression(Literal::new($expression));
4✔
952
        }
953

954
        return $access;
2✔
955
    }
956

957
    /**
958
     * Builds an arrow function from a string with an expression prefix,
959
     * otherwise just returns the provided value back untouched.
960
     *
961
     * Render example:
962
     *
963
     *      fn($value, $context, $info) => $services->getType($value)
964
     *
965
     * @param mixed $resolveType
966
     *
967
     * @return mixed|ArrowFunction
968
     */
969
    private function buildResolveType($resolveType)
970
    {
971
        if (EL::isStringWithTrigger($resolveType)) {
6✔
972
            $expression = $this->expressionConverter->convert($resolveType);
5✔
973

974
            return ArrowFunction::new()
5✔
975
                ->addArguments('value', 'context', 'info')
5✔
976
                ->setExpression(Literal::new($expression));
5✔
977
        }
978

979
        return $resolveType;
2✔
980
    }
981

982
    /**
983
     * Builds an arrow function from a string with an expression prefix,
984
     * otherwise just returns the provided value back untouched.
985
     *
986
     * Render example:
987
     *
988
     *      fn($className) => (($className = "App\\ClassName") && $value instanceof $className)
989
     *
990
     * @param mixed $isTypeOf
991
     */
992
    private function buildIsTypeOf($isTypeOf): ArrowFunction
993
    {
994
        if (EL::isStringWithTrigger($isTypeOf)) {
2✔
995
            $expression = $this->expressionConverter->convert($isTypeOf);
×
996

997
            return ArrowFunction::new(Literal::new($expression), 'bool')
×
UNCOV
998
                ->setStatic()
×
UNCOV
999
                ->addArguments('value', 'context')
×
UNCOV
1000
                ->addArgument('info', ResolveInfo::class);
×
1001
        }
1002

1003
        return ArrowFunction::new($isTypeOf);
2✔
1004
    }
1005

1006
    /**
1007
     * Creates and array from a formatted string.
1008
     *
1009
     * Examples:
1010
     *
1011
     *      "App\Entity\User::$firstName"  -> ['App\Entity\User', 'firstName', 'property']
1012
     *      "App\Entity\User::firstName()" -> ['App\Entity\User', 'firstName', 'getter']
1013
     *      "App\Entity\User::firstName"   -> ['App\Entity\User', 'firstName', 'member']
1014
     */
1015
    private function normalizeLink(string $link): array
1016
    {
1017
        [$fqcn, $classMember] = explode('::', $link);
1✔
1018

1019
        if ('$' === $classMember[0]) {
1✔
1020
            return [$fqcn, ltrim($classMember, '$'), 'property'];
1✔
1021
        } elseif (')' === substr($classMember, -1)) {
1✔
1022
            return [$fqcn, rtrim($classMember, '()'), 'getter'];
1✔
1023
        }
1024

1025
        return [$fqcn, $classMember, 'member'];
1✔
1026
    }
1027
}
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

© 2026 Coveralls, Inc