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

overblog / GraphQLBundle / 21443229552

28 Jan 2026 02:58PM UTC coverage: 98.568% (+0.005%) from 98.563%
21443229552

Pull #1228

github

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

16 of 16 new or added lines in 2 files covered. (100.0%)

3 existing lines in 1 file now uncovered.

4543 of 4609 relevant lines covered (98.57%)

39.2 hits per line

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

97.92
/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

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

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

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

79
    private ExpressionConverter $expressionConverter;
80
    private PhpFile $file;
81
    private string $namespace;
82
    private array $config;
83
    private string $type;
84
    private string $currentField;
85
    private string $gqlServices = '$'.TypeGenerator::GRAPHQL_SERVICES;
86

87
    public function __construct(ExpressionConverter $expressionConverter, string $namespace)
88
    {
89
        $this->expressionConverter = $expressionConverter;
163✔
90
        $this->namespace = $namespace;
163✔
91

92
        // Register additional converter in the php code generator
93
        Config::registerConverter($expressionConverter, ConverterInterface::TYPE_STRING);
163✔
94
    }
95

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

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

122
        $class = $this->file->createClass($config['class_name'])
45✔
123
            ->setFinal()
45✔
124
            ->setExtends(static::EXTENDS[$type])
45✔
125
            ->addImplements(GeneratedTypeInterface::class, AliasedInterface::class)
45✔
126
            ->addConst('NAME', $config['name'])
45✔
127
            ->setDocBlock(static::DOCBLOCK_TEXT);
45✔
128

129
        $class->emptyLine();
45✔
130

131
        $class->createConstructor()
45✔
132
            ->addArgument('configProcessor', ConfigProcessor::class)
45✔
133
            ->addArgument(TypeGenerator::GRAPHQL_SERVICES, GraphQLServices::class)
45✔
134
            ->append('$config = ', $this->buildConfig($config))
45✔
135
            ->emptyLine()
45✔
136
            ->append('parent::__construct($configProcessor->process($config))');
45✔
137

138
        $class->createMethod('getAliases', 'public')
42✔
139
            ->setStatic()
42✔
140
            ->setReturnType('array')
42✔
141
            ->setDocBlock('{@inheritdoc}')
42✔
142
            ->append('return [self::NAME]');
42✔
143

144
        return $this->file;
42✔
145
    }
146

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

165
        $isReference = false;
45✔
166
        $type = $this->wrapTypeRecursive($typeNode, $isReference);
45✔
167

168
        if ($isReference) {
45✔
169
            // References to other types should be wrapped in a closure
170
            // for performance reasons
171
            return ArrowFunction::new($type);
35✔
172
        }
173

174
        return $type;
45✔
175
    }
176

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

210
        return $type;
45✔
211
    }
212

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

302
        $configLoader = Collection::assoc();
45✔
303
        $configLoader->addItem('name', new Literal('self::NAME'));
45✔
304

305
        if (isset($c->description)) {
45✔
306
            $configLoader->addItem('description', $c->description);
42✔
307
        }
308

309
        // only by input-object types (for class level validation)
310
        if (isset($c->validation)) {
45✔
311
            $configLoader->addItem('validation', $this->buildValidationRules($c->validation));
1✔
312
        }
313

314
        // only by object, input-object and interface types
315
        if (!empty($c->fields)) {
45✔
316
            $configLoader->addItem('fields', ArrowFunction::new(
45✔
317
                Collection::map($c->fields, [$this, 'buildField'])
45✔
318
            ));
45✔
319
        }
320

321
        if (!empty($c->interfaces)) {
42✔
322
            $items = array_map(fn ($type) => "$this->gqlServices->getType('$type')", $c->interfaces);
9✔
323
            $configLoader->addItem('interfaces', ArrowFunction::new(Collection::numeric($items, true)));
9✔
324
        }
325

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

331
        if (isset($c->resolveType)) {
42✔
332
            $configLoader->addItem('resolveType', $this->buildResolveType($c->resolveType));
6✔
333
        }
334

335
        if (isset($c->isTypeOf)) {
42✔
336
            $configLoader->addItem('isTypeOf', $this->buildIsTypeOf($c->isTypeOf));
2✔
337
        }
338

339
        if (isset($c->resolveField)) {
42✔
340
            $configLoader->addItem('resolveField', $this->buildResolve($c->resolveField));
4✔
341
        }
342

343
        // only by enum types
344
        if (isset($c->values)) {
42✔
345
            $configLoader->addItem('values', Collection::assoc($c->values));
10✔
346
        }
347
        if (isset($c->enumClass)) {
42✔
348
            $configLoader->addItem('enumClass', $c->enumClass);
2✔
349
        }
350

351
        // only by custom-scalar types
