This repository has been archived on 2023-08-20. You can view files and clone it, but cannot push or open issues or pull requests.
symfony/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php

533 lines
18 KiB
PHP
Raw Normal View History

2010-01-04 14:26:20 +00:00
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\DependencyInjection\Loader;
2010-01-04 14:26:20 +00:00
use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\Config\Util\XmlUtils;
2011-01-26 23:14:31 +00:00
use Symfony\Component\DependencyInjection\DefinitionDecorator;
2011-01-17 22:28:59 +00:00
use Symfony\Component\DependencyInjection\ContainerInterface;
2011-01-07 14:44:29 +00:00
use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\ExpressionLanguage\Expression;
2010-01-04 14:26:20 +00:00
/**
* XmlFileLoader loads XML files service definitions.
*
* @author Fabien Potencier <fabien@symfony.com>
2010-01-04 14:26:20 +00:00
*/
class XmlFileLoader extends FileLoader
{
const NS = 'http://symfony.com/schema/dic/services';
/**
2010-10-26 15:01:39 +01:00
* Loads an XML file.
*
2011-05-30 00:25:35 +01:00
* @param mixed $file The resource
* @param string $type The resource type
*/
public function load($file, $type = null)
{
$path = $this->locator->locate($file);
$xml = $this->parseFileToDOM($path);
2010-01-04 14:26:20 +00:00
$this->container->addResource(new FileResource($path));
2010-01-04 14:26:20 +00:00
// anonymous services
$this->processAnonymousServices($xml, $path);
2010-01-04 14:26:20 +00:00
// imports
$this->parseImports($xml, $path);
2010-01-04 14:26:20 +00:00
// parameters
$this->parseParameters($xml, $path);
// extensions
$this->loadFromExtensions($xml);
// services
$this->parseDefinitions($xml, $path);
2010-01-04 14:26:20 +00:00
}
/**
* Returns true if this class supports the given resource.
*
* @param mixed $resource A resource
* @param string $type The resource type
*
* @return Boolean true if this class supports the given resource, false otherwise
*/
public function supports($resource, $type = null)
{
return is_string($resource) && 'xml' === pathinfo($resource, PATHINFO_EXTENSION);
}
2011-02-13 18:06:41 +00:00
/**
* Parses parameters
*
* @param \DOMDocument $xml
* @param string $file
2011-02-13 18:06:41 +00:00
*/
private function parseParameters(\DOMDocument $xml, $file)
2010-01-04 14:26:20 +00:00
{
if ($parameters = $this->getChildren($xml->documentElement, 'parameters')) {
$this->container->getParameterBag()->add($this->getArgumentsAsPhp($parameters[0], 'parameter'));
}
2010-01-04 14:26:20 +00:00
}
2011-02-13 18:06:41 +00:00
/**
* Parses imports
*
* @param \DOMDocument $xml
* @param string $file
2011-02-13 18:06:41 +00:00
*/
private function parseImports(\DOMDocument $xml, $file)
2010-01-04 14:26:20 +00:00
{
$xpath = new \DOMXPath($xml);
$xpath->registerNamespace('container', self::NS);
if (false === $imports = $xpath->query('//container:imports/container:import')) {
return;
}
2011-02-15 09:09:58 +00:00
foreach ($imports as $import) {
$this->setCurrentDir(dirname($file));
$this->import($import->getAttribute('resource'), null, (Boolean) XmlUtils::phpize($import->getAttribute('ignore-errors')), $file);
}
}
2011-02-13 18:06:41 +00:00
/**
* Parses multiple definitions
*
* @param \DOMDocument $xml
* @param string $file
2011-02-13 18:06:41 +00:00
*/
private function parseDefinitions(\DOMDocument $xml, $file)
2010-01-04 14:26:20 +00:00
{
$xpath = new \DOMXPath($xml);
$xpath->registerNamespace('container', self::NS);
if (false === $services = $xpath->query('//container:services/container:service')) {
return;
}
2011-02-15 09:09:58 +00:00
foreach ($services as $service) {
$this->parseDefinition((string) $service->getAttribute('id'), $service, $file);
}
2010-01-04 14:26:20 +00:00
}
2011-02-13 18:06:41 +00:00
/**
* Parses an individual Definition
*
* @param string $id
* @param \DOMElement $service
* @param string $file
2011-02-13 18:06:41 +00:00
*/
private function parseDefinition($id, \DOMElement $service, $file)
2010-01-04 14:26:20 +00:00
{
if ($alias = $service->getAttribute('alias')) {
2011-01-07 14:44:29 +00:00
$public = true;
if ($publicAttr = $service->getAttribute('public')) {
$public = XmlUtils::phpize($publicAttr);
2011-01-07 14:44:29 +00:00
}
$this->container->setAlias($id, new Alias($alias, $public));
2010-01-04 14:26:20 +00:00
return;
}
2010-01-04 14:26:20 +00:00
if ($parent = $service->getAttribute('parent')) {
$definition = new DefinitionDecorator($parent);
2011-01-26 23:14:31 +00:00
} else {
$definition = new Definition();
}
2010-01-04 14:26:20 +00:00
2013-03-29 23:21:12 +00:00
foreach (array('class', 'scope', 'public', 'factory-class', 'factory-method', 'factory-service', 'synthetic', 'synchronized', 'lazy', 'abstract') as $key) {
if ($value = $service->getAttribute($key)) {
$method = 'set'.str_replace('-', '', $key);
$definition->$method(XmlUtils::phpize($value));
}
2010-01-04 14:26:20 +00:00
}
if ($files = $this->getChildren($service, 'file')) {
$definition->setFile($files[0]->nodeValue);
2010-01-04 14:26:20 +00:00
}
$definition->setArguments($this->getArgumentsAsPhp($service, 'argument'));
$definition->setProperties($this->getArgumentsAsPhp($service, 'property'));
2010-01-04 14:26:20 +00:00
if ($configurators = $this->getChildren($service, 'configurator')) {
$configurator = $configurators[0];
if ($function = $configurator->getAttribute('function')) {
$definition->setConfigurator($function);
} else {
if ($childService = $configurator->getAttribute('service')) {
$class = new Reference($childService, ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, false);
} else {
$class = $configurator->getAttribute('class');
}
$definition->setConfigurator(array($class, $configurator->getAttribute('method')));
}
}
2010-01-04 14:26:20 +00:00
foreach ($this->getChildren($service, 'call') as $call) {
$definition->addMethodCall($call->getAttribute('method'), $this->getArgumentsAsPhp($call, 'argument'));
}
foreach ($this->getChildren($service, 'tag') as $tag) {
$parameters = array();
foreach ($tag->attributes as $name => $node) {
if ('name' === $name) {
continue;
}
2013-11-10 19:01:46 +00:00
if (false !== strpos($name, '-') && false === strpos($name, '_') && !array_key_exists($normalizedName = str_replace('-', '_', $name), $parameters)) {
$parameters[$normalizedName] = XmlUtils::phpize($node->nodeValue);
2013-11-10 19:01:46 +00:00
}
// keep not normalized key for BC too
$parameters[$name] = XmlUtils::phpize($node->nodeValue);
}
$definition->addTag($tag->getAttribute('name'), $parameters);
}
if ($value = $service->getAttribute('decorates')) {
$renameId = $service->hasAttribute('decoration-inner-name') ? $service->getAttribute('decoration-inner-name') : null;
$definition->setDecoratedService($value, $renameId);
}
$this->container->setDefinition($id, $definition);
}
/**
* 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
*/
private function parseFileToDOM($file)
2010-01-04 14:26:20 +00:00
{
try {
$dom = XmlUtils::loadFile($file, array($this, 'validateSchema'));
} catch (\InvalidArgumentException $e) {
throw new InvalidArgumentException(sprintf('Unable to parse file "%s".', $file), $e->getCode(), $e);
}
$this->validateExtensions($dom, $file);
2010-01-04 14:26:20 +00:00
return $dom;
2010-01-04 14:26:20 +00:00
}
2011-02-13 18:06:41 +00:00
/**
* Processes anonymous services
*
* @param \DOMDocument $xml
* @param string $file
2011-02-13 18:06:41 +00:00
*/
private function processAnonymousServices(\DOMDocument $xml, $file)
2010-01-04 14:26:20 +00:00
{
$definitions = array();
$count = 0;
2010-01-04 14:26:20 +00:00
$xpath = new \DOMXPath($xml);
$xpath->registerNamespace('container', self::NS);
// anonymous services as arguments/properties
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->setAttribute('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 = $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->setAttribute('id', $id);
if ($services = $this->getChildren($node, 'service')) {
$definitions[$id] = array($node, $file, true);
$services[0]->setAttribute('id', $id);
}
}
}
// resolve definitions
krsort($definitions);
foreach ($definitions as $id => $def) {
// anonymous services are always private
$def[0]->setAttribute('public', false);
$this->parseDefinition($id, $def[0], $def[1]);
$oNode = $def[0];
if (true === $def[2]) {
$nNode = new \DOMElement('_services', null, self::NS);
$oNode->parentNode->replaceChild($nNode, $oNode);
$nNode->setAttribute('id', $id);
} else {
$oNode->parentNode->removeChild($oNode);
}
}
}
/**
* 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;
}
/**
2011-02-13 18:06:41 +00:00
* Validates a documents XML schema.
*
* @param \DOMDocument $dom
*
* @return Boolean
*
* @throws RuntimeException When extension references a non-existent XSD file
*/
public function validateSchema(\DOMDocument $dom)
{
$schemaLocations = array('http://symfony.com/schema/dic/services' => str_replace('\\', '/', __DIR__.'/schema/dic/services/services-1.0.xsd'));
if ($element = $dom->documentElement->getAttributeNS('http://www.w3.org/2001/XMLSchema-instance', 'schemaLocation')) {
$items = preg_split('/\s+/', $element);
for ($i = 0, $nb = count($items); $i < $nb; $i += 2) {
if (!$this->container->hasExtension($items[$i])) {
continue;
}
if (($extension = $this->container->getExtension($items[$i])) && false !== $extension->getXsdValidationBasePath()) {
2010-06-27 17:54:03 +01:00
$path = str_replace($extension->getNamespace(), str_replace('\\', '/', $extension->getXsdValidationBasePath()).'/', $items[$i + 1]);
if (!is_file($path)) {
throw new RuntimeException(sprintf('Extension "%s" references a non-existent XSD file "%s"', get_class($extension), $path));
}
$schemaLocations[$items[$i]] = $path;
}
}
}
$tmpfiles = array();
$imports = '';
foreach ($schemaLocations as $namespace => $location) {
2010-05-21 15:18:29 +01:00
$parts = explode('/', $location);
if (0 === stripos($location, 'phar://')) {
$tmpfile = tempnam(sys_get_temp_dir(), 'sf2');
if ($tmpfile) {
copy($location, $tmpfile);
$tmpfiles[] = $tmpfile;
$parts = explode('/', str_replace('\\', '/', $tmpfile));
}
}
$drive = '\\' === DIRECTORY_SEPARATOR ? array_shift($parts).'/' : '';
2010-05-21 15:18:29 +01:00
$location = 'file:///'.$drive.implode('/', array_map('rawurlencode', $parts));
$imports .= sprintf(' <xsd:import namespace="%s" schemaLocation="%s" />'."\n", $namespace, $location);
}
$source = <<<EOF
<?xml version="1.0" encoding="utf-8" ?>
<xsd:schema xmlns="http://symfony.com/schema"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://symfony.com/schema"
elementFormDefault="qualified">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
$imports
</xsd:schema>
EOF
;
$valid = @$dom->schemaValidateSource($source);
foreach ($tmpfiles as $tmpfile) {
@unlink($tmpfile);
}
return $valid;
2010-01-04 14:26:20 +00:00
}
/**
2011-02-13 18:06:41 +00:00
* Validates an extension.
*
* @param \DOMDocument $dom
2012-05-18 18:41:48 +01:00
* @param string $file
2011-02-13 18:06:41 +00:00
*
* @throws InvalidArgumentException When no extension is found corresponding to a tag
*/
private function validateExtensions(\DOMDocument $dom, $file)
2010-01-04 14:26:20 +00:00
{
foreach ($dom->documentElement->childNodes as $node) {
if (!$node instanceof \DOMElement || 'http://symfony.com/schema/dic/services' === $node->namespaceURI) {
continue;
}
// can it be handled by an extension?
if (!$this->container->hasExtension($node->namespaceURI)) {
$extensionNamespaces = array_filter(array_map(function ($ext) { return $ext->getNamespace(); }, $this->container->getExtensions()));
throw new InvalidArgumentException(sprintf(
'There is no extension able to load the configuration for "%s" (in %s). Looked for namespace "%s", found %s',
$node->tagName,
$file,
$node->namespaceURI,
$extensionNamespaces ? sprintf('"%s"', implode('", "', $extensionNamespaces)) : 'none'
));
}
}
2010-01-04 14:26:20 +00:00
}
2011-02-13 18:06:41 +00:00
/**
* Loads from an extension.
*
* @param \DOMDocument $xml
2011-02-13 18:06:41 +00:00
*/
private function loadFromExtensions(\DOMDocument $xml)
2010-01-04 14:26:20 +00:00
{
foreach ($xml->documentElement->childNodes as $node) {
if (!$node instanceof \DOMElement || $node->namespaceURI === self::NS) {
continue;
}
2010-01-04 14:26:20 +00:00
$values = static::convertDomElementToArray($node);
if (!is_array($values)) {
$values = array();
}
$this->container->loadFromExtension($node->namespaceURI, $values);
}
2010-01-04 14:26:20 +00:00
}
/**
* Converts a \DomElement object to a PHP array.
*
* The following rules applies during the conversion:
*
* * Each tag is converted to a key value or an array
* if there is more than one "value"
*
* * The content of a tag is set under a "value" key (<foo>bar</foo>)
* if the tag also has some nested tags
*
* * The attributes are converted to keys (<foo foo="bar"/>)
*
* * The nested-tags are converted to keys (<foo><foo>bar</foo></foo>)
*
* @param \DomElement $element A \DomElement instance
*
* @return array A PHP array
*/
2012-07-09 13:50:58 +01:00
public static function convertDomElementToArray(\DomElement $element)
2010-01-04 14:26:20 +00:00
{
return XmlUtils::convertDomElementToArray($element);
2010-01-04 14:26:20 +00:00
}
}