XmlDumper::dump()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 17
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 11
dl 0
loc 17
rs 9.9
c 0
b 0
f 0
cc 1
nc 1
nop 1
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\DependencyInjection\Dumper;
13
14
use Symfony\Component\DependencyInjection\Alias;
15
use Symfony\Component\DependencyInjection\Argument\AbstractArgument;
16
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
17
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
18
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
19
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
20
use Symfony\Component\DependencyInjection\ContainerInterface;
21
use Symfony\Component\DependencyInjection\Definition;
22
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
23
use Symfony\Component\DependencyInjection\Parameter;
24
use Symfony\Component\DependencyInjection\Reference;
25
use Symfony\Component\ExpressionLanguage\Expression;
26
27
/**
28
 * XmlDumper dumps a service container as an XML string.
29
 *
30
 * @author Fabien Potencier <[email protected]>
31
 * @author Martin Hasoň <[email protected]>
32
 */
33
class XmlDumper extends Dumper
34
{
35
    private \DOMDocument $document;
36
37
    /**
38
     * Dumps the service container as an XML string.
39
     */
40
    public function dump(array $options = []): string
41
    {
42
        $this->document = new \DOMDocument('1.0', 'utf-8');
43
        $this->document->formatOutput = true;
44
45
        $container = $this->document->createElementNS('http://symfony.com/schema/dic/services', 'container');
46
        $container->setAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance');
47
        $container->setAttribute('xsi:schemaLocation', 'http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd');
48
49
        $this->addParameters($container);
50
        $this->addServices($container);
51
52
        $this->document->appendChild($container);
53
        $xml = $this->document->saveXML();
54
        unset($this->document);
55
56
        return $this->container->resolveEnvPlaceholders($xml);
57
    }
58
59
    private function addParameters(\DOMElement $parent): void
60
    {
61
        $data = $this->container->getParameterBag()->all();
62
        if (!$data) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $data of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
63
            return;
64
        }
65
66
        if ($this->container->isCompiled()) {
67
            $data = $this->escape($data);
68
        }
69
70
        $parameters = $this->document->createElement('parameters');
71
        $parent->appendChild($parameters);
72
        $this->convertParameters($data, 'parameter', $parameters);
73
    }
74
75
    private function addMethodCalls(array $methodcalls, \DOMElement $parent): void
76
    {
77
        foreach ($methodcalls as $methodcall) {
78
            $call = $this->document->createElement('call');
79
            $call->setAttribute('method', $methodcall[0]);
80
            if (\count($methodcall[1])) {
81
                $this->convertParameters($methodcall[1], 'argument', $call);
82
            }
83
            if ($methodcall[2] ?? false) {
84
                $call->setAttribute('returns-clone', 'true');
85
            }
86
            $parent->appendChild($call);
87
        }
88
    }
89
90
    private function addService(Definition $definition, ?string $id, \DOMElement $parent): void