352
        if ('custom-scalar' === $this->type) {
42✔
353
            if (isset($c->scalarType)) {
6✔
354
                $configLoader->addItem('scalarType', $c->scalarType);
2✔
355
            }
356

357
            if (isset($c->serialize)) {
6✔
358
                $configLoader->addItem('serialize', $this->buildScalarCallback($c->serialize, 'serialize'));
4✔
359
            }
360

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

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

370
        return $configLoader;
42✔
371
    }
372

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

392
        $closure = new ArrowFunction();
4✔
393

394
        if (!is_string($callback)) {
4✔
395
            [$class, $method] = $callback;
4✔
396
        } else {
397
            [$class, $method] = explode('::', $callback);
2✔
398
        }
399

400
        $className = Utils::resolveQualifier($class);
4✔
401

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

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

412
        return $closure;
4✔
413
    }
414

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

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

466
            $injectValidator = EL::expressionContainsVar('validator', $resolve);
40✔
467

468
            if ($this->configContainsValidation()) {
40✔
469
                $injectErrors = EL::expressionContainsVar('errors', $resolve);
6✔
470

471
                if ($injectErrors) {
6✔
472
                    $closure->append('$errors = ', Instance::new(ResolveErrors::class));
1✔
473
                }
474

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

477
                // If auto-validation on or errors are injected
478
                if (!$injectValidator || $injectErrors) {
6✔
479
                    if (!empty($groups)) {
3✔
480
                        $validationGroups = Collection::numeric($groups);
1✔
481
                    } else {
482
                        $validationGroups = 'null';
3✔
483
                    }
484

485
                    $closure->emptyLine();
3✔
486

487
                    if ($injectErrors) {
3✔
488
                        $closure->append('$errors->setValidationErrors($validator->validate(', $validationGroups, ', false))');
1✔
489
                    } else {
490
                        $closure->append('$validator->validate(', $validationGroups, ')');
3✔
491
                    }
492

493
                    $closure->emptyLine();
6✔
494
                }
495
            } elseif ($injectValidator) {
36✔
496
                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✔
497
            }
498

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

501
            return $closure;
39✔
502
        }
503

504
        return ArrowFunction::new($resolve);
24✔
505
    }
506

507
    /**
508
     * Checks if given config contains any validation rules.
509
     */
510
    private function configContainsValidation(): bool
511
    {
512
        $fieldConfig = $this->config['fields'][$this->currentField];
40✔
513

514
        if (!empty($fieldConfig['validation'])) {
40✔
515
            return true;
1✔
516
        }
517

518
        foreach ($fieldConfig['args'] ?? [] as $argConfig) {
40✔
519
            if (!empty($argConfig['validation'])) {
36✔
520
                return true;
6✔
521
            }
522
        }
523

524
        return false;
36✔
525
    }
526

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

553
        $array = Collection::assoc();
5✔
554

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

565
        if (isset($c->cascade)) {
5✔
566
            // If there are only constarainst, use short syntax
567
            if (empty($c->cascade['groups'])) {
3✔
568
                $this->file->addUse(InputValidator::class);
3✔
569

570
                return Literal::new('InputValidator::CASCADE');
3✔
571
            }
572
            $array->addItem('cascade', $c->cascade['groups']);
1✔
573
        }
574

575
        if (!empty($c->constraints)) {
5✔
576
            // If there are only constarainst, use short syntax
577
            if (0 === $array->count()) {
5✔
578
                return $this->buildConstraints($c->constraints);
5✔
579
            }
580
            $array->addItem('constraints', $this->buildConstraints($c->constraints));
1✔
581
        }
582

583
        return $array;
1✔
584
    }
585

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

620
        foreach ($constraints as $wrapper) {
5✔
621
            $name = key($wrapper);
5✔
622
            $args = reset($wrapper);
5✔
623

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

635
            if (!class_exists($fqcn)) {
5✔
636
                throw new GeneratorException("Constraint class '$fqcn' doesn't exist.");
1✔
637
            }
638

639
            if (is_array($args)) {
4✔
640
                $reflectionClass = new ReflectionClass($fqcn);
2✔
641
                $constructor = $reflectionClass->getConstructor();
2✔
642

643
                $validatorVersion = InstalledVersions::getVersion('symfony/validator');
2✔
644

645
                $inlineParameters = false;
2✔
646
                // if (null !== $constructor && version_compare($validatorVersion, '7.4', '>=')) {
647
                if (null !== $constructor) {
2✔
648
                    $parameterNames = [];
2✔
649
                    $parameters = $constructor->getParameters();
2✔
650
                    foreach ($parameters as $parameter) {
2✔
651
                        $name = $parameter->getName();
2✔
652
                        $parameterNames[] = $name;
2✔
653
                    }
654

655
                    $checkedPosition = 0;
2✔
656
                    foreach ($args as $key => $value) {
2✔
657
                        if (
658
                            true === isset($parameterNames[$checkedPosition])
2✔
659
                            && $parameterNames[$checkedPosition++] === $key
2✔
660
                        ) {
661
                            $instance->addArgument($value);
2✔
662
                            $inlineParameters = true;
2✔
663
                        }
664
                    }
665
                }
666

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

676
                    // Check for additional options
677
                    foreach ($args as $key => $option) {
1✔
678
                        if ('constraints' === $key) {
1✔
679
                            continue;
1✔
680
                        }
681
                        $options[$key] = $option;
1✔
682
                    }
683

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

693
            $result->push($instance);
4✔
694
        }
695

696
        if ($inClosure) {
4✔
697
            return ArrowFunction::new($result);
4✔
698
        }
699

700
        return $result; // @phpstan-ignore-line
1✔
701
    }
