XmlReferenceDumper::writeLine()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 6
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 2
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\Definition\Dumper;
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\ConfigurationInterface;
18
use Symfony\Component\Config\Definition\EnumNode;
19
use Symfony\Component\Config\Definition\FloatNode;
20
use Symfony\Component\Config\Definition\IntegerNode;
21
use Symfony\Component\Config\Definition\NodeInterface;
22
use Symfony\Component\Config\Definition\PrototypedArrayNode;
23
use Symfony\Component\Config\Definition\ScalarNode;
24
25
/**
26
 * Dumps an XML reference configuration for the given configuration/node instance.
27
 *
28
 * @author Wouter J <[email protected]>
29
 */
30
class XmlReferenceDumper
31
{
32
    private ?string $reference = null;
33
34
    public function dump(ConfigurationInterface $configuration, ?string $namespace = null): string
35
    {
36
        return $this->dumpNode($configuration->getConfigTreeBuilder()->buildTree(), $namespace);
37
    }
38
39
    public function dumpNode(NodeInterface $node, ?string $namespace = null): string
40
    {
41
        $this->reference = '';
42
        $this->writeNode($node, 0, true, $namespace);
43
        $ref = $this->reference;
44
        $this->reference = null;
45
46
        return $ref;
47
    }
48
49
    private function writeNode(NodeInterface $node, int $depth = 0, bool $root = false, ?string $namespace = null): void
50
    {
51
        $rootName = ($root ? 'config' : $node->getName());
52
        $rootNamespace = ($namespace ?: ($root ? 'http://example.org/schema/dic/'.$node->getName() : null));
53
54
        // xml remapping
55
        if ($node->getParent()) {
0 ignored issues
show
Bug introduced by
The method getParent() does not exist on Symfony\Component\Config\Definition\NodeInterface. Did you maybe mean getPath()? ( Ignorable by Annotation )

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

55
        if ($node->/** @scrutinizer ignore-call */ getParent()) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
56
            $remapping = array_filter($node->getParent()->getXmlRemappings(), fn (array $mapping) => $rootName === $mapping[1]);
57
58
            if (\count($remapping)) {
59
                [$singular] = current($remapping);
60
                $rootName = $singular;
61
            }
62
        }
63
        $rootName = str_replace('_', '-', $rootName);
64
65
        $rootAttributes = [];
66
        $rootAttributeComments = [];
67
        $rootChildren = [];
68
        $rootComments = [];
69
70
        if ($node instanceof ArrayNode) {
71
            $children = $node->getChildren();
72
73
            // comments about the root node
74
            if ($rootInfo = $node->getInfo()) {
75
                $rootComments[] = $rootInfo;
76
            }
77
78
            if ($rootNamespace) {
79
                $rootComments[] = 'Namespace: '.$rootNamespace;
80
            }
81
82
            // render prototyped nodes
83
            if ($node instanceof PrototypedArrayNode) {
84
                $prototype = $node->getPrototype();
85
86
                $info = 'prototype';
87
                if (null !== $prototype->getInfo()) {
88
                    $info .= ': '.$prototype->getInfo();
89
                }
90
                array_unshift($rootComments, $info);
91
92
                if ($key = $node->getKeyAttribute()) {
93
                    $rootAttributes[$key] = str_replace('-', ' ', $rootName).' '.$key;
94
                }
95
96
                if ($prototype instanceof PrototypedArrayNode) {
97
                    $prototype->setName($key ?? '');
98
                    $children = [$key => $prototype];
99
                } elseif ($prototype instanceof ArrayNode) {
100
                    $children = $prototype->getChildren();
101
                } else {
102
                    if ($prototype->hasDefaultValue()) {
103
                        $prototypeValue = $prototype->getDefaultValue();
104
                    } else {
105
                        $prototypeValue = match ($prototype::class) {
106
                            ScalarNode::class => 'scalar value',
107
                            FloatNode::class,
108
                            IntegerNode::class => 'numeric value',
109
                            BooleanNode::class => 'true|false',
110
                            EnumNode::class => $prototype->getPermissibleValues('|'),
0 ignored issues
show
Bug introduced by
The method getPermissibleValues() does not exist on Symfony\Component\Config...\PrototypeNodeInterface. It seems like you code against a sub-type of Symfony\Component\Config...\PrototypeNodeInterface such as Symfony\Component\Config\Definition\EnumNode. ( Ignorable by Annotation )

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

110
                            EnumNode::class => $prototype->/** @scrutinizer ignore-call */ getPermissibleValues('|'),
Loading history...
111
                            default => 'value',
112
                        };
113
                    }
114
                }
115
            }
116
117
            // get attributes and elements
118
            foreach ($children as $child) {
119
                if ($child instanceof ArrayNode) {
120
                    // get elements
121
                    $rootChildren[] = $child;
122
123
                    continue;
124
                }
125
126
                // get attributes
127
128
                // metadata
129
                $name = str_replace('_', '-', $child->getName());
130
                $value = '%%%%not_defined%%%%'; // use a string which isn't used in the normal world
131
132
                // comments
133
                $comments = [];
134
                if ($child instanceof BaseNode && $info = $child->getInfo()) {
135
                    $comments[] = $info;
136
                }
137
138
                if ($child instanceof BaseNode && $example = $child->getExample()) {
139
                    $comments[] = 'Example: '.(\is_array($example) ? implode(', ', $example) : $example);
140
                }
141
142
                if ($child->isRequired()) {
143
                    $comments[] = 'Required';
144
                }
145
146
                if ($child instanceof BaseNode && $child->isDeprecated()) {
147
                    $deprecation = $child->getDeprecation($child->getName(), $node->getPath());
148
                    $comments[] = \sprintf('Deprecated (%s)', ($deprecation['package'] || $deprecation['version'] ? "Since {$deprecation['package']} {$deprecation['version']}: " : '').$deprecation['message']);
149
                }
150
151
                if ($child instanceof EnumNode) {
152
                    $comments[] = 'One of '.$child->getPermissibleValues('; ');
153
                }
154
155
                if (\count($comments)) {
156
                    $rootAttributeComments[$name] = implode(";\n", $comments);
157
                }
158
159
                // default values
160
                if ($child->hasDefaultValue()) {
161
                    $value = $child->getDefaultValue();
162
                }
163
164
                // append attribute
165
                $rootAttributes[$name] = $value;
166
            }
167
        }
168
169
        // render comments
170
171
        // root node comment
172
        if (\count($rootComments)) {
173
            foreach ($rootComments as $comment) {
174
                $this->writeLine('<!-- '.$comment.' -->', $depth);
175
            }
176
        }
177
178
        // attribute comments
179
        if (\count($rootAttributeComments)) {
180
            foreach ($rootAttributeComments as $attrName => $comment) {
181
                $commentDepth = $depth + 4 + \strlen($attrName) + 2;
182
                $commentLines = explode("\n", $comment);
183
                $multiline = (\count($commentLines) > 1);
184
                $comment = implode(\PHP_EOL.str_repeat(' ', $commentDepth), $commentLines);
185
186
                if ($multiline) {
187
                    $this->writeLine('<!--', $depth);
188
                    $this->writeLine($attrName.': '.$comment, $depth + 4);
189
                    $this->writeLine('-->', $depth);
190
                } else {
191
                    $this->writeLine('<!-- '.$attrName.': '.$comment.' -->', $depth);
192
                }
193
            }
194
        }
195
196
        // render start tag + attributes
197
        $rootIsVariablePrototype = isset($prototypeValue);
198
        $rootIsEmptyTag = (0 === \count($rootChildren) && !$rootIsVariablePrototype);
199
        $rootOpenTag = '<'.$rootName;
200
        if (1 >= ($attributesCount = \count($rootAttributes))) {
201
            if (1 === $attributesCount) {
202
                $rootOpenTag .= \sprintf(' %s="%s"', current(array_keys($rootAttributes)), $this->writeValue(current($rootAttributes)));
203
            }
204
205
            $rootOpenTag .= $rootIsEmptyTag ? ' />' : '>';
206
207
            if ($rootIsVariablePrototype) {
208
                $rootOpenTag .= $prototypeValue.'</'.$rootName.'>';
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $prototypeValue does not seem to be defined for all execution paths leading up to this point.
Loading history...
209
            }
210
211
            $this->writeLine($rootOpenTag, $depth);
212
        } else {
213
            $this->writeLine($rootOpenTag, $depth);
214
215
            $i = 1;
216
217
            foreach ($rootAttributes as $attrName => $attrValue) {
218
                $attr = \sprintf('%s="%s"', $attrName, $this->writeValue($attrValue));
219
220
                $this->writeLine($attr, $depth + 4);
221
222
                if ($attributesCount === $i++) {
223
                    $this->writeLine($rootIsEmptyTag ? '/>' : '>', $depth);
224
225
                    if ($rootIsVariablePrototype) {
226
                        $rootOpenTag .= $prototypeValue.'</'.$rootName.'>';
227
                    }
228
                }
229
            }
230
        }
231
232
        // render children tags
233
        foreach ($rootChildren as $child) {
234
            $this->writeLine('');
235
            $this->writeNode($child, $depth + 4);
236
        }
237
238
        // render end tag
239
        if (!$rootIsEmptyTag && !$rootIsVariablePrototype) {
240
            $this->writeLine('');
241
242
            $rootEndTag = '</'.$rootName.'>';
243
            $this->writeLine($rootEndTag, $depth);
244
        }
245
    }
246
247
    /**
248
     * Outputs a single config reference line.
249
     */
250
    private function writeLine(string $text, int $indent = 0): void
251
    {
252
        $indent = \strlen($text) + $indent;
253
        $format = '%'.$indent.'s';
254
255
        $this->reference .= \sprintf($format, $text).\PHP_EOL;
256
    }
257
258
    /**
259
     * Renders the string conversion of the value.
260
     */
261
    private function writeValue(mixed $value): string
262
    {
263
        if ('%%%%not_defined%%%%' === $value) {
264
            return '';
265
        }
266
267
        if (\is_string($value) || is_numeric($value)) {
268
            return $value;
269
        }
270
271
        if (false === $value) {
272
            return 'false';
273
        }
274
275
        if (true === $value) {
276
            return 'true';
277
        }
278
279
        if (null === $value) {
280
            return 'null';
281
        }
282
283
        if (!$value) {
284
            return '';
285
        }
286
287
        if (\is_array($value)) {
288
            return implode(',', $value);
289
        }
290
291
        return '';
292
    }
293
}
294