91
    {
92
        $service = $this->document->createElement('service');
93
        if (null !== $id) {
94
            $service->setAttribute('id', $id);
95
        }
96
        if ($class = $definition->getClass()) {
97
            if (str_starts_with($class, '\\')) {
98
                $class = substr($class, 1);
99
            }
100
101
            $service->setAttribute('class', $class);
102
        }
103
        if (!$definition->isShared()) {
104
            $service->setAttribute('shared', 'false');
105
        }
106
        if ($definition->isPublic()) {
107
            $service->setAttribute('public', 'true');
108
        }
109
        if ($definition->isSynthetic()) {
110
            $service->setAttribute('synthetic', 'true');
111
        }
112
        if ($definition->isLazy()) {
113
            $service->setAttribute('lazy', 'true');
114
        }
115
        if (null !== $decoratedService = $definition->getDecoratedService()) {
116
            [$decorated, $renamedId, $priority] = $decoratedService;
117
            $service->setAttribute('decorates', $decorated);
118
119
            $decorationOnInvalid = $decoratedService[3] ?? ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE;
120
            if (\in_array($decorationOnInvalid, [ContainerInterface::IGNORE_ON_INVALID_REFERENCE, ContainerInterface::NULL_ON_INVALID_REFERENCE], true)) {
121
                $invalidBehavior = ContainerInterface::NULL_ON_INVALID_REFERENCE === $decorationOnInvalid ? 'null' : 'ignore';
122
                $service->setAttribute('decoration-on-invalid', $invalidBehavior);
123
            }
124
            if (null !== $renamedId) {
125
                $service->setAttribute('decoration-inner-name', $renamedId);
126
            }
127
            if (0 !== $priority) {
128
                $service->setAttribute('decoration-priority', $priority);
129
            }
130
        }
131
132
        $tags = $definition->getTags();
133
        $tags['container.error'] = array_map(fn ($e) => ['message' => $e], $definition->getErrors());
134
        foreach ($tags as $name => $tags) {
135
            foreach ($tags as $attributes) {
136
                $tag = $this->document->createElement('tag');
137
138
                // Check if we have recursive attributes
139
                if (array_filter($attributes, \is_array(...))) {
0 ignored issues
show
Bug introduced by
The type is_array 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...
140
                    $tag->setAttribute('name', $name);
141
                    $this->addTagRecursiveAttributes($tag, $attributes);
142
                } else {
143
                    if (!\array_key_exists('name', $attributes)) {
144
                        $tag->setAttribute('name', $name);
145
                    } else {
146
                        $tag->appendChild($this->document->createTextNode($name));
147
                    }
148
                    foreach ($attributes as $key => $value) {
149
                        $tag->setAttribute($key, $value ?? '');
150
                    }
151
                }
152
                $service->appendChild($tag);
153
            }
154
        }
155
156
        if ($definition->getFile()) {
157
            $file = $this->document->createElement('file');
158
            $file->appendChild($this->document->createTextNode($definition->getFile()));
159
            $service->appendChild($file);
160
        }
161
162
        if ($parameters = $definition->getArguments()) {
163
            $this->convertParameters($parameters, 'argument', $service);
164
        }
165
166
        if ($parameters = $definition->getProperties()) {
167
            $this->convertParameters($parameters, 'property', $service, 'name');
168
        }
169
170
        $this->addMethodCalls($definition->getMethodCalls(), $service);
171
172
        if ($callable = $definition->getFactory()) {
173
            if (\is_array($callable) && ['Closure', 'fromCallable'] !== $callable && $definition->getClass() === $callable[0]) {
174
                $service->setAttribute('constructor', $callable[1]);
175
            } else {
176
                $factory = $this->document->createElement('factory');
177
178
                if (\is_array($callable) && $callable[0] instanceof Definition) {
179
                    $this->addService($callable[0], null, $factory);
180
                    $factory->setAttribute('method', $callable[1]);
181
                } elseif (\is_array($callable)) {
0 ignored issues
show
introduced by
The condition is_array($callable) is always false.
Loading history...
182
                    if (null !== $callable[0]) {
183
                        $factory->setAttribute($callable[0] instanceof Reference ? 'service' : 'class', $callable[0]);
184
                    }
185
                    $factory->setAttribute('method', $callable[1]);
186
                } else {
187
                    $factory->setAttribute('function', $callable);
188
                }
189
                $service->appendChild($factory);
190
            }
191
        }
192
193
        if ($definition->isDeprecated()) {
194
            $deprecation = $definition->getDeprecation('%service_id%');
195
            $deprecated = $this->document->createElement('deprecated');
196
            $deprecated->appendChild($this->document->createTextNode($definition->getDeprecation('%service_id%')['message']));
197
            $deprecated->setAttribute('package', $deprecation['package']);
198
            $deprecated->setAttribute('version', $deprecation['version']);
199
200
            $service->appendChild($deprecated);
201
        }
202
203
        if ($definition->isAutowired()) {
204
            $service->setAttribute('autowire', 'true');
205
        }
206
207
        if ($definition->isAutoconfigured()) {
208
            $service->setAttribute('autoconfigure', 'true');
209
        }
210
211
        if ($definition->isAbstract()) {
212
            $service->setAttribute('abstract', 'true');
213
        }
214
215
        if ($callable = $definition->getConfigurator()) {
216
            $configurator = $this->document->createElement('configurator');
217
218
            if (\is_array($callable) && $callable[0] instanceof Definition) {
219
                $this->addService($callable[0], null, $configurator);
220
                $configurator->setAttribute('method', $callable[1]);
221
            } elseif (\is_array($callable)) {
0 ignored issues
show
introduced by
The condition is_array($callable) is always false.
Loading history...
222
                $configurator->setAttribute($callable[0] instanceof Reference ? 'service' : 'class', $callable[0]);
223
                $configurator->setAttribute('method', $callable[1]);
224
            } else {
225
                $configurator->setAttribute('function', $callable);
226
            }
227
            $service->appendChild($configurator);
228
        }
229
230
        $parent->appendChild($service);
231
    }