702

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

739
        // Convert to object for better readability
740
        $c = (object) $fieldConfig;
45✔
741

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

749
        $field = Collection::assoc()
45✔
750
            ->addItem('type', $this->buildType($c->type));
45✔
751

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

760
        if (isset($c->deprecationReason)) {
44✔
761
            $field->addItem('deprecationReason', $c->deprecationReason);
2✔
762
        }
763

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

768
        if (!empty($c->args)) {
44✔
769
            $field->addItem('args', Collection::map($c->args, [$this, 'buildArg'], false));
36✔
770
        }
771

772
        if (isset($c->complexity)) {
42✔
773
            $field->addItem('complexity', $this->buildComplexity($c->complexity));
5✔
774
        }
775

776
        if (isset($c->public)) {
42✔
777
            $field->addItem('public', $this->buildPublic($c->public));
3✔
778
        }
779

780
        if (isset($c->access)) {
42✔
781
            $field->addItem('access', $this->buildAccess($c->access));
4✔
782
        }
783

784
        if (!empty($c->access) && is_string($c->access) && EL::expressionContainsVar('object', $c->access)) {
42✔
785
            $field->addItem('useStrictAccess', false);
2✔
786
        }
787

788
        if ('input-object' === $this->type) {
42✔
789
            if (property_exists($c, 'defaultValue')) {
17✔
790
                $field->addItem('defaultValue', $c->defaultValue);
4✔
791
            }
792

793
            if (isset($c->validation)) {
17✔
794
                $field->addItem('validation', $this->buildValidationRules($c->validation));
3✔
795
            }
796
        }
797

798
        return $field;
42✔
799
    }
800

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

827
        $arg = Collection::assoc()
36✔
828
            ->addItem('name', $argName)
36✔
829
            ->addItem('type', $this->buildType($c->type));
36✔
830

831
        if (isset($c->description)) {
36✔
832
            $arg->addIfNotEmpty('description', $c->description);
3✔
833
        }
834

835
        if (property_exists($c, 'defaultValue')) {
36✔
836
            $arg->addItem('defaultValue', $c->defaultValue);
3✔
837
        }
838

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

844
            $arg->addIfNotEmpty('validation', $this->buildValidationRules($c->validation));
5✔
845
        }
846

847
        return $arg;
34✔
848
    }
849

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

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

883
            $arrow = ArrowFunction::new(is_string($expression) ? new Literal($expression) : $expression);
4✔
884

885
            if (EL::expressionContainsVar('childrenComplexity', $complexity)) {
4✔
886
                $arrow->addArgument('childrenComplexity');
4✔
887
            }
888

889
            return $arrow;
4✔
890
        }
891

892
        return new ArrowFunction(0);
1✔
893
    }
894

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

913
            if (EL::expressionContainsVar('fieldName', $public)) {
2✔
914
                $arrow->addArgument('fieldName');
2✔
915
            }
916

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

922
            return $arrow;
2✔
923
        }
924

925
        return $public;
1✔
926
    }
927

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

945
            return ArrowFunction::new()
4✔
946
                ->addArguments('value', 'args', 'context', 'info', 'object')
4✔
947
                ->setExpression(Literal::new($expression));
4✔
948
        }
949

950
        return $access;
2✔
951
    }
952

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

970
            return ArrowFunction::new()
5✔
971
                ->addArguments('value', 'context', 'info')
5✔
972
                ->setExpression(Literal::new($expression));
5✔
973
        }
974

975
        return $resolveType;
2✔
976
    }
977

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

UNCOV
993
            return ArrowFunction::new(Literal::new($expression), 'bool')
×
994
                ->setStatic()
×
995
                ->addArguments('value', 'context')
×
996
                ->addArgument('info', ResolveInfo::class);
×
997
        }
998

999
        return ArrowFunction::new($isTypeOf);
2✔
1000
    }
1001

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

1015
        if ('$' === $classMember[0]) {
1✔
1016
            return [$fqcn, ltrim($classMember, '$'), 'property'];
1✔
1017
        } elseif (')' === substr($classMember, -1)) {
1✔
1018
            return [$fqcn, rtrim($classMember, '()'), 'getter'];
1✔
1019
        }
1020

1021
        return [$fqcn, $classMember, 'member'];
1✔
1022
    }
1023
}
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