diff --git a/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php index 5d7a5a7fd722..7aea1325b767 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php @@ -18,9 +18,9 @@ use Symfony\Component\DependencyInjection\Alias; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; -use Symfony\Component\DependencyInjection\SimpleXMLElement; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Exception\RuntimeException; +use Symfony\Component\ExpressionLanguage\Expression; /** * XmlFileLoader loads XML files service definitions. @@ -29,6 +29,8 @@ */ class XmlFileLoader extends FileLoader { + const NS = 'http://symfony.com/schema/dic/services'; + /** * Loads an XML file. * @@ -39,8 +41,7 @@ public function load($file, $type = null) { $path = $this->locator->locate($file); - $xml = $this->parseFile($path); - $xml->registerXPathNamespace('container', 'http://symfony.com/schema/dic/services'); + $xml = $this->parseFileToDOM($path); $this->container->addResource(new FileResource($path)); @@ -76,125 +77,131 @@ public function supports($resource, $type = null) /** * Parses parameters * - * @param SimpleXMLElement $xml - * @param string $file + * @param \DOMDocument $xml + * @param string $file */ - private function parseParameters(SimpleXMLElement $xml, $file) + private function parseParameters(\DOMDocument $xml, $file) { - if (!$xml->parameters) { - return; + if ($parameters = $this->getChildren($xml->documentElement, 'parameters')) { + $this->container->getParameterBag()->add($this->getArgumentsAsPhp($parameters[0], 'parameter')); } - - $this->container->getParameterBag()->add($xml->parameters->getArgumentsAsPhp('parameter')); } /** * Parses imports * - * @param SimpleXMLElement $xml - * @param string $file + * @param \DOMDocument $xml + * @param string $file */ - private function parseImports(SimpleXMLElement $xml, $file) + private function parseImports(\DOMDocument $xml, $file) { - if (false === $imports = $xml->xpath('//container:imports/container:import')) { + $xpath = new \DOMXPath($xml); + $xpath->registerNamespace('container', self::NS); + + if (false === $imports = $xpath->query('//container:imports/container:import')) { return; } foreach ($imports as $import) { $this->setCurrentDir(dirname($file)); - $this->import((string) $import['resource'], null, (Boolean) $import->getAttributeAsPhp('ignore-errors'), $file); + $this->import($import->getAttribute('resource'), null, (Boolean) XmlUtils::phpize($import->getAttribute('ignore-errors')), $file); } } /** * Parses multiple definitions * - * @param SimpleXMLElement $xml - * @param string $file + * @param \DOMDocument $xml + * @param string $file */ - private function parseDefinitions(SimpleXMLElement $xml, $file) + private function parseDefinitions(\DOMDocument $xml, $file) { - if (false === $services = $xml->xpath('//container:services/container:service')) { + $xpath = new \DOMXPath($xml); + $xpath->registerNamespace('container', self::NS); + + if (false === $services = $xpath->query('//container:services/container:service')) { return; } foreach ($services as $service) { - $this->parseDefinition((string) $service['id'], $service, $file); + $this->parseDefinition((string) $service->getAttribute('id'), $service, $file); } } /** * Parses an individual Definition * - * @param string $id - * @param SimpleXMLElement $service - * @param string $file + * @param string $id + * @param \DOMElement $service + * @param string $file */ - private function parseDefinition($id, $service, $file) + private function parseDefinition($id, \DOMElement $service, $file) { - if ((string) $service['alias']) { + if ($alias = $service->getAttribute('alias')) { $public = true; - if (isset($service['public'])) { - $public = $service->getAttributeAsPhp('public'); + if ($publicAttr = $service->getAttribute('public')) { + $public = XmlUtils::phpize($publicAttr); } - $this->container->setAlias($id, new Alias((string) $service['alias'], $public)); + $this->container->setAlias($id, new Alias($alias, $public)); return; } - if (isset($service['parent'])) { - $definition = new DefinitionDecorator((string) $service['parent']); + if ($parent = $service->getAttribute('parent')) { + $definition = new DefinitionDecorator($parent); } else { $definition = new Definition(); } foreach (array('class', 'scope', 'public', 'factory-class', 'factory-method', 'factory-service', 'synthetic', 'synchronized', 'lazy', 'abstract') as $key) { - if (isset($service[$key])) { + if ($value = $service->getAttribute($key)) { $method = 'set'.str_replace('-', '', $key); - $definition->$method((string) $service->getAttributeAsPhp($key)); + $definition->$method(XmlUtils::phpize($value)); } } - if ($service->file) { - $definition->setFile((string) $service->file); + if ($files = $this->getChildren($service, 'file')) { + $definition->setFile($files[0]->nodeValue); } - $definition->setArguments($service->getArgumentsAsPhp('argument')); - $definition->setProperties($service->getArgumentsAsPhp('property')); + $definition->setArguments($this->getArgumentsAsPhp($service, 'argument')); + $definition->setProperties($this->getArgumentsAsPhp($service, 'property')); - if (isset($service->configurator)) { - if (isset($service->configurator['function'])) { - $definition->setConfigurator((string) $service->configurator['function']); + if ($configurators = $this->getChildren($service, 'configurator')) { + $configurator = $configurators[0]; + if ($function = $configurator->getAttribute('function')) { + $definition->setConfigurator($function); } else { - if (isset($service->configurator['service'])) { - $class = new Reference((string) $service->configurator['service'], ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, false); + if ($childService = $configurator->getAttribute('service')) { + $class = new Reference($childService, ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, false); } else { - $class = (string) $service->configurator['class']; + $class = $configurator->getAttribute('class'); } - $definition->setConfigurator(array($class, (string) $service->configurator['method'])); + $definition->setConfigurator(array($class, $configurator->getAttribute('method'))); } } - foreach ($service->call as $call) { - $definition->addMethodCall((string) $call['method'], $call->getArgumentsAsPhp('argument')); + foreach ($this->getChildren($service, 'call') as $call) { + $definition->addMethodCall($call->getAttribute('method'), $this->getArgumentsAsPhp($call, 'argument')); } - foreach ($service->tag as $tag) { + foreach ($this->getChildren($service, 'tag') as $tag) { $parameters = array(); - foreach ($tag->attributes() as $name => $value) { + foreach ($tag->attributes as $name => $node) { if ('name' === $name) { continue; } if (false !== strpos($name, '-') && false === strpos($name, '_') && !array_key_exists($normalizedName = str_replace('-', '_', $name), $parameters)) { - $parameters[$normalizedName] = SimpleXMLElement::phpize($value); + $parameters[$normalizedName] = XmlUtils::phpize($node->nodeValue); } // keep not normalized key for BC too $parameters[$name] = SimpleXMLElement::phpize($value); } - $definition->addTag((string) $tag['name'], $parameters); +// $definition->addTag((string) $tag['name'], $parameters); + $definition->addTag($tag->getAttribute('name'), $parameters); } if (isset($service['decorates'])) { @@ -215,6 +222,22 @@ private function parseDefinition($id, $service, $file) * @throws InvalidArgumentException When loading of XML file returns error */ protected function parseFile($file) + { + $dom = $this->parseFileToDOM($file); + + return simplexml_import_dom($dom, 'Symfony\\Component\\DependencyInjection\\SimpleXMLElement'); + } + + /** + * Parses a XML file to a \DOMDocument + * + * @param string $file Path to a file + * + * @return \DOMDocument + * + * @throws InvalidArgumentException When loading of XML file returns error + */ + protected function parseFileToDOM($file) { try { $dom = XmlUtils::loadFile($file, array($this, 'validateSchema')); @@ -224,41 +247,48 @@ protected function parseFile($file) $this->validateExtensions($dom, $file); - return simplexml_import_dom($dom, 'Symfony\\Component\\DependencyInjection\\SimpleXMLElement'); + return $dom; } /** * Processes anonymous services * - * @param SimpleXMLElement $xml - * @param string $file + * @param \DOMDocument $xml + * @param string $file */ - private function processAnonymousServices(SimpleXMLElement $xml, $file) + private function processAnonymousServices(\DOMDocument $xml, $file) { $definitions = array(); $count = 0; + $xpath = new \DOMXPath($xml); + $xpath->registerNamespace('container', self::NS); + // anonymous services as arguments/properties - if (false !== $nodes = $xml->xpath('//container:argument[@type="service"][not(@id)]|//container:property[@type="service"][not(@id)]')) { + if (false !== $nodes = $xpath->query('//container:argument[@type="service"][not(@id)]|//container:property[@type="service"][not(@id)]')) { foreach ($nodes as $node) { // give it a unique name $id = sprintf('%s_%d', hash('sha256', $file), ++$count); - $node['id'] = $id; + $node->setAttribute('id', $id); - $definitions[$id] = array($node->service, $file, false); - $node->service['id'] = $id; + if ($services = $this->getChildren($node, 'service')) { + $definitions[$id] = array($services[0], $file, false); + $services[0]->setAttribute('id', $id); + } } } // anonymous services "in the wild" - if (false !== $nodes = $xml->xpath('//container:services/container:service[not(@id)]')) { + if (false !== $nodes = $xpath->query('//container:services/container:service[not(@id)]')) { foreach ($nodes as $node) { // give it a unique name $id = sprintf('%s_%d', hash('sha256', $file), ++$count); - $node['id'] = $id; + $node->setAttribute('id', $id); - $definitions[$id] = array($node, $file, true); - $node->service['id'] = $id; + if ($services = $this->getChildren($node, 'service')) { + $definitions[$id] = array($node, $file, true); + $services[0]->setAttribute('id', $id); + } } } @@ -266,13 +296,13 @@ private function processAnonymousServices(SimpleXMLElement $xml, $file) krsort($definitions); foreach ($definitions as $id => $def) { // anonymous services are always private - $def[0]['public'] = false; + $def[0]->setAttribute('public', false); $this->parseDefinition($id, $def[0], $def[1]); - $oNode = dom_import_simplexml($def[0]); + $oNode = $def[0]; if (true === $def[2]) { - $nNode = new \DOMElement('_services'); + $nNode = new \DOMElement('_services', null, self::NS); $oNode->parentNode->replaceChild($nNode, $oNode); $nNode->setAttribute('id', $id); } else { @@ -281,6 +311,96 @@ private function processAnonymousServices(SimpleXMLElement $xml, $file) } } + /** + * Returns arguments as valid php types. + * + * @param \DOMElement $node + * @param string $name + * @param Boolean $lowercase + * + * @return mixed + */ + private function getArgumentsAsPhp(\DOMElement $node, $name, $lowercase = true) + { + $arguments = array(); + foreach ($this->getChildren($node, $name) as $arg) { + if ($nameAttr = $arg->getAttribute('name')) { + $arg->setAttribute('key', $nameAttr); + } + + if (!$key = $arg->getAttribute('key')) { + $key = !$arguments ? 0 : max(array_keys($arguments)) + 1; + } + + // parameter keys are case insensitive + if ('parameter' == $name && $lowercase) { + $key = strtolower($key); + } + + // this is used by DefinitionDecorator to overwrite a specific + // argument of the parent definition + if ($index = $arg->getAttribute('index')) { + $key = 'index_'.$index; + } + + switch ($arg->getAttribute('type')) { + case 'service': + $onInvalid = $arg->getAttribute('on-invalid'); + $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE; + if ('ignore' == $onInvalid) { + $invalidBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE; + } elseif ('null' == $onInvalid) { + $invalidBehavior = ContainerInterface::NULL_ON_INVALID_REFERENCE; + } + + if ($strict = $arg->getAttribute('strict')) { + $strict = XmlUtils::phpize($strict); + } else { + $strict = true; + } + + $arguments[$key] = new Reference($arg->getAttribute('id'), $invalidBehavior, $strict); + break; + case 'expression': + $arguments[$key] = new Expression($arg->nodeValue); + break; + case 'collection': + $arguments[$key] = $this->getArgumentsAsPhp($arg, $name, false); + break; + case 'string': + $arguments[$key] = $arg->nodeValue; + break; + case 'constant': + $arguments[$key] = constant($arg->nodeValue); + break; + default: + $arguments[$key] = XmlUtils::phpize($arg->nodeValue); + } + } + + return $arguments; + } + + /** + * Get child elements by name + * + * @param \DOMNode $node + * @param mixed $name + * + * @return array + */ + private function getChildren(\DOMNode $node, $name) + { + $children = array(); + foreach ($node->childNodes as $child) { + if ($child instanceof \DOMElement && $child->localName === $name && $child->namespaceURI === self::NS) { + $children[] = $child; + } + } + + return $children; + } + /** * Validates a documents XML schema. * @@ -385,12 +505,12 @@ private function validateExtensions(\DOMDocument $dom, $file) /** * Loads from an extension. * - * @param SimpleXMLElement $xml + * @param \DOMDocument $xml */ - private function loadFromExtensions(SimpleXMLElement $xml) + private function loadFromExtensions(\DOMDocument $xml) { - foreach (dom_import_simplexml($xml)->childNodes as $node) { - if (!$node instanceof \DOMElement || $node->namespaceURI === 'http://symfony.com/schema/dic/services') { + foreach ($xml->documentElement->childNodes as $node) { + if (!$node instanceof \DOMElement || $node->namespaceURI === self::NS) { continue; } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/namespaces.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/namespaces.xml new file mode 100644 index 000000000000..5a05cedd7e79 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/namespaces.xml @@ -0,0 +1,17 @@ + + + + + + + + + foo + + + + diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php index 6a660a3af26c..c6075f68ec8c 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php @@ -432,4 +432,16 @@ public function testDocTypeIsNotAllowed() $this->assertSame('Document types are not allowed.', $e->getMessage(), '->load() throws an InvalidArgumentException if the configuration contains a document type'); } } + + public function testXmlNamespaces() + { + $container = new ContainerBuilder(); + $loader = new XmlFileLoader($container, new FileLocator(self::$fixturesPath.'/xml')); + $loader->load('namespaces.xml'); + $services = $container->getDefinitions(); + + $this->assertTrue(isset($services['foo']), '->load() parses elements'); + $this->assertEquals(1, count($services['foo']->getTag('foo.tag')), '->load parses elements'); + $this->assertEquals(array(array('setBar', array('foo'))), $services['foo']->getMethodCalls(), '->load() parses the tag'); + } }