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) { |
|
|
|
|
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(...))) { |
|
|
|
|
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)) { |
|
|
|
|
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)) { |
|
|
|
|
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) { |
|
|
|
|
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); |
|
|
|
|
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) { |
|
|
|
|
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
|
|
|
|
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.