diff --git a/src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php index 7c38455b5876c..2036b3231f81f 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php @@ -13,6 +13,7 @@ use Symfony\Component\DependencyInjection\Alias; use Symfony\Component\DependencyInjection\Argument\AbstractArgument; +use Symfony\Component\DependencyInjection\Argument\ArgumentInterface; use Symfony\Component\DependencyInjection\Argument\IteratorArgument; use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument; @@ -32,34 +33,31 @@ */ class XmlDumper extends Dumper { - private \DOMDocument $document; - /** * Dumps the service container as an XML string. */ public function dump(array $options = []): string { - $this->document = new \DOMDocument('1.0', 'utf-8'); - $this->document->formatOutput = true; - - $container = $this->document->createElementNS('http://symfony.com/schema/dic/services', 'container'); - $container->setAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance'); - $container->setAttribute('xsi:schemaLocation', 'http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd'); + $xml = << + + EOXML; - $this->addParameters($container); - $this->addServices($container); + foreach ($this->addParameters() as $line) { + $xml .= "\n ".$line; + } + foreach ($this->addServices() as $line) { + $xml .= "\n ".$line; + } - $this->document->appendChild($container); - $xml = $this->document->saveXML(); - unset($this->document); + $xml .= "\n\n"; return $this->container->resolveEnvPlaceholders($xml); } - private function addParameters(\DOMElement $parent): void + private function addParameters(): iterable { - $data = $this->container->getParameterBag()->all(); - if (!$data) { + if (!$data = $this->container->getParameterBag()->all()) { return; } @@ -67,201 +65,205 @@ private function addParameters(\DOMElement $parent): void $data = $this->escape($data); } - $parameters = $this->document->createElement('parameters'); - $parent->appendChild($parameters); - $this->convertParameters($data, 'parameter', $parameters); + yield ''; + foreach ($this->convertParameters($data, 'parameter') as $line) { + yield ' '.$line; + } + yield ''; } - private function addMethodCalls(array $methodcalls, \DOMElement $parent): void + private function addMethodCalls(array $methodcalls): iterable { foreach ($methodcalls as $methodcall) { - $call = $this->document->createElement('call'); - $call->setAttribute('method', $methodcall[0]); - if (\count($methodcall[1])) { - $this->convertParameters($methodcall[1], 'argument', $call); - } - if ($methodcall[2] ?? false) { - $call->setAttribute('returns-clone', 'true'); + $xmlAttr = \sprintf(' method="%s"%s', $this->encode($methodcall[0]), ($methodcall[2] ?? false) ? ' returns-clone="true"' : ''); + + if ($methodcall[1]) { + yield \sprintf('', $xmlAttr); + foreach ($this->convertParameters($methodcall[1], 'argument') as $line) { + yield ' '.$line; + } + yield ''; + } else { + yield \sprintf('', $xmlAttr); } - $parent->appendChild($call); } } - private function addService(Definition $definition, ?string $id, \DOMElement $parent): void + private function addService(Definition $definition, ?string $id): iterable { - $service = $this->document->createElement('service'); + $xmlAttr = ''; if (null !== $id) { - $service->setAttribute('id', $id); + $xmlAttr .= \sprintf(' id="%s"', $this->encode($id)); } if ($class = $definition->getClass()) { if (str_starts_with($class, '\\')) { $class = substr($class, 1); } - $service->setAttribute('class', $class); + $xmlAttr .= \sprintf(' class="%s"', $this->encode($class)); } if (!$definition->isShared()) { - $service->setAttribute('shared', 'false'); + $xmlAttr .= ' shared="false"'; } if ($definition->isPublic()) { - $service->setAttribute('public', 'true'); + $xmlAttr .= ' public="true"'; } if ($definition->isSynthetic()) { - $service->setAttribute('synthetic', 'true'); + $xmlAttr .= ' synthetic="true"'; } if ($definition->isLazy()) { - $service->setAttribute('lazy', 'true'); + $xmlAttr .= ' lazy="true"'; } if (null !== $decoratedService = $definition->getDecoratedService()) { [$decorated, $renamedId, $priority] = $decoratedService; - $service->setAttribute('decorates', $decorated); + $xmlAttr .= \sprintf(' decorates="%s"', $this->encode($decorated)); $decorationOnInvalid = $decoratedService[3] ?? ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE; if (\in_array($decorationOnInvalid, [ContainerInterface::IGNORE_ON_INVALID_REFERENCE, ContainerInterface::NULL_ON_INVALID_REFERENCE], true)) { $invalidBehavior = ContainerInterface::NULL_ON_INVALID_REFERENCE === $decorationOnInvalid ? 'null' : 'ignore'; - $service->setAttribute('decoration-on-invalid', $invalidBehavior); + $xmlAttr .= \sprintf(' decoration-on-invalid="%s"', $invalidBehavior); } if (null !== $renamedId) { - $service->setAttribute('decoration-inner-name', $renamedId); + $xmlAttr .= \sprintf(' decoration-inner-name="%s"', $this->encode($renamedId)); } if (0 !== $priority) { - $service->setAttribute('decoration-priority', $priority); + $xmlAttr .= \sprintf(' decoration-priority="%d"', $priority); } } + $xml = []; + $tags = $definition->getTags(); $tags['container.error'] = array_map(fn ($e) => ['message' => $e], $definition->getErrors()); foreach ($tags as $name => $tags) { foreach ($tags as $attributes) { - $tag = $this->document->createElement('tag'); - // Check if we have recursive attributes if (array_filter($attributes, \is_array(...))) { - $tag->setAttribute('name', $name); - $this->addTagRecursiveAttributes($tag, $attributes); - } else { - if (!\array_key_exists('name', $attributes)) { - $tag->setAttribute('name', $name); - } else { - $tag->appendChild($this->document->createTextNode($name)); + $xml[] = \sprintf(' ', $this->encode($name)); + foreach ($this->addTagRecursiveAttributes($attributes) as $line) { + $xml[] = ' '.$line; } + $xml[] = ' '; + } else { + $hasNameAttr = \array_key_exists('name', $attributes); + $attr = \sprintf(' name="%s"', $this->encode($hasNameAttr ? $attributes['name'] : $name)); foreach ($attributes as $key => $value) { - $tag->setAttribute($key, $value ?? ''); + if ('name' !== $key) { + $attr .= \sprintf(' %s="%s"', $this->encode($key), $this->encode(self::phpToXml($value ?? ''))); + } + } + if ($hasNameAttr) { + $xml[] = \sprintf(' %s', $attr, $this->encode($name, 0)); + } else { + $xml[] = \sprintf(' ', $attr); } } - $service->appendChild($tag); } } if ($definition->getFile()) { - $file = $this->document->createElement('file'); - $file->appendChild($this->document->createTextNode($definition->getFile())); - $service->appendChild($file); + $xml[] = \sprintf(' %s', $this->encode($definition->getFile(), 0)); } - if ($parameters = $definition->getArguments()) { - $this->convertParameters($parameters, 'argument', $service); + foreach ($this->convertParameters($definition->getArguments(), 'argument') as $line) { + $xml[] = ' '.$line; } - if ($parameters = $definition->getProperties()) { - $this->convertParameters($parameters, 'property', $service, 'name'); + foreach ($this->convertParameters($definition->getProperties(), 'property', 'name') as $line) { + $xml[] = ' '.$line; } - $this->addMethodCalls($definition->getMethodCalls(), $service); + foreach ($this->addMethodCalls($definition->getMethodCalls()) as $line) { + $xml[] = ' '.$line; + } if ($callable = $definition->getFactory()) { if (\is_array($callable) && ['Closure', 'fromCallable'] !== $callable && $definition->getClass() === $callable[0]) { - $service->setAttribute('constructor', $callable[1]); + $xmlAttr .= \sprintf(' constructor="%s"', $this->encode($callable[1])); } else { - $factory = $this->document->createElement('factory'); - if (\is_array($callable) && $callable[0] instanceof Definition) { - $this->addService($callable[0], null, $factory); - $factory->setAttribute('method', $callable[1]); + $xml[] = \sprintf(' ', $this->encode($callable[1])); + foreach ($this->addService($callable[0], null) as $line) { + $xml[] = ' '.$line; + } + $xml[] = ' '; } elseif (\is_array($callable)) { if (null !== $callable[0]) { - $factory->setAttribute($callable[0] instanceof Reference ? 'service' : 'class', $callable[0]); + $xml[] = \sprintf(' ', $callable[0] instanceof Reference ? 'service' : 'class', $this->encode($callable[0]), $this->encode($callable[1])); + } else { + $xml[] = \sprintf(' ', $this->encode($callable[1])); } - $factory->setAttribute('method', $callable[1]); } else { - $factory->setAttribute('function', $callable); + $xml[] = \sprintf(' ', $this->encode($callable)); } - $service->appendChild($factory); } } if ($definition->isDeprecated()) { $deprecation = $definition->getDeprecation('%service_id%'); - $deprecated = $this->document->createElement('deprecated'); - $deprecated->appendChild($this->document->createTextNode($definition->getDeprecation('%service_id%')['message'])); - $deprecated->setAttribute('package', $deprecation['package']); - $deprecated->setAttribute('version', $deprecation['version']); - - $service->appendChild($deprecated); + $xml[] = \sprintf(' %s', $this->encode($deprecation['package']), $this->encode($deprecation['version']), $this->encode($deprecation['message'], 0)); } if ($definition->isAutowired()) { - $service->setAttribute('autowire', 'true'); + $xmlAttr .= ' autowire="true"'; } if ($definition->isAutoconfigured()) { - $service->setAttribute('autoconfigure', 'true'); + $xmlAttr .= ' autoconfigure="true"'; } if ($definition->isAbstract()) { - $service->setAttribute('abstract', 'true'); + $xmlAttr .= ' abstract="true"'; } if ($callable = $definition->getConfigurator()) { - $configurator = $this->document->createElement('configurator'); - if (\is_array($callable) && $callable[0] instanceof Definition) { - $this->addService($callable[0], null, $configurator); - $configurator->setAttribute('method', $callable[1]); + $xml[] = \sprintf(' ', $this->encode($callable[1])); + foreach ($this->addService($callable[0], null) as $line) { + $xml[] = ' '.$line; + } + $xml[] = ' '; } elseif (\is_array($callable)) { - $configurator->setAttribute($callable[0] instanceof Reference ? 'service' : 'class', $callable[0]); - $configurator->setAttribute('method', $callable[1]); + $xml[] = \sprintf(' ', $callable[0] instanceof Reference ? 'service' : 'class', $this->encode($callable[0]), $this->encode($callable[1])); } else { - $configurator->setAttribute('function', $callable); + $xml[] = \sprintf(' ', $this->encode($callable)); } - $service->appendChild($configurator); } - $parent->appendChild($service); + if (!$xml) { + yield \sprintf('', $xmlAttr); + } else { + yield \sprintf('', $xmlAttr); + yield from $xml; + yield ''; + } } - private function addServiceAlias(string $alias, Alias $id, \DOMElement $parent): void + private function addServiceAlias(string $alias, Alias $id): iterable { - $service = $this->document->createElement('service'); - $service->setAttribute('id', $alias); - $service->setAttribute('alias', $id); - if ($id->isPublic()) { - $service->setAttribute('public', 'true'); - } + $xmlAttr = \sprintf(' id="%s" alias="%s"%s', $this->encode($alias), $this->encode($id), $id->isPublic() ? ' public="true"' : ''); if ($id->isDeprecated()) { $deprecation = $id->getDeprecation('%alias_id%'); - $deprecated = $this->document->createElement('deprecated'); - $deprecated->appendChild($this->document->createTextNode($deprecation['message'])); - $deprecated->setAttribute('package', $deprecation['package']); - $deprecated->setAttribute('version', $deprecation['version']); - - $service->appendChild($deprecated); + yield \sprintf('', $xmlAttr); + yield \sprintf(' %s', $this->encode($deprecation['package']), $this->encode($deprecation['version']), $this->encode($deprecation['message'], 0)); + yield ''; + } else { + yield \sprintf('', $xmlAttr); } - - $parent->appendChild($service); } - private function addServices(\DOMElement $parent): void + private function addServices(): iterable { - $definitions = $this->container->getDefinitions(); - if (!$definitions) { + if (!$definitions = $this->container->getDefinitions()) { return; } - $services = $this->document->createElement('services'); + yield ''; foreach ($definitions as $id => $definition) { - $this->addService($definition, $id, $services); + foreach ($this->addService($definition, $id) as $line) { + yield ' '.$line; + } } $aliases = $this->container->getAliases(); @@ -269,137 +271,150 @@ private function addServices(\DOMElement $parent): void while (isset($aliases[(string) $id])) { $id = $aliases[(string) $id]; } - $this->addServiceAlias($alias, $id, $services); + foreach ($this->addServiceAlias($alias, $id) as $line) { + yield ' '.$line; + } } - $parent->appendChild($services); + yield ''; } - private function addTagRecursiveAttributes(\DOMElement $parent, array $attributes): void + private function addTagRecursiveAttributes(array $attributes): iterable { foreach ($attributes as $name => $value) { - $attribute = $this->document->createElement('attribute'); - $attribute->setAttribute('name', $name); - if (\is_array($value)) { - $this->addTagRecursiveAttributes($attribute, $value); - } else { - $attribute->appendChild($this->document->createTextNode($value)); + yield \sprintf('', $this->encode($name)); + foreach ($this->addTagRecursiveAttributes($value) as $line) { + yield ' '.$line; + } + yield ''; + } elseif ('' !== $value = self::phpToXml($value ?? '')) { + yield \sprintf('%s', $this->encode($name), $this->encode($value, 0)); } - - $parent->appendChild($attribute); } } - private function convertParameters(array $parameters, string $type, \DOMElement $parent, string $keyAttribute = 'key'): void + private function convertParameters(array $parameters, string $type, string $keyAttribute = 'key'): iterable { $withKeys = !array_is_list($parameters); foreach ($parameters as $key => $value) { - $element = $this->document->createElement($type); - if ($withKeys) { - $element->setAttribute($keyAttribute, $key); - } + $xmlAttr = $withKeys ? \sprintf(' %s="%s"', $keyAttribute, $this->encode($key)) : ''; - if (\is_array($tag = $value)) { - $element->setAttribute('type', 'collection'); - $this->convertParameters($value, $type, $element, 'key'); - } elseif ($value instanceof TaggedIteratorArgument || ($value instanceof ServiceLocatorArgument && $tag = $value->getTaggedIteratorArgument())) { - $element->setAttribute('type', $value instanceof TaggedIteratorArgument ? 'tagged_iterator' : 'tagged_locator'); - $element->setAttribute('tag', $tag->getTag()); + if (($value instanceof TaggedIteratorArgument && $tag = $value) + || ($value instanceof ServiceLocatorArgument && $tag = $value->getTaggedIteratorArgument()) + ) { + $xmlAttr .= \sprintf(' type="%s"', $value instanceof TaggedIteratorArgument ? 'tagged_iterator' : 'tagged_locator'); + $xmlAttr .= \sprintf(' tag="%s"', $this->encode($tag->getTag())); if (null !== $tag->getIndexAttribute()) { - $element->setAttribute('index-by', $tag->getIndexAttribute()); + $xmlAttr .= \sprintf(' index-by="%s"', $this->encode($tag->getIndexAttribute())); if (null !== $tag->getDefaultIndexMethod()) { - $element->setAttribute('default-index-method', $tag->getDefaultIndexMethod()); + $xmlAttr .= \sprintf(' default-index-method="%s"', $this->encode($tag->getDefaultIndexMethod())); } if (null !== $tag->getDefaultPriorityMethod()) { - $element->setAttribute('default-priority-method', $tag->getDefaultPriorityMethod()); + $xmlAttr .= \sprintf(' default-priority-method="%s"', $this->encode($tag->getDefaultPriorityMethod())); } } - if ($excludes = $tag->getExclude()) { - if (1 === \count($excludes)) { - $element->setAttribute('exclude', $excludes[0]); - } else { - foreach ($excludes as $exclude) { - $element->appendChild($this->document->createElement('exclude', $exclude)); - } - } + if (1 === \count($excludes = $tag->getExclude())) { + $xmlAttr .= \sprintf(' exclude="%s"', $this->encode($excludes[0])); } if (!$tag->excludeSelf()) { - $element->setAttribute('exclude-self', 'false'); + $xmlAttr .= ' exclude-self="false"'; + } + + if (1 < \count($excludes)) { + yield \sprintf('<%s%s>', $type, $xmlAttr); + foreach ($excludes as $exclude) { + yield \sprintf(' %s', $this->encode($exclude, 0)); + } + yield \sprintf('', $type); + } else { + yield \sprintf('<%s%s/>', $type, $xmlAttr); + } + } elseif (match (true) { + \is_array($value) && $xmlAttr .= ' type="collection"' => true, + $value instanceof IteratorArgument && $xmlAttr .= ' type="iterator"' => true, + $value instanceof ServiceLocatorArgument && $xmlAttr .= ' type="service_locator"' => true, + $value instanceof ServiceClosureArgument && !$value->getValues()[0] instanceof Reference && $xmlAttr .= ' type="service_closure"' => true, + default => false, + }) { + if ($value instanceof ArgumentInterface) { + $value = $value->getValues(); + } + if ($value) { + yield \sprintf('<%s%s>', $type, $xmlAttr); + foreach ($this->convertParameters($value, $type, 'key') as $line) { + yield ' '.$line; + } + yield \sprintf('', $type); + } else { + yield \sprintf('<%s%s/>', $type, $xmlAttr); } - } elseif ($value instanceof IteratorArgument) { - $element->setAttribute('type', 'iterator'); - $this->convertParameters($value->getValues(), $type, $element, 'key'); - } elseif ($value instanceof ServiceLocatorArgument) { - $element->setAttribute('type', 'service_locator'); - $this->convertParameters($value->getValues(), $type, $element, 'key'); - } elseif ($value instanceof ServiceClosureArgument && !$value->getValues()[0] instanceof Reference) { - $element->setAttribute('type', 'service_closure'); - $this->convertParameters($value->getValues(), $type, $element, 'key'); } elseif ($value instanceof Reference || $value instanceof ServiceClosureArgument) { - $element->setAttribute('type', 'service'); if ($value instanceof ServiceClosureArgument) { - $element->setAttribute('type', 'service_closure'); + $xmlAttr .= ' type="service_closure"'; $value = $value->getValues()[0]; + } else { + $xmlAttr .= ' type="service"'; } - $element->setAttribute('id', (string) $value); - $behavior = $value->getInvalidBehavior(); - if (ContainerInterface::NULL_ON_INVALID_REFERENCE == $behavior) { - $element->setAttribute('on-invalid', 'null'); - } elseif (ContainerInterface::IGNORE_ON_INVALID_REFERENCE == $behavior) { - $element->setAttribute('on-invalid', 'ignore'); - } elseif (ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE == $behavior) { - $element->setAttribute('on-invalid', 'ignore_uninitialized'); - } + $xmlAttr .= \sprintf(' id="%s"', $this->encode((string) $value)); + $xmlAttr .= match ($value->getInvalidBehavior()) { + ContainerInterface::NULL_ON_INVALID_REFERENCE => ' on-invalid="null"', + ContainerInterface::IGNORE_ON_INVALID_REFERENCE => ' on-invalid="ignore"', + ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE => ' on-invalid="ignore_uninitialized"', + default => '', + }; + + yield \sprintf('<%s%s/>', $type, $xmlAttr); } elseif ($value instanceof Definition) { - $element->setAttribute('type', 'service'); - $this->addService($value, null, $element); - } elseif ($value instanceof Expression) { - $element->setAttribute('type', 'expression'); - $text = $this->document->createTextNode(self::phpToXml((string) $value)); - $element->appendChild($text); - } elseif (\is_string($value) && !preg_match('/^[^\x00-\x08\x0B\x0C\x0E-\x1F\x7F]*+$/u', $value)) { - $element->setAttribute('type', 'binary'); - $text = $this->document->createTextNode(self::phpToXml(base64_encode($value))); - $element->appendChild($text); - } elseif ($value instanceof \UnitEnum) { - $element->setAttribute('type', 'constant'); - $element->appendChild($this->document->createTextNode(self::phpToXml($value))); - } elseif ($value instanceof AbstractArgument) { - $element->setAttribute('type', 'abstract'); - $text = $this->document->createTextNode(self::phpToXml($value->getText())); - $element->appendChild($text); + $xmlAttr .= ' type="service"'; + + yield \sprintf('<%s%s>', $type, $xmlAttr); + foreach ($this->addService($value, null) as $line) { + yield ' '.$line; + } + yield \sprintf('', $type); } else { - if (\in_array($value, ['null', 'true', 'false'], true)) { - $element->setAttribute('type', 'string'); + if ($value instanceof Expression) { + $xmlAttr .= ' type="expression"'; + $value = (string) $value; + } elseif (\is_string($value) && !preg_match('/^[^\x00-\x08\x0B\x0C\x0E-\x1F\x7F]*+$/u', $value)) { + $xmlAttr .= ' type="binary"'; + $value = base64_encode($value); + } elseif ($value instanceof \UnitEnum) { + $xmlAttr .= ' type="constant"'; + } elseif ($value instanceof AbstractArgument) { + $xmlAttr .= ' type="abstract"'; + $value = $value->getText(); + } elseif (\in_array($value, ['null', 'true', 'false'], true)) { + $xmlAttr .= ' type="string"'; + } elseif (\is_string($value) && (is_numeric($value) || preg_match('/^0b[01]*$/', $value) || preg_match('/^0x[0-9a-f]++$/i', $value))) { + $xmlAttr .= ' type="string"'; } - if (\is_string($value) && (is_numeric($value) || preg_match('/^0b[01]*$/', $value) || preg_match('/^0x[0-9a-f]++$/i', $value))) { - $element->setAttribute('type', 'string'); + if ('' === $value = self::phpToXml($value)) { + yield \sprintf('<%s%s/>', $type, $xmlAttr); + } else { + yield \sprintf('<%s%s>%s', $type, $xmlAttr, $this->encode($value, 0)); } - - $text = $this->document->createTextNode(self::phpToXml($value)); - $element->appendChild($text); } - $parent->appendChild($element); } } - /** - * Escapes arguments. - */ + private function encode(string $value, int $flags = \ENT_COMPAT): string + { + return htmlspecialchars($value, \ENT_XML1 | \ENT_SUBSTITUTE | $flags, 'UTF-8'); + } + private function escape(array $arguments): array { $args = []; foreach ($arguments as $k => $v) { - if (\is_array($v)) { - $args[$k] = $this->escape($v); - } elseif (\is_string($v)) { - $args[$k] = str_replace('%', '%%', $v); - } else { - $args[$k] = $v; - } + $args[$k] = match (true) { + \is_array($v) => $this->escape($v), + \is_string($v) => str_replace('%', '%%', $v), + default => $v, + }; } return $args; @@ -412,21 +427,15 @@ private function escape(array $arguments): array */ public static function phpToXml(mixed $value): string { - switch (true) { - case null === $value: - return 'null'; - case true === $value: - return 'true'; - case false === $value: - return 'false'; - case $value instanceof Parameter: - return '%'.$value.'%'; - case $value instanceof \UnitEnum: - return \sprintf('%s::%s', $value::class, $value->name); - case \is_object($value) || \is_resource($value): - 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))); - default: - return (string) $value; - } + return match (true) { + null === $value => 'null', + true === $value => 'true', + false === $value => 'false', + $value instanceof Parameter => '%'.$value.'%', + $value instanceof \UnitEnum => \sprintf('%s::%s', $value::class, $value->name), + \is_object($value), + \is_resource($value) => 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))), + default => (string) $value, + }; } }