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