232
233
    private function addServiceAlias(string $alias, Alias $id, \DOMElement $parent): void
234
    {
235
        $service = $this->document->createElement('service');
236
        $service->setAttribute('id', $alias);
237
        $service->setAttribute('alias', $id);
238
        if ($id->isPublic()) {
239
            $service->setAttribute('public', 'true');
240
        }
241
242
        if ($id->isDeprecated()) {
243
            $deprecation = $id->getDeprecation('%alias_id%');
244
            $deprecated = $this->document->createElement('deprecated');
245
            $deprecated->appendChild($this->document->createTextNode($deprecation['message']));
246
            $deprecated->setAttribute('package', $deprecation['package']);
247
            $deprecated->setAttribute('version', $deprecation['version']);
248
249
            $service->appendChild($deprecated);
250
        }
251
252
        $parent->appendChild($service);
253
    }
254
255
    private function addServices(\DOMElement $parent): void
256
    {
257
        $definitions = $this->container->getDefinitions();
258
        if (!$definitions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $definitions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
259
            return;
260
        }
261
262
        $services = $this->document->createElement('services');
263
        foreach ($definitions as $id => $definition) {
264
            $this->addService($definition, $id, $services);
265
        }
266
267
        $aliases = $this->container->getAliases();
268
        foreach ($aliases as $alias => $id) {
269
            while (isset($aliases[(string) $id])) {
270
                $id = $aliases[(string) $id];
271
            }
272
            $this->addServiceAlias($alias, $id, $services);
273
        }
274
        $parent->appendChild($services);
275
    }
276
277
    private function addTagRecursiveAttributes(\DOMElement $parent, array $attributes): void
278
    {
279
        foreach ($attributes as $name => $value) {
280
            $attribute = $this->document->createElement('attribute');
281
            $attribute->setAttribute('name', $name);
282
283
            if (\is_array($value)) {
284
                $this->addTagRecursiveAttributes($attribute, $value);
285
            } else {
286
                $attribute->appendChild($this->document->createTextNode($value));
287
            }
288
289
            $parent->appendChild($attribute);
290
        }
291
    }
292
293
    private function convertParameters(array $parameters, string $type, \DOMElement $parent, string $keyAttribute = 'key'): void
