Passed
Branch main (339f6d)
by Cornelia
17:44 queued 07:47
created

ConfigBuilderGenerator   F

Complexity

Total Complexity 88

Size/Duplication

Total Lines 564
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 88
eloc 267
dl 0
loc 564
rs 2
c 0
b 0
f 0

18 Methods

Rating   Name   Duplication   Size   Complexity  
B buildToArray() 0 33 7
A hasNormalizationClosures() 0 9 2
B buildConstructor() 0 44 7
A __construct() 0 3 1
A getFullPath() 0 8 2
A getType() 0 3 2
A writeClasses() 0 14 3
A buildNode() 0 13 3
A handleVariableNode() 0 22 2
B getParameterTypes() 0 27 10
A handleScalarNode() 0 19 1
A getSingularName() 0 17 5
A handleArrayNode() 0 62 5
A getSubNamespace() 0 3 1
A buildSetExtraKey() 0 11 2
C getComment() 0 37 12
D handlePrototypedArrayNode() 0 148 21
A build() 0 27 2

How to fix   Complexity   

Complex Class

Complex classes like ConfigBuilderGenerator often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ConfigBuilderGenerator, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/*
4
 * This file is part of the Symfony package.
5
 *
6
 * (c) Fabien Potencier <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Symfony\Component\Config\Builder;
13
14
use Symfony\Component\Config\Definition\ArrayNode;
15
use Symfony\Component\Config\Definition\BaseNode;
16
use Symfony\Component\Config\Definition\BooleanNode;
17
use Symfony\Component\Config\Definition\Builder\ExprBuilder;
18
use Symfony\Component\Config\Definition\ConfigurationInterface;
19
use Symfony\Component\Config\Definition\EnumNode;
20
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
21
use Symfony\Component\Config\Definition\FloatNode;
22
use Symfony\Component\Config\Definition\IntegerNode;
23
use Symfony\Component\Config\Definition\NodeInterface;
24
use Symfony\Component\Config\Definition\PrototypedArrayNode;
25
use Symfony\Component\Config\Definition\ScalarNode;
26
use Symfony\Component\Config\Definition\VariableNode;
27
use Symfony\Component\Config\Loader\ParamConfigurator;
28
29
/**
30
 * Generate ConfigBuilders to help create valid config.
31
 *
32
 * @author Tobias Nyholm <[email protected]>
33
 */
