Chris@0: Chris@0: * Chris@0: * For the full copyright and license information, please view the LICENSE Chris@0: * file that was distributed with this source code. Chris@0: */ Chris@0: Chris@0: namespace Symfony\Component\DependencyInjection\Loader; Chris@0: Chris@0: use Symfony\Component\Config\Resource\FileResource; Chris@0: use Symfony\Component\Config\Util\XmlUtils; Chris@0: use Symfony\Component\DependencyInjection\DefinitionDecorator; Chris@0: use Symfony\Component\DependencyInjection\ContainerInterface; Chris@0: use Symfony\Component\DependencyInjection\Alias; Chris@0: use Symfony\Component\DependencyInjection\Definition; Chris@0: use Symfony\Component\DependencyInjection\Reference; Chris@0: use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; Chris@0: use Symfony\Component\DependencyInjection\Exception\RuntimeException; Chris@0: use Symfony\Component\ExpressionLanguage\Expression; Chris@0: Chris@0: /** Chris@0: * XmlFileLoader loads XML files service definitions. Chris@0: * Chris@0: * @author Fabien Potencier Chris@0: */ Chris@0: class XmlFileLoader extends FileLoader Chris@0: { Chris@0: const NS = 'http://symfony.com/schema/dic/services'; Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function load($resource, $type = null) Chris@0: { Chris@0: $path = $this->locator->locate($resource); Chris@0: Chris@0: $xml = $this->parseFileToDOM($path); Chris@0: Chris@0: $this->container->addResource(new FileResource($path)); Chris@0: Chris@0: // anonymous services Chris@0: $this->processAnonymousServices($xml, $path); Chris@0: Chris@0: // imports Chris@0: $this->parseImports($xml, $path); Chris@0: Chris@0: // parameters Chris@0: $this->parseParameters($xml); Chris@0: Chris@0: // extensions Chris@0: $this->loadFromExtensions($xml); Chris@0: Chris@0: // services Chris@0: $this->parseDefinitions($xml, $path); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function supports($resource, $type = null) Chris@0: { Chris@0: return is_string($resource) && 'xml' === pathinfo($resource, PATHINFO_EXTENSION); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Parses parameters. Chris@0: * Chris@0: * @param \DOMDocument $xml Chris@0: */ Chris@0: private function parseParameters(\DOMDocument $xml) Chris@0: { Chris@0: if ($parameters = $this->getChildren($xml->documentElement, 'parameters')) { Chris@0: $this->container->getParameterBag()->add($this->getArgumentsAsPhp($parameters[0], 'parameter')); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Parses imports. Chris@0: * Chris@0: * @param \DOMDocument $xml Chris@0: * @param string $file Chris@0: */ Chris@0: private function parseImports(\DOMDocument $xml, $file) Chris@0: { Chris@0: $xpath = new \DOMXPath($xml); Chris@0: $xpath->registerNamespace('container', self::NS); Chris@0: Chris@0: if (false === $imports = $xpath->query('//container:imports/container:import')) { Chris@0: return; Chris@0: } Chris@0: Chris@0: $defaultDirectory = dirname($file); Chris@0: foreach ($imports as $import) { Chris@0: $this->setCurrentDir($defaultDirectory); Chris@0: $this->import($import->getAttribute('resource'), null, (bool) XmlUtils::phpize($import->getAttribute('ignore-errors')), $file); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Parses multiple definitions. Chris@0: * Chris@0: * @param \DOMDocument $xml Chris@0: * @param string $file Chris@0: */ Chris@0: private function parseDefinitions(\DOMDocument $xml, $file) Chris@0: { Chris@0: $xpath = new \DOMXPath($xml); Chris@0: $xpath->registerNamespace('container', self::NS); Chris@0: Chris@0: if (false === $services = $xpath->query('//container:services/container:service')) { Chris@0: return; Chris@0: } Chris@0: Chris@0: foreach ($services as $service) { Chris@0: if (null !== $definition = $this->parseDefinition($service, $file)) { Chris@0: $this->container->setDefinition((string) $service->getAttribute('id'), $definition); Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Parses an individual Definition. Chris@0: * Chris@0: * @param \DOMElement $service Chris@0: * @param string $file Chris@0: * Chris@0: * @return Definition|null Chris@0: */ Chris@0: private function parseDefinition(\DOMElement $service, $file) Chris@0: { Chris@0: if ($alias = $service->getAttribute('alias')) { Chris@0: $this->validateAlias($service, $file); Chris@0: Chris@0: $public = true; Chris@0: if ($publicAttr = $service->getAttribute('public')) { Chris@0: $public = XmlUtils::phpize($publicAttr); Chris@0: } Chris@0: $this->container->setAlias((string) $service->getAttribute('id'), new Alias($alias, $public)); Chris@0: Chris@0: return; Chris@0: } Chris@0: Chris@0: if ($parent = $service->getAttribute('parent')) { Chris@0: $definition = new DefinitionDecorator($parent); Chris@0: } else { Chris@0: $definition = new Definition(); Chris@0: } Chris@0: Chris@0: foreach (array('class', 'shared', 'public', 'synthetic', 'lazy', 'abstract') as $key) { Chris@0: if ($value = $service->getAttribute($key)) { Chris@0: $method = 'set'.$key; Chris@0: $definition->$method(XmlUtils::phpize($value)); Chris@0: } Chris@0: } Chris@0: Chris@0: if ($value = $service->getAttribute('autowire')) { Chris@0: $definition->setAutowired(XmlUtils::phpize($value)); Chris@0: } Chris@0: Chris@0: if ($files = $this->getChildren($service, 'file')) { Chris@0: $definition->setFile($files[0]->nodeValue); Chris@0: } Chris@0: Chris@0: if ($deprecated = $this->getChildren($service, 'deprecated')) { Chris@0: $definition->setDeprecated(true, $deprecated[0]->nodeValue ?: null); Chris@0: } Chris@0: Chris@0: $definition->setArguments($this->getArgumentsAsPhp($service, 'argument')); Chris@0: $definition->setProperties($this->getArgumentsAsPhp($service, 'property')); Chris@0: Chris@0: if ($factories = $this->getChildren($service, 'factory')) { Chris@0: $factory = $factories[0]; Chris@0: if ($function = $factory->getAttribute('function')) { Chris@0: $definition->setFactory($function); Chris@0: } else { Chris@0: $factoryService = $this->getChildren($factory, 'service'); Chris@0: Chris@0: if (isset($factoryService[0])) { Chris@0: $class = $this->parseDefinition($factoryService[0], $file); Chris@0: } elseif ($childService = $factory->getAttribute('service')) { Chris@0: $class = new Reference($childService, ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE); Chris@0: } else { Chris@0: $class = $factory->getAttribute('class'); Chris@0: } Chris@0: Chris@0: $definition->setFactory(array($class, $factory->getAttribute('method'))); Chris@0: } Chris@0: } Chris@0: Chris@0: if ($configurators = $this->getChildren($service, 'configurator')) { Chris@0: $configurator = $configurators[0]; Chris@0: if ($function = $configurator->getAttribute('function')) { Chris@0: $definition->setConfigurator($function); Chris@0: } else { Chris@0: $configuratorService = $this->getChildren($configurator, 'service'); Chris@0: Chris@0: if (isset($configuratorService[0])) { Chris@0: $class = $this->parseDefinition($configuratorService[0], $file); Chris@0: } elseif ($childService = $configurator->getAttribute('service')) { Chris@0: $class = new Reference($childService, ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE); Chris@0: } else { Chris@0: $class = $configurator->getAttribute('class'); Chris@0: } Chris@0: Chris@0: $definition->setConfigurator(array($class, $configurator->getAttribute('method'))); Chris@0: } Chris@0: } Chris@0: Chris@0: foreach ($this->getChildren($service, 'call') as $call) { Chris@0: $definition->addMethodCall($call->getAttribute('method'), $this->getArgumentsAsPhp($call, 'argument')); Chris@0: } Chris@0: Chris@0: foreach ($this->getChildren($service, 'tag') as $tag) { Chris@0: $parameters = array(); Chris@0: foreach ($tag->attributes as $name => $node) { Chris@0: if ('name' === $name) { Chris@0: continue; Chris@0: } Chris@0: Chris@0: if (false !== strpos($name, '-') && false === strpos($name, '_') && !array_key_exists($normalizedName = str_replace('-', '_', $name), $parameters)) { Chris@0: $parameters[$normalizedName] = XmlUtils::phpize($node->nodeValue); Chris@0: } Chris@0: // keep not normalized key Chris@0: $parameters[$name] = XmlUtils::phpize($node->nodeValue); Chris@0: } Chris@0: Chris@0: if ('' === $tag->getAttribute('name')) { Chris@0: throw new InvalidArgumentException(sprintf('The tag name for service "%s" in %s must be a non-empty string.', (string) $service->getAttribute('id'), $file)); Chris@0: } Chris@0: Chris@0: $definition->addTag($tag->getAttribute('name'), $parameters); Chris@0: } Chris@0: Chris@0: foreach ($this->getChildren($service, 'autowiring-type') as $type) { Chris@0: $definition->addAutowiringType($type->textContent); Chris@0: } Chris@0: Chris@0: if ($value = $service->getAttribute('decorates')) { Chris@0: $renameId = $service->hasAttribute('decoration-inner-name') ? $service->getAttribute('decoration-inner-name') : null; Chris@0: $priority = $service->hasAttribute('decoration-priority') ? $service->getAttribute('decoration-priority') : 0; Chris@0: $definition->setDecoratedService($value, $renameId, $priority); Chris@0: } Chris@0: Chris@0: return $definition; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Parses a XML file to a \DOMDocument. Chris@0: * Chris@0: * @param string $file Path to a file Chris@0: * Chris@0: * @return \DOMDocument Chris@0: * Chris@0: * @throws InvalidArgumentException When loading of XML file returns error Chris@0: */ Chris@0: private function parseFileToDOM($file) Chris@0: { Chris@0: try { Chris@0: $dom = XmlUtils::loadFile($file, array($this, 'validateSchema')); Chris@0: } catch (\InvalidArgumentException $e) { Chris@0: throw new InvalidArgumentException(sprintf('Unable to parse file "%s".', $file), $e->getCode(), $e); Chris@0: } Chris@0: Chris@0: $this->validateExtensions($dom, $file); Chris@0: Chris@0: return $dom; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Processes anonymous services. Chris@0: * Chris@0: * @param \DOMDocument $xml Chris@0: * @param string $file Chris@0: */ Chris@0: private function processAnonymousServices(\DOMDocument $xml, $file) Chris@0: { Chris@0: $definitions = array(); Chris@0: $count = 0; Chris@0: Chris@0: $xpath = new \DOMXPath($xml); Chris@0: $xpath->registerNamespace('container', self::NS); Chris@0: Chris@0: // anonymous services as arguments/properties Chris@0: if (false !== $nodes = $xpath->query('//container:argument[@type="service"][not(@id)]|//container:property[@type="service"][not(@id)]')) { Chris@0: foreach ($nodes as $node) { Chris@0: // give it a unique name Chris@0: $id = sprintf('%s_%d', hash('sha256', $file), ++$count); Chris@0: $node->setAttribute('id', $id); Chris@0: Chris@0: if ($services = $this->getChildren($node, 'service')) { Chris@0: $definitions[$id] = array($services[0], $file, false); Chris@0: $services[0]->setAttribute('id', $id); Chris@0: Chris@0: // anonymous services are always private Chris@0: // we could not use the constant false here, because of XML parsing Chris@0: $services[0]->setAttribute('public', 'false'); Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: // anonymous services "in the wild" Chris@0: if (false !== $nodes = $xpath->query('//container:services/container:service[not(@id)]')) { Chris@0: foreach ($nodes as $node) { Chris@0: // give it a unique name Chris@0: $id = sprintf('%s_%d', hash('sha256', $file), ++$count); Chris@0: $node->setAttribute('id', $id); Chris@0: $definitions[$id] = array($node, $file, true); Chris@0: } Chris@0: } Chris@0: Chris@0: // resolve definitions Chris@0: krsort($definitions); Chris@0: foreach ($definitions as $id => list($domElement, $file, $wild)) { Chris@0: if (null !== $definition = $this->parseDefinition($domElement, $file)) { Chris@0: $this->container->setDefinition($id, $definition); Chris@0: } Chris@0: Chris@0: if (true === $wild) { Chris@0: $tmpDomElement = new \DOMElement('_services', null, self::NS); Chris@0: $domElement->parentNode->replaceChild($tmpDomElement, $domElement); Chris@0: $tmpDomElement->setAttribute('id', $id); Chris@0: } else { Chris@0: $domElement->parentNode->removeChild($domElement); Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns arguments as valid php types. Chris@0: * Chris@0: * @param \DOMElement $node Chris@0: * @param string $name Chris@0: * @param bool $lowercase Chris@0: * Chris@0: * @return mixed Chris@0: */ Chris@0: private function getArgumentsAsPhp(\DOMElement $node, $name, $lowercase = true) Chris@0: { Chris@0: $arguments = array(); Chris@0: foreach ($this->getChildren($node, $name) as $arg) { Chris@0: if ($arg->hasAttribute('name')) { Chris@0: $arg->setAttribute('key', $arg->getAttribute('name')); Chris@0: } Chris@0: Chris@0: // this is used by DefinitionDecorator to overwrite a specific Chris@0: // argument of the parent definition Chris@0: if ($arg->hasAttribute('index')) { Chris@0: $key = 'index_'.$arg->getAttribute('index'); Chris@0: } elseif (!$arg->hasAttribute('key')) { Chris@0: // Append an empty argument, then fetch its key to overwrite it later Chris@0: $arguments[] = null; Chris@0: $keys = array_keys($arguments); Chris@0: $key = array_pop($keys); Chris@0: } else { Chris@0: $key = $arg->getAttribute('key'); Chris@0: Chris@0: // parameter keys are case insensitive Chris@0: if ('parameter' == $name && $lowercase) { Chris@0: $key = strtolower($key); Chris@0: } Chris@0: } Chris@0: Chris@0: switch ($arg->getAttribute('type')) { Chris@0: case 'service': Chris@0: $onInvalid = $arg->getAttribute('on-invalid'); Chris@0: $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE; Chris@0: if ('ignore' == $onInvalid) { Chris@0: $invalidBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE; Chris@0: } elseif ('null' == $onInvalid) { Chris@0: $invalidBehavior = ContainerInterface::NULL_ON_INVALID_REFERENCE; Chris@0: } Chris@0: Chris@0: $arguments[$key] = new Reference($arg->getAttribute('id'), $invalidBehavior); Chris@0: break; Chris@0: case 'expression': Chris@0: $arguments[$key] = new Expression($arg->nodeValue); Chris@0: break; Chris@0: case 'collection': Chris@0: $arguments[$key] = $this->getArgumentsAsPhp($arg, $name, false); Chris@0: break; Chris@0: case 'string': Chris@0: $arguments[$key] = $arg->nodeValue; Chris@0: break; Chris@0: case 'constant': Chris@0: $arguments[$key] = constant(trim($arg->nodeValue)); Chris@0: break; Chris@0: default: Chris@0: $arguments[$key] = XmlUtils::phpize($arg->nodeValue); Chris@0: } Chris@0: } Chris@0: Chris@0: return $arguments; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Get child elements by name. Chris@0: * Chris@0: * @param \DOMNode $node Chris@0: * @param mixed $name Chris@0: * Chris@0: * @return array Chris@0: */ Chris@0: private function getChildren(\DOMNode $node, $name) Chris@0: { Chris@0: $children = array(); Chris@0: foreach ($node->childNodes as $child) { Chris@0: if ($child instanceof \DOMElement && $child->localName === $name && $child->namespaceURI === self::NS) { Chris@0: $children[] = $child; Chris@0: } Chris@0: } Chris@0: Chris@0: return $children; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Validates a documents XML schema. Chris@0: * Chris@0: * @param \DOMDocument $dom Chris@0: * Chris@0: * @return bool Chris@0: * Chris@0: * @throws RuntimeException When extension references a non-existent XSD file Chris@0: */ Chris@0: public function validateSchema(\DOMDocument $dom) Chris@0: { Chris@0: $schemaLocations = array('http://symfony.com/schema/dic/services' => str_replace('\\', '/', __DIR__.'/schema/dic/services/services-1.0.xsd')); Chris@0: Chris@0: if ($element = $dom->documentElement->getAttributeNS('http://www.w3.org/2001/XMLSchema-instance', 'schemaLocation')) { Chris@0: $items = preg_split('/\s+/', $element); Chris@0: for ($i = 0, $nb = count($items); $i < $nb; $i += 2) { Chris@0: if (!$this->container->hasExtension($items[$i])) { Chris@0: continue; Chris@0: } Chris@0: Chris@0: if (($extension = $this->container->getExtension($items[$i])) && false !== $extension->getXsdValidationBasePath()) { Chris@0: $path = str_replace($extension->getNamespace(), str_replace('\\', '/', $extension->getXsdValidationBasePath()).'/', $items[$i + 1]); Chris@0: Chris@0: if (!is_file($path)) { Chris@0: throw new RuntimeException(sprintf('Extension "%s" references a non-existent XSD file "%s"', get_class($extension), $path)); Chris@0: } Chris@0: Chris@0: $schemaLocations[$items[$i]] = $path; Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: $tmpfiles = array(); Chris@0: $imports = ''; Chris@0: foreach ($schemaLocations as $namespace => $location) { Chris@0: $parts = explode('/', $location); Chris@0: if (0 === stripos($location, 'phar://')) { Chris@0: $tmpfile = tempnam(sys_get_temp_dir(), 'sf2'); Chris@0: if ($tmpfile) { Chris@0: copy($location, $tmpfile); Chris@0: $tmpfiles[] = $tmpfile; Chris@0: $parts = explode('/', str_replace('\\', '/', $tmpfile)); Chris@0: } Chris@0: } Chris@0: $drive = '\\' === DIRECTORY_SEPARATOR ? array_shift($parts).'/' : ''; Chris@0: $location = 'file:///'.$drive.implode('/', array_map('rawurlencode', $parts)); Chris@0: Chris@0: $imports .= sprintf(' '."\n", $namespace, $location); Chris@0: } Chris@0: Chris@0: $source = << Chris@0: Chris@0: Chris@0: Chris@0: $imports Chris@0: Chris@0: EOF Chris@0: ; Chris@0: Chris@0: $disableEntities = libxml_disable_entity_loader(false); Chris@0: $valid = @$dom->schemaValidateSource($source); Chris@0: libxml_disable_entity_loader($disableEntities); Chris@0: Chris@0: foreach ($tmpfiles as $tmpfile) { Chris@0: @unlink($tmpfile); Chris@0: } Chris@0: Chris@0: return $valid; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Validates an alias. Chris@0: * Chris@0: * @param \DOMElement $alias Chris@0: * @param string $file Chris@0: */ Chris@0: private function validateAlias(\DOMElement $alias, $file) Chris@0: { Chris@0: foreach ($alias->attributes as $name => $node) { Chris@0: if (!in_array($name, array('alias', 'id', 'public'))) { Chris@0: @trigger_error(sprintf('Using the attribute "%s" is deprecated for the service "%s" which is defined as an alias in "%s". Allowed attributes for service aliases are "alias", "id" and "public". The XmlFileLoader will raise an exception in Symfony 4.0, instead of silently ignoring unsupported attributes.', $name, $alias->getAttribute('id'), $file), E_USER_DEPRECATED); Chris@0: } Chris@0: } Chris@0: Chris@0: foreach ($alias->childNodes as $child) { Chris@0: if ($child instanceof \DOMElement && $child->namespaceURI === self::NS) { Chris@0: @trigger_error(sprintf('Using the element "%s" is deprecated for the service "%s" which is defined as an alias in "%s". The XmlFileLoader will raise an exception in Symfony 4.0, instead of silently ignoring unsupported elements.', $child->localName, $alias->getAttribute('id'), $file), E_USER_DEPRECATED); Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Validates an extension. Chris@0: * Chris@0: * @param \DOMDocument $dom Chris@0: * @param string $file Chris@0: * Chris@0: * @throws InvalidArgumentException When no extension is found corresponding to a tag Chris@0: */ Chris@0: private function validateExtensions(\DOMDocument $dom, $file) Chris@0: { Chris@0: foreach ($dom->documentElement->childNodes as $node) { Chris@0: if (!$node instanceof \DOMElement || 'http://symfony.com/schema/dic/services' === $node->namespaceURI) { Chris@0: continue; Chris@0: } Chris@0: Chris@0: // can it be handled by an extension? Chris@0: if (!$this->container->hasExtension($node->namespaceURI)) { Chris@0: $extensionNamespaces = array_filter(array_map(function ($ext) { return $ext->getNamespace(); }, $this->container->getExtensions())); Chris@0: throw new InvalidArgumentException(sprintf( Chris@0: 'There is no extension able to load the configuration for "%s" (in %s). Looked for namespace "%s", found %s', Chris@0: $node->tagName, Chris@0: $file, Chris@0: $node->namespaceURI, Chris@0: $extensionNamespaces ? sprintf('"%s"', implode('", "', $extensionNamespaces)) : 'none' Chris@0: )); Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Loads from an extension. Chris@0: * Chris@0: * @param \DOMDocument $xml Chris@0: */ Chris@0: private function loadFromExtensions(\DOMDocument $xml) Chris@0: { Chris@0: foreach ($xml->documentElement->childNodes as $node) { Chris@0: if (!$node instanceof \DOMElement || $node->namespaceURI === self::NS) { Chris@0: continue; Chris@0: } Chris@0: Chris@0: $values = static::convertDomElementToArray($node); Chris@0: if (!is_array($values)) { Chris@0: $values = array(); Chris@0: } Chris@0: Chris@0: $this->container->loadFromExtension($node->namespaceURI, $values); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Converts a \DomElement object to a PHP array. Chris@0: * Chris@0: * The following rules applies during the conversion: Chris@0: * Chris@0: * * Each tag is converted to a key value or an array Chris@0: * if there is more than one "value" Chris@0: * Chris@0: * * The content of a tag is set under a "value" key (bar) Chris@0: * if the tag also has some nested tags Chris@0: * Chris@0: * * The attributes are converted to keys () Chris@0: * Chris@0: * * The nested-tags are converted to keys (bar) Chris@0: * Chris@0: * @param \DomElement $element A \DomElement instance Chris@0: * Chris@0: * @return array A PHP array Chris@0: */ Chris@0: public static function convertDomElementToArray(\DOMElement $element) Chris@0: { Chris@0: return XmlUtils::convertDomElementToArray($element); Chris@0: } Chris@0: }