294
    {
295
        $withKeys = !array_is_list($parameters);
0 ignored issues
show
Bug introduced by
The function array_is_list was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

295
        $withKeys = !/** @scrutinizer ignore-call */ array_is_list($parameters);
Loading history...
296
        foreach ($parameters as $key => $value) {
297
            $element = $this->document->createElement($type);
298
            if ($withKeys) {
299
                $element->setAttribute($keyAttribute, $key);
300
            }
301
302
            if (\is_array($tag = $value)) {
303
                $element->setAttribute('type', 'collection');
304
                $this->convertParameters($value, $type, $element, 'key');
305
            } elseif ($value instanceof TaggedIteratorArgument || ($value instanceof ServiceLocatorArgument && $tag = $value->getTaggedIteratorArgument())) {
306
                $element->setAttribute('type', $value instanceof TaggedIteratorArgument ? 'tagged_iterator' : 'tagged_locator');
307
                $element->setAttribute('tag', $tag->getTag());
308
309
                if (null !== $tag->getIndexAttribute()) {
310
                    $element->setAttribute('index-by', $tag->getIndexAttribute());
311
312
                    if (null !== $tag->getDefaultIndexMethod()) {
313
                        $element->setAttribute('default-index-method', $tag->getDefaultIndexMethod());
314
                    }
315
                    if (null !== $tag->getDefaultPriorityMethod()) {
316
                        $element->setAttribute('default-priority-method', $tag->getDefaultPriorityMethod());
317
                    }
318
                }
319
                if ($excludes = $tag->getExclude()) {
320
                    if (1 === \count($excludes)) {
321
                        $element->setAttribute('exclude', $excludes[0]);
322
                    } else {
323
                        foreach ($excludes as $exclude) {
324
                            $element->appendChild($this->document->createElement('exclude', $exclude));
325
                        }
326
                    }
327
                }
328
                if (!$tag->excludeSelf()) {
329
                    $element->setAttribute('exclude-self', 'false');
330
                }
331
            } elseif ($value instanceof IteratorArgument) {
332
                $element->setAttribute('type', 'iterator');
333
                $this->convertParameters($value->getValues(), $type, $element, 'key');
334
            } elseif ($value instanceof ServiceLocatorArgument) {
335
                $element->setAttribute('type', 'service_locator');
336
                $this->convertParameters($value->getValues(), $type, $element, 'key');
337
            } elseif ($value instanceof ServiceClosureArgument && !$value->getValues()[0] instanceof Reference) {
338
                $element->setAttribute('type', 'service_closure');
339
                $this->convertParameters($value->getValues(), $type, $element, 'key');
340
            } elseif ($value instanceof Reference || $value instanceof ServiceClosureArgument) {
341
                $element->setAttribute('type', 'service');
342
                if ($value instanceof ServiceClosureArgument) {
343
                    $element->setAttribute('type', 'service_closure');
344
                    $value = $value->getValues()[0];
345
                }
346
                $element->setAttribute('id', (string) $value);
347
                $behavior = $value->getInvalidBehavior();
348
                if (ContainerInterface::NULL_ON_INVALID_REFERENCE == $behavior) {
349
                    $element->setAttribute('on-invalid', 'null');
350
                } elseif (ContainerInterface::IGNORE_ON_INVALID_REFERENCE == $behavior) {
351
                    $element->setAttribute('on-invalid', 'ignore');
352
                } elseif (ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE == $behavior) {
353
                    $element->setAttribute('on-invalid', 'ignore_uninitialized');
354
                }
355
            } elseif ($value instanceof Definition) {
356
                $element->setAttribute('type', 'service');
357
                $this->addService($value, null, $element);
358
            } elseif ($value instanceof Expression) {
359
                $element->setAttribute('type', 'expression');
360
                $text = $this->document->createTextNode(self::phpToXml((string) $value));
361
                $element->appendChild($text);
362
            } elseif (\is_string($value) && !preg_match('/^[^\x00-\x08\x0B\x0C\x0E-\x1F\x7F]*+$/u', $value)) {
363
                $element->setAttribute('type', 'binary');
364
                $text = $this->document->createTextNode(self::phpToXml(base64_encode($value)));
365
                $element->appendChild($text);
366
            } elseif ($value instanceof \UnitEnum) {
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...
367
                $element->setAttribute('type', 'constant');
368
                $element->appendChild($this->document->createTextNode(self::phpToXml($value)));
369
            } elseif ($value instanceof AbstractArgument) {
370
                $element->setAttribute('type', 'abstract');
371
                $text = $this->document->createTextNode(self::phpToXml($value->getText()));
372
                $element->appendChild($text);
373
            } else {
374
                if (\in_array($value, ['null', 'true', 'false'], true)) {
375
                    $element->setAttribute('type', 'string');
376
                }
377
378
                if (\is_string($value) && (is_numeric($value) || preg_match('/^0b[01]*$/', $value) || preg_match('/^0x[0-9a-f]++$/i', $value))) {
379
                    $element->setAttribute('type', 'string');
380
                }
381
382
                $text = $this->document->createTextNode(self::phpToXml($value));
383
                $element->appendChild($text);
384
            }
385
            $parent->appendChild($element);
386
        }
387
    }
388
389
    /**
390
     * Escapes arguments.
391
     */
392
    private function escape(array $arguments): array
393
    {
394
        $args = [];
395
        foreach ($arguments as $k => $v) {
396
            if (\is_array($v)) {
397
                $args[$k] = $this->escape($v);
398
            } elseif (\is_string($v)) {
399
                $args[$k] = str_replace('%', '%%', $v);
400
            } else {
401
                $args[$k] = $v;
402
            }
403
        }
404
405
        return $args;
406
    }
407
408
    /**
409
     * Converts php types to xml types.
410
     *
411
     * @throws RuntimeException When trying to dump object or resource
412
     */
413
    public static function phpToXml(mixed $value): string
414
    {
415
        switch (true) {
416
            case null === $value:
417
                return 'null';
418
            case true === $value:
419
                return 'true';
420
            case false === $value:
421
                return 'false';
422
            case $value instanceof Parameter:
423
                return '%'.$value.'%';
424
            case $value instanceof \UnitEnum:
425
                return \sprintf('%s::%s', $value::class, $value->name);
426
            case \is_object($value) || \is_resource($value):
427
                throw new RuntimeException(\sprintf('Unable to dump a service container if a parameter is an object or a resource, got "%s".', get_debug_type($value)));
428
            default:
429
                return (string) $value;
430
        }
431
    }
432
}
433