34
class ConfigBuilderGenerator implements ConfigBuilderGeneratorInterface
35
{
36
    /**
37
     * @var ClassBuilder[]
38
     */
39
    private array $classes = [];
40
41
    public function __construct(
42
        private string $outputDir,
43
    ) {
44
    }
45
46
    /**
47
     * @return \Closure that will return the root config class
48
     */
49
    public function build(ConfigurationInterface $configuration): \Closure
50
    {
51
        $this->classes = [];
52
53
        $rootNode = $configuration->getConfigTreeBuilder()->buildTree();
54
        $rootClass = new ClassBuilder('Symfony\\Config', $rootNode->getName());
55
56
        $path = $this->getFullPath($rootClass);
57
        if (!is_file($path)) {
58
            // Generate the class if the file not exists
59
            $this->classes[] = $rootClass;
60
            $this->buildNode($rootNode, $rootClass, $this->getSubNamespace($rootClass));
61
            $rootClass->addImplements(ConfigBuilderInterface::class);
62
            $rootClass->addMethod('getExtensionAlias', '
63
public function NAME(): string
64
{
65
    return \'ALIAS\';
66
}', ['ALIAS' => $rootNode->getPath()]);
67
68
            $this->writeClasses();
69
        }
70
71
        return function () use ($path, $rootClass) {
72
            require_once $path;
73
            $className = $rootClass->getFqcn();
74
75
            return new $className();
76
        };
77
    }
78
79
    private function getFullPath(ClassBuilder $class): string
80
    {
81
        $directory = $this->outputDir.\DIRECTORY_SEPARATOR.$class->getDirectory();
82
        if (!is_dir($directory)) {
83
            @mkdir($directory, 0777, true);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for mkdir(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

83
            /** @scrutinizer ignore-unhandled */ @mkdir($directory, 0777, true);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
84
        }
85
86
        return $directory.\DIRECTORY_SEPARATOR.$class->getFilename();
87
    }
88
89
    private function writeClasses(): void
90
    {
91
        foreach ($this->classes as $class) {
92
            $this->buildConstructor($class);
93
            $this->buildToArray($class);
94
            if ($class->getProperties()) {
95
                $class->addProperty('_usedProperties', null, '[]');
96
            }
97
            $this->buildSetExtraKey($class);
98
99
            file_put_contents($this->getFullPath($class), $class->build());
100
        }
101
102
        $this->classes = [];
103
    }
104
105
    private function buildNode(NodeInterface $node, ClassBuilder $class, string $namespace): void
106
    {
107
        if (!$node instanceof ArrayNode) {
108
            throw new \LogicException('The node was expected to be an ArrayNode. This Configuration includes an edge case not supported yet.');
109
        }
110
111
        foreach ($node->getChildren() as $child) {
112
            match (true) {
113
                $child instanceof ScalarNode => $this->handleScalarNode($child, $class),
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->handleScalarNode($child, $class) targeting Symfony\Component\Config...tor::handleScalarNode() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
114
                $child instanceof PrototypedArrayNode => $this->handlePrototypedArrayNode($child, $class, $namespace),
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->handlePrototypedA...ld, $class, $namespace) targeting Symfony\Component\Config...lePrototypedArrayNode() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
115
                $child instanceof VariableNode => $this->handleVariableNode($child, $class),
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->handleVariableNode($child, $class) targeting Symfony\Component\Config...r::handleVariableNode() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
116
                $child instanceof ArrayNode => $this->handleArrayNode($child, $class, $namespace),
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->handleArrayNode($...ld, $class, $namespace) targeting Symfony\Component\Config...ator::handleArrayNode() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
117
                default => throw new \RuntimeException(\sprintf('Unknown node "%s".', $child::class)),
118
            };
119
        }
120
    }
121
122
    private function handleArrayNode(ArrayNode $node, ClassBuilder $class, string $namespace): void
123
    {
124
        $childClass = new ClassBuilder($namespace, $node->getName());
125
        $childClass->setAllowExtraKeys($node->shouldIgnoreExtraKeys());
126
        $class->addRequire($childClass);
127
        $this->classes[] = $childClass;
128
129
        $hasNormalizationClosures = $this->hasNormalizationClosures($node);
130
        $comment = $this->getComment($node);
131
        if ($hasNormalizationClosures) {
132
            $comment = \sprintf(" * @template TValue\n * @param TValue \$value\n%s", $comment);
133
            $comment .= \sprintf(' * @return %s|$this'."\n", $childClass->getFqcn());
134
            $comment .= \sprintf(' * @psalm-return (TValue is array ? %s : static)'."\n ", $childClass->getFqcn());
135
        }
136
        if ('' !== $comment) {
137
            $comment = "/**\n$comment*/\n";
138
        }
139
140
        $property = $class->addProperty(
141
            $node->getName(),
142
            $this->getType($childClass->getFqcn(), $hasNormalizationClosures)
143
        );
144
        $nodeTypes = $this->getParameterTypes($node);
145
        $body = $hasNormalizationClosures ? '
146
COMMENTpublic function NAME(PARAM_TYPE $value = []): CLASS|static
147
{
148
    if (!\is_array($value)) {
149
        $this->_usedProperties[\'PROPERTY\'] = true;
150
        $this->PROPERTY = $value;
151
152
        return $this;
153
    }
154
155
    if (!$this->PROPERTY instanceof CLASS) {
156
        $this->_usedProperties[\'PROPERTY\'] = true;
157
        $this->PROPERTY = new CLASS($value);
158
    } elseif (0 < \func_num_args()) {
159
        throw new InvalidConfigurationException(\'The node created by "NAME()" has already been initialized. You cannot pass values the second time you call NAME().\');
160
    }
161
162
    return $this->PROPERTY;
163
}' : '
164
COMMENTpublic function NAME(array $value = []): CLASS
165
{
166
    if (null === $this->PROPERTY) {
167
        $this->_usedProperties[\'PROPERTY\'] = true;
168
        $this->PROPERTY = new CLASS($value);
169
    } elseif (0 < \func_num_args()) {
170
        throw new InvalidConfigurationException(\'The node created by "NAME()" has already been initialized. You cannot pass values the second time you call NAME().\');
171
    }
172
173
    return $this->PROPERTY;
174
}';
175
        $class->addUse(InvalidConfigurationException::class);
176
        $class->addMethod($node->getName(), $body, [
177
            'COMMENT' => $comment,
178
            'PROPERTY' => $property->getName(),
179
            'CLASS' => $childClass->getFqcn(),
180
            'PARAM_TYPE' => \in_array('mixed', $nodeTypes, true) ? 'mixed' : implode('|', $nodeTypes),
181
        ]);
182
183
        $this->buildNode($node, $childClass, $this->getSubNamespace($childClass));
184
    }
185
186
    private function handleVariableNode(VariableNode $node, ClassBuilder $class): void
187
    {
188
        $comment = $this->getComment($node);
189
        $property = $class->addProperty($node->getName());
190
        $class->addUse(ParamConfigurator::class);
191
192
        $body = '
193
/**
194
COMMENT *
195
 * @return $this
196
 */
197
public function NAME(mixed $valueDEFAULT): static
198
{
199
    $this->_usedProperties[\'PROPERTY\'] = true;
200
    $this->PROPERTY = $value;
201
202
    return $this;
203
}';
204
        $class->addMethod($node->getName(), $body, [
205
            'PROPERTY' => $property->getName(),
206
            'COMMENT' => $comment,
207
            'DEFAULT' => $node->hasDefaultValue() ? ' = '.var_export($node->getDefaultValue(), true) : '',
208
        ]);
209
    }
210
211
    private function handlePrototypedArrayNode(PrototypedArrayNode $node, ClassBuilder $class, string $namespace): void
212
    {
213
        $name = $this->getSingularName($node);
214
        $prototype = $node->getPrototype();
215
        $methodName = $name;
216
        $hasNormalizationClosures = $this->hasNormalizationClosures($node) || $this->hasNormalizationClosures($prototype);
217
218
        $nodeParameterTypes = $this->getParameterTypes($node);
219
        $prototypeParameterTypes = $this->getParameterTypes($prototype);
220
        if (!$prototype instanceof ArrayNode || ($prototype instanceof PrototypedArrayNode && $prototype->getPrototype() instanceof ScalarNode)) {
221
            $class->addUse(ParamConfigurator::class);
222
            $property = $class->addProperty($node->getName());
223
            if (null === $key = $node->getKeyAttribute()) {
224
                // This is an array of values; don't use singular name
225
                $nodeTypesWithoutArray = array_filter($nodeParameterTypes, static fn ($type) => 'array' !== $type);
226
                $body = '
227
/**
228
 * @param ParamConfigurator|list<ParamConfigurator|PROTOTYPE_TYPE>EXTRA_TYPE $value
229
 *
230
 * @return $this
231
 */
232
public function NAME(PARAM_TYPE $value): static
233
{
234
    $this->_usedProperties[\'PROPERTY\'] = true;
235
    $this->PROPERTY = $value;
236
237
    return $this;
238
}';
239
240
                $class->addMethod($node->getName(), $body, [
241
                    'PROPERTY' => $property->getName(),
242
                    'PROTOTYPE_TYPE' => implode('|', $prototypeParameterTypes),
243
                    'EXTRA_TYPE' => $nodeTypesWithoutArray ? '|'.implode('|', $nodeTypesWithoutArray) : '',
244
                    'PARAM_TYPE' => \in_array('mixed', $nodeParameterTypes, true) ? 'mixed' : 'ParamConfigurator|'.implode('|', $nodeParameterTypes),
245
                ]);
246
            } else {
247
                $body = '
248
/**
249
 * @return $this
250
 */
251
public function NAME(string $VAR, TYPE $VALUE): static
252
{
253
    $this->_usedProperties[\'PROPERTY\'] = true;
254
    $this->PROPERTY[$VAR] = $VALUE;
255
256
    return $this;
257
}';
258
259
                $class->addMethod($methodName, $body, [
260
                    'PROPERTY' => $property->getName(),
261
                    'TYPE' => \in_array('mixed', $prototypeParameterTypes, true) ? 'mixed' : 'ParamConfigurator|'.implode('|', $prototypeParameterTypes),
262
                    'VAR' => '' === $key ? 'key' : $key,
263
                    'VALUE' => 'value' === $key ? 'data' : 'value',
264
                ]);
265
            }
266
267
            return;
268
        }
269
270
        $childClass = new ClassBuilder($namespace, $name);
271
        if ($prototype instanceof ArrayNode) {
0 ignored issues
show
introduced by
$prototype is always a sub-type of Symfony\Component\Config\Definition\ArrayNode.
Loading history...
272
            $childClass->setAllowExtraKeys($prototype->shouldIgnoreExtraKeys());
273
        }
274
        $class->addRequire($childClass);
275
        $this->classes[] = $childClass;
276
277
        $property = $class->addProperty(
278
            $node->getName(),
279
            $this->getType($childClass->getFqcn().'[]', $hasNormalizationClosures)
280
        );
281
282
        $comment = $this->getComment($node);
283
        if ($hasNormalizationClosures) {
284
            $comment = \sprintf(" * @template TValue\n * @param TValue \$value\n%s", $comment);
285
            $comment .= \sprintf(' * @return %s|$this'."\n", $childClass->getFqcn());
286
            $comment .= \sprintf(' * @psalm-return (TValue is array ? %s : static)'."\n ", $childClass->getFqcn());
287
        }
288
        if ('' !== $comment) {
289
            $comment = "/**\n$comment*/\n";
290
        }
291
292
        if (null === $key = $node->getKeyAttribute()) {
293
            $body = $hasNormalizationClosures ? '
294
COMMENTpublic function NAME(PARAM_TYPE $value = []): CLASS|static
295
{
296
    $this->_usedProperties[\'PROPERTY\'] = true;
297
    if (!\is_array($value)) {
298
        $this->PROPERTY[] = $value;
299
300
        return $this;
301
    }
302
303
    return $this->PROPERTY[] = new CLASS($value);
304
}' : '
305
COMMENTpublic function NAME(array $value = []): CLASS
306
{
307
    $this->_usedProperties[\'PROPERTY\'] = true;
308
309
    return $this->PROPERTY[] = new CLASS($value);
310
}';
311
            $class->addMethod($methodName, $body, [
312
                'COMMENT' => $comment,
313
                'PROPERTY' => $property->getName(),
314
                'CLASS' => $childClass->getFqcn(),
315
                'PARAM_TYPE' => \in_array('mixed', $nodeParameterTypes, true) ? 'mixed' : implode('|', $nodeParameterTypes),
316
            ]);
317
        } else {
318
            $body = $hasNormalizationClosures ? '
319
COMMENTpublic function NAME(string $VAR, PARAM_TYPE $VALUE = []): CLASS|static
320
{
321
    if (!\is_array($VALUE)) {
322
        $this->_usedProperties[\'PROPERTY\'] = true;
323
        $this->PROPERTY[$VAR] = $VALUE;
324
325
        return $this;
326
    }
327
328
    if (!isset($this->PROPERTY[$VAR]) || !$this->PROPERTY[$VAR] instanceof CLASS) {
329
        $this->_usedProperties[\'PROPERTY\'] = true;
330
        $this->PROPERTY[$VAR] = new CLASS($VALUE);
331
    } elseif (1 < \func_num_args()) {
332
        throw new InvalidConfigurationException(\'The node created by "NAME()" has already been initialized. You cannot pass values the second time you call NAME().\');
333
    }
334
335
    return $this->PROPERTY[$VAR];
336
}' : '
337
COMMENTpublic function NAME(string $VAR, array $VALUE = []): CLASS
338
{
339
    if (!isset($this->PROPERTY[$VAR])) {
340
        $this->_usedProperties[\'PROPERTY\'] = true;
341
        $this->PROPERTY[$VAR] = new CLASS($VALUE);
342
    } elseif (1 < \func_num_args()) {
343
        throw new InvalidConfigurationException(\'The node created by "NAME()" has already been initialized. You cannot pass values the second time you call NAME().\');
344
    }
345
346
    return $this->PROPERTY[$VAR];
347
}';
348
            $class->addUse(InvalidConfigurationException::class);
349
            $class->addMethod($methodName, str_replace('$value', '$VAR', $body), [
350
                'COMMENT' => $comment, 'PROPERTY' => $property->getName(),
351
                'CLASS' => $childClass->getFqcn(),
352
                'VAR' => '' === $key ? 'key' : $key,
353
                'VALUE' => 'value' === $key ? 'data' : 'value',
354
                'PARAM_TYPE' => \in_array('mixed', $prototypeParameterTypes, true) ? 'mixed' : implode('|', $prototypeParameterTypes),
355
            ]);
356
        }
357
358
        $this->buildNode($prototype, $childClass, $namespace.'\\'.$childClass->getName());
359
    }
360
361
    private function handleScalarNode(ScalarNode $node, ClassBuilder $class): void
362
    {
363
        $comment = $this->getComment($node);
364
        $property = $class->addProperty($node->getName());
365
        $class->addUse(ParamConfigurator::class);
366
367
        $body = '
368
/**
369
COMMENT * @return $this
370
 */
371
public function NAME($value): static
372
{
373
    $this->_usedProperties[\'PROPERTY\'] = true;
374
    $this->PROPERTY = $value;
375
376
    return $this;
377
}';
378
379
        $class->addMethod($node->getName(), $body, ['PROPERTY' => $property->getName(), 'COMMENT' => $comment]);
380
    }
381
382
    private function getParameterTypes(NodeInterface $node): array
383
    {
384
        $paramTypes = [];
385
        if ($node instanceof BaseNode) {
386
            $types = $node->getNormalizedTypes();
387
            if (\in_array(ExprBuilder::TYPE_ANY, $types, true)) {
388
                $paramTypes[] = 'mixed';
389
            }
390
            if (\in_array(ExprBuilder::TYPE_STRING, $types, true)) {
391
                $paramTypes[] = 'string';
392
            }
393
        }
394
        if ($node instanceof BooleanNode) {
395
            $paramTypes[] = 'bool';
396
        } elseif ($node instanceof IntegerNode) {
397
            $paramTypes[] = 'int';
398
        } elseif ($node instanceof FloatNode) {
399
            $paramTypes[] = 'float';
400
        } elseif ($node instanceof EnumNode) {
401
            $paramTypes[] = 'mixed';
402
        } elseif ($node instanceof ArrayNode) {
403
            $paramTypes[] = 'array';
404
        } elseif ($node instanceof VariableNode) {
405
            $paramTypes[] = 'mixed';
406
        }
407
408
        return array_unique($paramTypes);
409
    }
410
411
    private function getComment(BaseNode $node): string
412
    {
413
        $comment = '';
414
        if ('' !== $info = (string) $node->getInfo()) {
415
            $comment .= ' * '.$info."\n";
416
        }
417
418
        if (!$node instanceof ArrayNode) {
419
            foreach ((array) ($node->getExample() ?? []) as $example) {
420
                $comment .= ' * @example '.$example."\n";
421
            }
422
423
            if ('' !== $default = $node->getDefaultValue()) {
424
                $comment .= ' * @default '.(null === $default ? 'null' : var_export($default, true))."\n";
425
            }
426
427
            if ($node instanceof EnumNode) {
428
                $comment .= \sprintf(' * @param ParamConfigurator|%s $value', implode('|', array_unique(array_map(fn ($a) => !$a instanceof \UnitEnum ? var_export($a, true) : '\\'.ltrim(var_export($a, true), '\\'), $node->getValues()))))."\n";
0 ignored issues
show
Bug introduced by
The type UnitEnum was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
429
            } else {
430
                $parameterTypes = $this->getParameterTypes($node);
431
                $comment .= ' * @param ParamConfigurator|'.implode('|', $parameterTypes).' $value'."\n";
432
            }
433
        } else {
434
            foreach ((array) ($node->getExample() ?? []) as $example) {
435
                $comment .= ' * @example '.json_encode($example)."\n";
436
            }
437
438
            if ($node->hasDefaultValue() && [] != $default = $node->getDefaultValue()) {
439
                $comment .= ' * @default '.json_encode($default)."\n";
440
            }
441
        }
442
443
        if ($node->isDeprecated()) {
444
            $comment .= ' * @deprecated '.$node->getDeprecation($node->getName(), $node->getParent()->getName())['message']."\n";
445
        }
446
447
        return $comment;
448
    }
449
450
    /**
451
     * Pick a good singular name.
452
     */
453
    private function getSingularName(PrototypedArrayNode $node): string
454
    {
455
        $name = $node->getName();
456
        if (!str_ends_with($name, 's')) {
457
            return $name;
458
        }
459
460
        $parent = $node->getParent();
461
        $mappings = $parent instanceof ArrayNode ? $parent->getXmlRemappings() : [];
462
        foreach ($mappings as $map) {
463
            if ($map[1] === $name) {
464
                $name = $map[0];
465
                break;
466
            }
467
        }
468
469
        return $name;
470
    }
471
472
    private function buildToArray(ClassBuilder $class): void
473
    {
474
        $body = '$output = [];';
475
        foreach ($class->getProperties() as $p) {
476
            $code = '$this->PROPERTY';
477
            if (null !== $p->getType()) {
478
                if ($p->isArray()) {
479
                    $code = $p->areScalarsAllowed()
480
                        ? 'array_map(fn ($v) => $v instanceof CLASS ? $v->toArray() : $v, $this->PROPERTY)'
481
                        : 'array_map(fn ($v) => $v->toArray(), $this->PROPERTY)'
482
                    ;
483
                } else {
484
                    $code = $p->areScalarsAllowed()
485
                        ? '$this->PROPERTY instanceof CLASS ? $this->PROPERTY->toArray() : $this->PROPERTY'
486
                        : '$this->PROPERTY->toArray()'
487
                    ;
488
                }
489
            }
490
491
            $body .= strtr('
492
    if (isset($this->_usedProperties[\'PROPERTY\'])) {
493
        $output[\'ORG_NAME\'] = '.$code.';
494
    }', ['PROPERTY' => $p->getName(), 'ORG_NAME' => $p->getOriginalName(), 'CLASS' => $p->getType()]);
495
        }
496
497
        $extraKeys = $class->shouldAllowExtraKeys() ? ' + $this->_extraKeys' : '';
498
499
        $class->addMethod('toArray', '
500
public function NAME(): array
501
{
502
    '.$body.'
503
504
    return $output'.$extraKeys.';
505
}');
506
    }
507
508
    private function buildConstructor(ClassBuilder $class): void
509
    {
510
        $body = '';
511
        foreach ($class->getProperties() as $p) {
512
            $code = '$value[\'ORG_NAME\']';
513
            if (null !== $p->getType()) {
514
                if ($p->isArray()) {
515
                    $code = $p->areScalarsAllowed()
516
                        ? 'array_map(fn ($v) => \is_array($v) ? new '.$p->getType().'($v) : $v, $value[\'ORG_NAME\'])'
517
                        : 'array_map(fn ($v) => new '.$p->getType().'($v), $value[\'ORG_NAME\'])'
518
                    ;
519
                } else {
520
                    $code = $p->areScalarsAllowed()
521
                        ? '\is_array($value[\'ORG_NAME\']) ? new '.$p->getType().'($value[\'ORG_NAME\']) : $value[\'ORG_NAME\']'
522
                        : 'new '.$p->getType().'($value[\'ORG_NAME\'])'
523
                    ;
524
                }
525
            }
526
527
            $body .= strtr('
528
    if (array_key_exists(\'ORG_NAME\', $value)) {
529
        $this->_usedProperties[\'PROPERTY\'] = true;
530
        $this->PROPERTY = '.$code.';
531
        unset($value[\'ORG_NAME\']);
532
    }
533
', ['PROPERTY' => $p->getName(), 'ORG_NAME' => $p->getOriginalName()]);
534
        }
535
536
        if ($class->shouldAllowExtraKeys()) {
537
            $body .= '
538
    $this->_extraKeys = $value;
539
';
540
        } else {
541
            $body .= '
542
    if ([] !== $value) {
543
        throw new InvalidConfigurationException(sprintf(\'The following keys are not supported by "%s": \', __CLASS__).implode(\', \', array_keys($value)));
544
    }';
545
546
            $class->addUse(InvalidConfigurationException::class);
547
        }
548
549
        $class->addMethod('__construct', '
550
public function __construct(array $value = [])
551
{'.$body.'
552
}');
553
    }
554
555
    private function buildSetExtraKey(ClassBuilder $class): void
556
    {
557
        if (!$class->shouldAllowExtraKeys()) {
558
            return;
559
        }
560
561
        $class->addUse(ParamConfigurator::class);
562
563
        $class->addProperty('_extraKeys');
564
565
        $class->addMethod('set', '
566
/**
567
 * @param ParamConfigurator|mixed $value
568
 *
569
 * @return $this
570
 */
571
public function NAME(string $key, mixed $value): static
572
{
573
    $this->_extraKeys[$key] = $value;
574
575
    return $this;
576
}');
577
    }
578
579
    private function getSubNamespace(ClassBuilder $rootClass): string
580
    {
581
        return \sprintf('%s\\%s', $rootClass->getNamespace(), substr($rootClass->getName(), 0, -6));
582
    }
583
584
    private function hasNormalizationClosures(NodeInterface $node): bool
585
    {
586
        try {
587
            $r = new \ReflectionProperty($node, 'normalizationClosures');
588
        } catch (\ReflectionException) {
589
            return false;
590
        }
591
592
        return [] !== $r->getValue($node);
593
    }
594
595
    private function getType(string $classType, bool $hasNormalizationClosures): string
596
    {
597
        return $classType.($hasNormalizationClosures ? '|scalar' : '');
598
    }
599
}
600