vendor/symfony/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php
changeset 0 7f95f8617b0b
equal deleted inserted replaced
-1:000000000000 0:7f95f8617b0b
       
     1 <?php
       
     2 
       
     3 /*
       
     4  * This file is part of the Symfony package.
       
     5  *
       
     6  * (c) Fabien Potencier <fabien@symfony.com>
       
     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\Loader;
       
    13 
       
    14 use Symfony\Component\DependencyInjection\DefinitionDecorator;
       
    15 
       
    16 use Symfony\Component\DependencyInjection\ContainerInterface;
       
    17 use Symfony\Component\DependencyInjection\Alias;
       
    18 use Symfony\Component\DependencyInjection\Definition;
       
    19 use Symfony\Component\DependencyInjection\Reference;
       
    20 use Symfony\Component\DependencyInjection\ContainerBuilder;
       
    21 use Symfony\Component\DependencyInjection\SimpleXMLElement;
       
    22 use Symfony\Component\Config\Resource\FileResource;
       
    23 
       
    24 /**
       
    25  * XmlFileLoader loads XML files service definitions.
       
    26  *
       
    27  * @author Fabien Potencier <fabien@symfony.com>
       
    28  */
       
    29 class XmlFileLoader extends FileLoader
       
    30 {
       
    31     /**
       
    32      * Loads an XML file.
       
    33      *
       
    34      * @param mixed  $file The resource
       
    35      * @param string $type The resource type
       
    36      */
       
    37     public function load($file, $type = null)
       
    38     {
       
    39         $path = $this->locator->locate($file);
       
    40 
       
    41         $xml = $this->parseFile($path);
       
    42         $xml->registerXPathNamespace('container', 'http://symfony.com/schema/dic/services');
       
    43 
       
    44         $this->container->addResource(new FileResource($path));
       
    45 
       
    46         // anonymous services
       
    47         $xml = $this->processAnonymousServices($xml, $path);
       
    48 
       
    49         // imports
       
    50         $this->parseImports($xml, $path);
       
    51 
       
    52         // parameters
       
    53         $this->parseParameters($xml, $path);
       
    54 
       
    55         // extensions
       
    56         $this->loadFromExtensions($xml);
       
    57 
       
    58         // services
       
    59         $this->parseDefinitions($xml, $path);
       
    60     }
       
    61 
       
    62     /**
       
    63      * Returns true if this class supports the given resource.
       
    64      *
       
    65      * @param mixed  $resource A resource
       
    66      * @param string $type     The resource type
       
    67      *
       
    68      * @return Boolean true if this class supports the given resource, false otherwise
       
    69      */
       
    70     public function supports($resource, $type = null)
       
    71     {
       
    72         return is_string($resource) && 'xml' === pathinfo($resource, PATHINFO_EXTENSION);
       
    73     }
       
    74 
       
    75     /**
       
    76      * Parses parameters
       
    77      *
       
    78      * @param SimpleXMLElement $xml
       
    79      * @param string $file
       
    80      * @return void
       
    81      */
       
    82     private function parseParameters(SimpleXMLElement $xml, $file)
       
    83     {
       
    84         if (!$xml->parameters) {
       
    85             return;
       
    86         }
       
    87 
       
    88         $this->container->getParameterBag()->add($xml->parameters->getArgumentsAsPhp('parameter'));
       
    89     }
       
    90 
       
    91     /**
       
    92      * Parses imports
       
    93      *
       
    94      * @param SimpleXMLElement $xml
       
    95      * @param string $file
       
    96      * @return void
       
    97      */
       
    98     private function parseImports(SimpleXMLElement $xml, $file)
       
    99     {
       
   100         if (false === $imports = $xml->xpath('//container:imports/container:import')) {
       
   101             return;
       
   102         }
       
   103 
       
   104         foreach ($imports as $import) {
       
   105             $this->setCurrentDir(dirname($file));
       
   106             $this->import((string) $import['resource'], null, (Boolean) $import->getAttributeAsPhp('ignore-errors'), $file);
       
   107         }
       
   108     }
       
   109 
       
   110     /**
       
   111      * Parses multiple definitions
       
   112      *
       
   113      * @param SimpleXMLElement $xml
       
   114      * @param string $file
       
   115      * @return void
       
   116      */
       
   117     private function parseDefinitions(SimpleXMLElement $xml, $file)
       
   118     {
       
   119         if (false === $services = $xml->xpath('//container:services/container:service')) {
       
   120             return;
       
   121         }
       
   122 
       
   123         foreach ($services as $service) {
       
   124             $this->parseDefinition((string) $service['id'], $service, $file);
       
   125         }
       
   126     }
       
   127 
       
   128     /**
       
   129      * Parses an individual Definition
       
   130      *
       
   131      * @param string $id
       
   132      * @param SimpleXMLElement $service
       
   133      * @param string $file
       
   134      * @return void
       
   135      */
       
   136     private function parseDefinition($id, $service, $file)
       
   137     {
       
   138         if ((string) $service['alias']) {
       
   139             $public = true;
       
   140             if (isset($service['public'])) {
       
   141                 $public = $service->getAttributeAsPhp('public');
       
   142             }
       
   143             $this->container->setAlias($id, new Alias((string) $service['alias'], $public));
       
   144 
       
   145             return;
       
   146         }
       
   147 
       
   148         if (isset($service['parent'])) {
       
   149             $definition = new DefinitionDecorator((string) $service['parent']);
       
   150         } else {
       
   151             $definition = new Definition();
       
   152         }
       
   153 
       
   154         foreach (array('class', 'scope', 'public', 'factory-class', 'factory-method', 'factory-service', 'synthetic', 'abstract') as $key) {
       
   155             if (isset($service[$key])) {
       
   156                 $method = 'set'.str_replace('-', '', $key);
       
   157                 $definition->$method((string) $service->getAttributeAsPhp($key));
       
   158             }
       
   159         }
       
   160 
       
   161         if ($service->file) {
       
   162             $definition->setFile((string) $service->file);
       
   163         }
       
   164 
       
   165         $definition->setArguments($service->getArgumentsAsPhp('argument'));
       
   166         $definition->setProperties($service->getArgumentsAsPhp('property'));
       
   167 
       
   168         if (isset($service->configurator)) {
       
   169             if (isset($service->configurator['function'])) {
       
   170                 $definition->setConfigurator((string) $service->configurator['function']);
       
   171             } else {
       
   172                 if (isset($service->configurator['service'])) {
       
   173                     $class = new Reference((string) $service->configurator['service'], ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, false);
       
   174                 } else {
       
   175                     $class = (string) $service->configurator['class'];
       
   176                 }
       
   177 
       
   178                 $definition->setConfigurator(array($class, (string) $service->configurator['method']));
       
   179             }
       
   180         }
       
   181 
       
   182         foreach ($service->call as $call) {
       
   183             $definition->addMethodCall((string) $call['method'], $call->getArgumentsAsPhp('argument'));
       
   184         }
       
   185 
       
   186         foreach ($service->tag as $tag) {
       
   187             $parameters = array();
       
   188             foreach ($tag->attributes() as $name => $value) {
       
   189                 if ('name' === $name) {
       
   190                     continue;
       
   191                 }
       
   192 
       
   193                 $parameters[$name] = SimpleXMLElement::phpize($value);
       
   194             }
       
   195 
       
   196             $definition->addTag((string) $tag['name'], $parameters);
       
   197         }
       
   198 
       
   199         $this->container->setDefinition($id, $definition);
       
   200     }
       
   201 
       
   202     /**
       
   203      * Parses a XML file.
       
   204      *
       
   205      * @param string $file Path to a file
       
   206      * @throws \InvalidArgumentException When loading of XML file returns error
       
   207      */
       
   208     private function parseFile($file)
       
   209     {
       
   210         $dom = new \DOMDocument();
       
   211         libxml_use_internal_errors(true);
       
   212         if (!$dom->load($file, LIBXML_COMPACT)) {
       
   213             throw new \InvalidArgumentException(implode("\n", $this->getXmlErrors()));
       
   214         }
       
   215         $dom->validateOnParse = true;
       
   216         $dom->normalizeDocument();
       
   217         libxml_use_internal_errors(false);
       
   218         $this->validate($dom, $file);
       
   219 
       
   220         return simplexml_import_dom($dom, 'Symfony\\Component\\DependencyInjection\\SimpleXMLElement');
       
   221     }
       
   222 
       
   223     /**
       
   224      * Processes anonymous services
       
   225      *
       
   226      * @param SimpleXMLElement $xml
       
   227      * @param string $file
       
   228      * @return array An array of anonymous services
       
   229      */
       
   230     private function processAnonymousServices(SimpleXMLElement $xml, $file)
       
   231     {
       
   232         $definitions = array();
       
   233         $count = 0;
       
   234 
       
   235         // anonymous services as arguments
       
   236         if (false === $nodes = $xml->xpath('//container:argument[@type="service"][not(@id)]')) {
       
   237             return $xml;
       
   238         }
       
   239         foreach ($nodes as $node) {
       
   240             // give it a unique name
       
   241             $node['id'] = sprintf('%s_%d', md5($file), ++$count);
       
   242 
       
   243             $definitions[(string) $node['id']] = array($node->service, $file, false);
       
   244             $node->service['id'] = (string) $node['id'];
       
   245         }
       
   246 
       
   247         // anonymous services "in the wild"
       
   248         if (false === $nodes = $xml->xpath('//container:services/container:service[not(@id)]')) {
       
   249             return $xml;
       
   250         }
       
   251         foreach ($nodes as $node) {
       
   252             // give it a unique name
       
   253             $node['id'] = sprintf('%s_%d', md5($file), ++$count);
       
   254 
       
   255             $definitions[(string) $node['id']] = array($node, $file, true);
       
   256             $node->service['id'] = (string) $node['id'];
       
   257         }
       
   258 
       
   259         // resolve definitions
       
   260         krsort($definitions);
       
   261         foreach ($definitions as $id => $def) {
       
   262             // anonymous services are always private
       
   263             $def[0]['public'] = false;
       
   264 
       
   265             $this->parseDefinition($id, $def[0], $def[1]);
       
   266 
       
   267             $oNode = dom_import_simplexml($def[0]);
       
   268             if (true === $def[2]) {
       
   269                 $nNode = new \DOMElement('_services');
       
   270                 $oNode->parentNode->replaceChild($nNode, $oNode);
       
   271                 $nNode->setAttribute('id', $id);
       
   272             } else {
       
   273                 $oNode->parentNode->removeChild($oNode);
       
   274             }
       
   275         }
       
   276 
       
   277         return $xml;
       
   278     }
       
   279 
       
   280     /**
       
   281      * Validates an XML document.
       
   282      *
       
   283      * @param DOMDocument $dom
       
   284      * @param string $file
       
   285      */
       
   286     private function validate(\DOMDocument $dom, $file)
       
   287     {
       
   288         $this->validateSchema($dom, $file);
       
   289         $this->validateExtensions($dom, $file);
       
   290     }
       
   291 
       
   292     /**
       
   293      * Validates a documents XML schema.
       
   294      *
       
   295      * @param \DOMDocument $dom
       
   296      * @param string $file
       
   297      * @return void
       
   298      *
       
   299      * @throws \RuntimeException         When extension references a non-existent XSD file
       
   300      * @throws \InvalidArgumentException When xml doesn't validate its xsd schema
       
   301      */
       
   302     private function validateSchema(\DOMDocument $dom, $file)
       
   303     {
       
   304         $schemaLocations = array('http://symfony.com/schema/dic/services' => str_replace('\\', '/', __DIR__.'/schema/dic/services/services-1.0.xsd'));
       
   305 
       
   306         if ($element = $dom->documentElement->getAttributeNS('http://www.w3.org/2001/XMLSchema-instance', 'schemaLocation')) {
       
   307             $items = preg_split('/\s+/', $element);
       
   308             for ($i = 0, $nb = count($items); $i < $nb; $i += 2) {
       
   309                 if (!$this->container->hasExtension($items[$i])) {
       
   310                     continue;
       
   311                 }
       
   312 
       
   313                 if (($extension = $this->container->getExtension($items[$i])) && false !== $extension->getXsdValidationBasePath()) {
       
   314                     $path = str_replace($extension->getNamespace(), str_replace('\\', '/', $extension->getXsdValidationBasePath()).'/', $items[$i + 1]);
       
   315 
       
   316                     if (!file_exists($path)) {
       
   317                         throw new \RuntimeException(sprintf('Extension "%s" references a non-existent XSD file "%s"', get_class($extension), $path));
       
   318                     }
       
   319 
       
   320                     $schemaLocations[$items[$i]] = $path;
       
   321                 }
       
   322             }
       
   323         }
       
   324 
       
   325         $tmpfiles = array();
       
   326         $imports = '';
       
   327         foreach ($schemaLocations as $namespace => $location) {
       
   328             $parts = explode('/', $location);
       
   329             if (preg_match('#^phar://#i', $location)) {
       
   330                 $tmpfile = tempnam(sys_get_temp_dir(), 'sf2');
       
   331                 if ($tmpfile) {
       
   332                     file_put_contents($tmpfile, file_get_contents($location));
       
   333                     $tmpfiles[] = $tmpfile;
       
   334                     $parts = explode('/', str_replace('\\', '/', $tmpfile));
       
   335                 }
       
   336             }
       
   337             $drive = '\\' === DIRECTORY_SEPARATOR ? array_shift($parts).'/' : '';
       
   338             $location = 'file:///'.$drive.implode('/', array_map('rawurlencode', $parts));
       
   339 
       
   340             $imports .= sprintf('  <xsd:import namespace="%s" schemaLocation="%s" />'."\n", $namespace, $location);
       
   341         }
       
   342 
       
   343         $source = <<<EOF
       
   344 <?xml version="1.0" encoding="utf-8" ?>
       
   345 <xsd:schema xmlns="http://symfony.com/schema"
       
   346     xmlns:xsd="http://www.w3.org/2001/XMLSchema"
       
   347     targetNamespace="http://symfony.com/schema"
       
   348     elementFormDefault="qualified">
       
   349 
       
   350     <xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
       
   351 $imports
       
   352 </xsd:schema>
       
   353 EOF
       
   354         ;
       
   355 
       
   356         $current = libxml_use_internal_errors(true);
       
   357         $valid = $dom->schemaValidateSource($source);
       
   358         foreach ($tmpfiles as $tmpfile) {
       
   359             @unlink($tmpfile);
       
   360         }
       
   361         if (!$valid) {
       
   362             throw new \InvalidArgumentException(implode("\n", $this->getXmlErrors()));
       
   363         }
       
   364         libxml_use_internal_errors($current);
       
   365     }
       
   366 
       
   367     /**
       
   368      * Validates an extension.
       
   369      *
       
   370      * @param \DOMDocument $dom
       
   371      * @param string $file
       
   372      * @return void
       
   373      *
       
   374      * @throws  \InvalidArgumentException When non valid tag are found or no extension are found
       
   375      */
       
   376     private function validateExtensions(\DOMDocument $dom, $file)
       
   377     {
       
   378         foreach ($dom->documentElement->childNodes as $node) {
       
   379             if (!$node instanceof \DOMElement || 'http://symfony.com/schema/dic/services' === $node->namespaceURI) {
       
   380                 continue;
       
   381             }
       
   382 
       
   383             // can it be handled by an extension?
       
   384             if (!$this->container->hasExtension($node->namespaceURI)) {
       
   385                 $extensionNamespaces = array_filter(array_map(function ($ext) { return $ext->getNamespace(); }, $this->container->getExtensions()));
       
   386                 throw new \InvalidArgumentException(sprintf(
       
   387                     'There is no extension able to load the configuration for "%s" (in %s). Looked for namespace "%s", found %s',
       
   388                     $node->tagName,
       
   389                     $file,
       
   390                     $node->namespaceURI,
       
   391                     $extensionNamespaces ? sprintf('"%s"', implode('", "', $extensionNamespaces)) : 'none'
       
   392                 ));
       
   393             }
       
   394         }
       
   395     }
       
   396 
       
   397     /**
       
   398      * Returns an array of XML errors.
       
   399      *
       
   400      * @return array
       
   401      */
       
   402     private function getXmlErrors()
       
   403     {
       
   404         $errors = array();
       
   405         foreach (libxml_get_errors() as $error) {
       
   406             $errors[] = sprintf('[%s %s] %s (in %s - line %d, column %d)',
       
   407                 LIBXML_ERR_WARNING == $error->level ? 'WARNING' : 'ERROR',
       
   408                 $error->code,
       
   409                 trim($error->message),
       
   410                 $error->file ? $error->file : 'n/a',
       
   411                 $error->line,
       
   412                 $error->column
       
   413             );
       
   414         }
       
   415 
       
   416         libxml_clear_errors();
       
   417 
       
   418         return $errors;
       
   419     }
       
   420 
       
   421     /**
       
   422      * Loads from an extension.
       
   423      *
       
   424      * @param SimpleXMLElement $xml
       
   425      * @return void
       
   426      */
       
   427     private function loadFromExtensions(SimpleXMLElement $xml)
       
   428     {
       
   429         foreach (dom_import_simplexml($xml)->childNodes as $node) {
       
   430             if (!$node instanceof \DOMElement || $node->namespaceURI === 'http://symfony.com/schema/dic/services') {
       
   431                 continue;
       
   432             }
       
   433 
       
   434             $values = static::convertDomElementToArray($node);
       
   435             if (!is_array($values)) {
       
   436                 $values = array();
       
   437             }
       
   438 
       
   439             $this->container->loadFromExtension($node->namespaceURI, $values);
       
   440         }
       
   441     }
       
   442 
       
   443     /**
       
   444      * Converts a \DomElement object to a PHP array.
       
   445      *
       
   446      * The following rules applies during the conversion:
       
   447      *
       
   448      *  * Each tag is converted to a key value or an array
       
   449      *    if there is more than one "value"
       
   450      *
       
   451      *  * The content of a tag is set under a "value" key (<foo>bar</foo>)
       
   452      *    if the tag also has some nested tags
       
   453      *
       
   454      *  * The attributes are converted to keys (<foo foo="bar"/>)
       
   455      *
       
   456      *  * The nested-tags are converted to keys (<foo><foo>bar</foo></foo>)
       
   457      *
       
   458      * @param \DomElement $element A \DomElement instance
       
   459      *
       
   460      * @return array A PHP array
       
   461      */
       
   462     static public function convertDomElementToArray(\DomElement $element)
       
   463     {
       
   464         $empty = true;
       
   465         $config = array();
       
   466         foreach ($element->attributes as $name => $node) {
       
   467             $config[$name] = SimpleXMLElement::phpize($node->value);
       
   468             $empty = false;
       
   469         }
       
   470 
       
   471         $nodeValue = false;
       
   472         foreach ($element->childNodes as $node) {
       
   473             if ($node instanceof \DOMText) {
       
   474                 if (trim($node->nodeValue)) {
       
   475                     $nodeValue = trim($node->nodeValue);
       
   476                     $empty = false;
       
   477                 }
       
   478             } elseif (!$node instanceof \DOMComment) {
       
   479                 if ($node instanceof \DOMElement && '_services' === $node->nodeName) {
       
   480                     $value = new Reference($node->getAttribute('id'));
       
   481                 } else {
       
   482                     $value = static::convertDomElementToArray($node);
       
   483                 }
       
   484 
       
   485                 $key = $node->localName;
       
   486                 if (isset($config[$key])) {
       
   487                     if (!is_array($config[$key]) || !is_int(key($config[$key]))) {
       
   488                         $config[$key] = array($config[$key]);
       
   489                     }
       
   490                     $config[$key][] = $value;
       
   491                 } else {
       
   492                     $config[$key] = $value;
       
   493                 }
       
   494 
       
   495                 $empty = false;
       
   496             }
       
   497         }
       
   498 
       
   499         if (false !== $nodeValue) {
       
   500             $value = SimpleXMLElement::phpize($nodeValue);
       
   501             if (count($config)) {
       
   502                 $config['value'] = $value;
       
   503             } else {
       
   504                 $config = $value;
       
   505             }
       
   506         }
       
   507 
       
   508         return !$empty ? $config : null;
       
   509     }
       
   510 }