XmlFileLoader::getChildren()   B
last analyzed

Complexity

Conditions 5
Paths 3

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 11
rs 8.8571
cc 5
eloc 6
nc 3
nop 2
1
<?php
2
3
/*
4
 * This file is part of the Symfony package.
5
 *
6
 * (c) Fabien Potencier <[email protected]>
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\Config\Resource\FileResource;
15
use Symfony\Component\Config\Util\XmlUtils;
16
use Symfony\Component\DependencyInjection\DefinitionDecorator;
17
use Symfony\Component\DependencyInjection\ContainerInterface;
18
use Symfony\Component\DependencyInjection\Alias;
19
use Symfony\Component\DependencyInjection\Definition;
20
use Symfony\Component\DependencyInjection\Reference;
21
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
22
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
23
use Symfony\Component\ExpressionLanguage\Expression;
24
25
/**
26
 * XmlFileLoader loads XML files service definitions.
27
 *
28
 * @author Fabien Potencier <[email protected]>
29
 */
30
class 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, $path);
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
     * @param string       $file
74
     */
75
    private function parseParameters(\DOMDocument $xml, $file)
76
    {
77
        if ($parameters = $this->getChildren($xml->documentElement, 'parameters')) {
78
            $this->container->getParameterBag()->add($this->getArgumentsAsPhp($parameters[0], 'parameter'));
79
        }
80
    }
81
82
    /**
83
     * Parses imports.
84
     *
85
     * @param \DOMDocument $xml
86
     * @param string       $file
87
     */
88
    private function parseImports(\DOMDocument $xml, $file)
89
    {
90
        $xpath = new \DOMXPath($xml);
91
        $xpath->registerNamespace('container', self::NS);
92
93
        if (false === $imports = $xpath->query('//container:imports/container:import')) {
94
            return;
95
        }
96
97
        foreach ($imports as $import) {
98
            $this->setCurrentDir(dirname($file));
99
            $this->import($import->getAttribute('resource'), null, (bool) XmlUtils::phpize($import->getAttribute('ignore-errors')), $file);
100
        }
101
    }
102
103
    /**
104
     * Parses multiple definitions.
105
     *
106
     * @param \DOMDocument $xml
107
     * @param string       $file
108
     */
109
    private function parseDefinitions(\DOMDocument $xml, $file)
110
    {
111
        $xpath = new \DOMXPath($xml);
112
        $xpath->registerNamespace('container', self::NS);
113
114
        if (false === $services = $xpath->query('//container:services/container:service')) {
115
            return;
116
        }
117
118
        foreach ($services as $service) {
119
            if (null !== $definition = $this->parseDefinition($service, $file)) {
120
                $this->container->setDefinition((string) $service->getAttribute('id'), $definition);
121
            }
122
        }
123
    }
124
125
    /**
126
     * Parses an individual Definition.
127
     *
128
     * @param \DOMElement $service
129
     * @param string      $file
130
     *
131
     * @return Definition|null
132
     */
133
    private function parseDefinition(\DOMElement $service, $file)
134
    {
135
        if ($alias = $service->getAttribute('alias')) {
136
            $public = true;
137
            if ($publicAttr = $service->getAttribute('public')) {
138
                $public = XmlUtils::phpize($publicAttr);
139
            }
140
            $this->container->setAlias((string) $service->getAttribute('id'), new Alias($alias, $public));
141
142
            return;
143
        }
144
145 View Code Duplication
        if ($parent = $service->getAttribute('parent')) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
146
            $definition = new DefinitionDecorator($parent);
147
        } else {
148
            $definition = new Definition();
149
        }
150
151
        foreach (array('class', 'scope', 'public', 'factory-class', 'factory-method', 'factory-service', 'synthetic', 'lazy', 'abstract') as $key) {
152
            if ($value = $service->getAttribute($key)) {
153
                if (in_array($key, array('factory-class', 'factory-method', 'factory-service'))) {
154
                    trigger_error(sprintf('The "%s" attribute in file "%s" is deprecated since version 2.6 and will be removed in 3.0. Use the "factory" element instead.', $key, $file), E_USER_DEPRECATED);
155
                }
156
                $method = 'set'.str_replace('-', '', $key);
157
                $definition->$method(XmlUtils::phpize($value));
158
            }
159
        }
160
161
        if ($value = $service->getAttribute('synchronized')) {
162
            $triggerDeprecation = 'request' !== (string) $service->getAttribute('id');
163
164
            if ($triggerDeprecation) {
165
                trigger_error(sprintf('The "synchronized" attribute in file "%s" is deprecated since version 2.7 and will be removed in 3.0.', $file), E_USER_DEPRECATED);
166
            }
167
168
            $definition->setSynchronized(XmlUtils::phpize($value), $triggerDeprecation);
0 ignored issues
show
Deprecated Code introduced by
The method Symfony\Component\Depend...tion::setSynchronized() has been deprecated with message: since version 2.7, will be removed in 3.0.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
169
        }
170
171
        if ($files = $this->getChildren($service, 'file')) {
172
            $definition->setFile($files[0]->nodeValue);
173
        }
174
175
        $definition->setArguments($this->getArgumentsAsPhp($service, 'argument'));
176
        $definition->setProperties($this->getArgumentsAsPhp($service, 'property'));
177
178 View Code Duplication
        if ($factories = $this->getChildren($service, 'factory')) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
179
            $factory = $factories[0];
180
            if ($function = $factory->getAttribute('function')) {
181
                $definition->setFactory($function);
182
            } else {
183
                $factoryService = $this->getChildren($factory, 'service');
184
185
                if (isset($factoryService[0])) {
186
                    $class = $this->parseDefinition($factoryService[0], $file);
187
                } elseif ($childService = $factory->getAttribute('service')) {
188
                    $class = new Reference($childService, ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, false);
189
                } else {
190
                    $class = $factory->getAttribute('class');
191
                }
192
193
                $definition->setFactory(array($class, $factory->getAttribute('method')));
194
            }
195
        }
196
197 View Code Duplication
        if ($configurators = $this->getChildren($service, 'configurator')) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
198
            $configurator = $configurators[0];
199
            if ($function = $configurator->getAttribute('function')) {
200
                $definition->setConfigurator($function);
201
            } else {
202
                $configuratorService = $this->getChildren($configurator, 'service');
203
204
                if (isset($configuratorService[0])) {
205
                    $class = $this->parseDefinition($configuratorService[0], $file);
206
                } elseif ($childService = $configurator->getAttribute('service')) {
207
                    $class = new Reference($childService, ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, false);
208
                } else {
209
                    $class = $configurator->getAttribute('class');
210
                }
211
212
                $definition->setConfigurator(array($class, $configurator->getAttribute('method')));
213
            }
214
        }
215
216
        foreach ($this->getChildren($service, 'call') as $call) {
217
            $definition->addMethodCall($call->getAttribute('method'), $this->getArgumentsAsPhp($call, 'argument'));
218
        }
219
220
        foreach ($this->getChildren($service, 'tag') as $tag) {
221
            $parameters = array();
222
            foreach ($tag->attributes as $name => $node) {
223
                if ('name' === $name) {
224
                    continue;
225
                }
226
227
                if (false !== strpos($name, '-') && false === strpos($name, '_') && !array_key_exists($normalizedName = str_replace('-', '_', $name), $parameters)) {
228
                    $parameters[$normalizedName] = XmlUtils::phpize($node->nodeValue);
229
                }
230
                // keep not normalized key for BC too
231
                $parameters[$name] = XmlUtils::phpize($node->nodeValue);
232
            }
233
234
            $definition->addTag($tag->getAttribute('name'), $parameters);
235
        }
236
237
        if ($value = $service->getAttribute('decorates')) {
238
            $renameId = $service->hasAttribute('decoration-inner-name') ? $service->getAttribute('decoration-inner-name') : null;
239
            $definition->setDecoratedService($value, $renameId);
240
        }
241
242
        return $definition;
243
    }
244
245
    /**
246
     * Parses a XML file to a \DOMDocument.
247
     *
248
     * @param string $file Path to a file
249
     *
250
     * @return \DOMDocument
251
     *
252
     * @throws InvalidArgumentException When loading of XML file returns error
253
     */
254
    private function parseFileToDOM($file)
255
    {
256
        try {
257
            $dom = XmlUtils::loadFile($file, array($this, 'validateSchema'));
258
        } catch (\InvalidArgumentException $e) {
259
            throw new InvalidArgumentException(sprintf('Unable to parse file "%s".', $file), $e->getCode(), $e);
260
        }
261
262
        $this->validateExtensions($dom, $file);
263
264
        return $dom;
265
    }
266
267
    /**
268
     * Processes anonymous services.
269
     *
270
     * @param \DOMDocument $xml
271
     * @param string       $file
272
     */
273
    private function processAnonymousServices(\DOMDocument $xml, $file)
274
    {
275
        $definitions = array();
276
        $count = 0;
277
278
        $xpath = new \DOMXPath($xml);
279
        $xpath->registerNamespace('container', self::NS);
280
281
        // anonymous services as arguments/properties
282 View Code Duplication
        if (false !== $nodes = $xpath->query('//container:argument[@type="service"][not(@id)]|//container:property[@type="service"][not(@id)]')) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
283
            foreach ($nodes as $node) {
284
                // give it a unique name
285
                $id = sprintf('%s_%d', hash('sha256', $file), ++$count);
286
                $node->setAttribute('id', $id);
287
288
                if ($services = $this->getChildren($node, 'service')) {
289
                    $definitions[$id] = array($services[0], $file, false);
290
                    $services[0]->setAttribute('id', $id);
291
                }
292
            }
293
        }
294
295
        // anonymous services "in the wild"
296 View Code Duplication
        if (false !== $nodes = $xpath->query('//container:services/container:service[not(@id)]')) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
297
            foreach ($nodes as $node) {
298
                // give it a unique name
299
                $id = sprintf('%s_%d', hash('sha256', $file), ++$count);
300
                $node->setAttribute('id', $id);
301
302
                if ($services = $this->getChildren($node, 'service')) {
303
                    $definitions[$id] = array($node, $file, true);
304
                    $services[0]->setAttribute('id', $id);
305
                }
306
            }
307
        }
308
309
        // resolve definitions
310
        krsort($definitions);
311
        foreach ($definitions as $id => $def) {
312
            list($domElement, $file, $wild) = $def;
313
314
            // anonymous services are always private
315
            // we could not use the constant false here, because of XML parsing
316
            $domElement->setAttribute('public', 'false');
317
318
            if (null !== $definition = $this->parseDefinition($domElement, $file)) {
319
                $this->container->setDefinition($id, $definition);
320
            }
321
322
            if (true === $wild) {
323
                $tmpDomElement = new \DOMElement('_services', null, self::NS);
324
                $domElement->parentNode->replaceChild($tmpDomElement, $domElement);
325
                $tmpDomElement->setAttribute('id', $id);
326
            } else {
327
                $domElement->parentNode->removeChild($domElement);
328
            }
329
        }
330
    }
331
332
    /**
333
     * Returns arguments as valid php types.
334
     *
335
     * @param \DOMElement $node
336
     * @param string      $name
337
     * @param bool        $lowercase
338
     *
339
     * @return mixed
340
     */
341
    private function getArgumentsAsPhp(\DOMElement $node, $name, $lowercase = true)
342
    {
343
        $arguments = array();
344
        foreach ($this->getChildren($node, $name) as $arg) {
345
            if ($arg->hasAttribute('name')) {
346
                $arg->setAttribute('key', $arg->getAttribute('name'));
347
            }
348
349
            if (!$arg->hasAttribute('key')) {
350
                $key = !$arguments ? 0 : max(array_keys($arguments)) + 1;
351
            } else {
352
                $key = $arg->getAttribute('key');
353
            }
354
355
            // parameter keys are case insensitive
356
            if ('parameter' == $name && $lowercase) {
357
                $key = strtolower($key);
358
            }
359
360
            // this is used by DefinitionDecorator to overwrite a specific
361
            // argument of the parent definition
362
            if ($arg->hasAttribute('index')) {
363
                $key = 'index_'.$arg->getAttribute('index');
364
            }
365
366
            switch ($arg->getAttribute('type')) {
367
                case 'service':
368
                    $onInvalid = $arg->getAttribute('on-invalid');
369
                    $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE;
370
                    if ('ignore' == $onInvalid) {
371
                        $invalidBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE;
372
                    } elseif ('null' == $onInvalid) {
373
                        $invalidBehavior = ContainerInterface::NULL_ON_INVALID_REFERENCE;
374
                    }
375
376
                    if ($strict = $arg->getAttribute('strict')) {
377
                        $strict = XmlUtils::phpize($strict);
378
                    } else {
379
                        $strict = true;
380
                    }
381
382
                    $arguments[$key] = new Reference($arg->getAttribute('id'), $invalidBehavior, $strict);
383
                    break;
384
                case 'expression':
385
                    $arguments[$key] = new Expression($arg->nodeValue);
386
                    break;
387
                case 'collection':
388
                    $arguments[$key] = $this->getArgumentsAsPhp($arg, $name, false);
389
                    break;
390
                case 'string':
391
                    $arguments[$key] = $arg->nodeValue;
392
                    break;
393
                case 'constant':
394
                    $arguments[$key] = constant($arg->nodeValue);
395
                    break;
396
                default:
397
                    $arguments[$key] = XmlUtils::phpize($arg->nodeValue);
398
            }
399
        }
400
401
        return $arguments;
402
    }
403
404
    /**
405
     * Get child elements by name.
406
     *
407
     * @param \DOMNode $node
408
     * @param mixed    $name
409
     *
410
     * @return array
411
     */
412
    private function getChildren(\DOMNode $node, $name)
413
    {
414
        $children = array();
415
        foreach ($node->childNodes as $child) {
416
            if ($child instanceof \DOMElement && $child->localName === $name && $child->namespaceURI === self::NS) {
417
                $children[] = $child;
418
            }
419
        }
420
421
        return $children;
422
    }
423
424
    /**
425
     * Validates a documents XML schema.
426
     *
427
     * @param \DOMDocument $dom
428
     *
429
     * @return bool
430
     *
431
     * @throws RuntimeException When extension references a non-existent XSD file
432
     */
433
    public function validateSchema(\DOMDocument $dom)
434
    {
435
        $schemaLocations = array('http://symfony.com/schema/dic/services' => str_replace('\\', '/', __DIR__.'/schema/dic/services/services-1.0.xsd'));
436
437
        if ($element = $dom->documentElement->getAttributeNS('http://www.w3.org/2001/XMLSchema-instance', 'schemaLocation')) {
438
            $items = preg_split('/\s+/', $element);
439
            for ($i = 0, $nb = count($items); $i < $nb; $i += 2) {
440
                if (!$this->container->hasExtension($items[$i])) {
441
                    continue;
442
                }
443
444
                if (($extension = $this->container->getExtension($items[$i])) && false !== $extension->getXsdValidationBasePath()) {
445
                    $path = str_replace($extension->getNamespace(), str_replace('\\', '/', $extension->getXsdValidationBasePath()).'/', $items[$i + 1]);
446
447
                    if (!is_file($path)) {
448
                        throw new RuntimeException(sprintf('Extension "%s" references a non-existent XSD file "%s"', get_class($extension), $path));
449
                    }
450
451
                    $schemaLocations[$items[$i]] = $path;
452
                }
453
            }
454
        }
455
456
        $tmpfiles = array();
457
        $imports = '';
458
        foreach ($schemaLocations as $namespace => $location) {
459
            $parts = explode('/', $location);
460
            if (0 === stripos($location, 'phar://')) {
461
                $tmpfile = tempnam(sys_get_temp_dir(), 'sf2');
462
                if ($tmpfile) {
463
                    copy($location, $tmpfile);
464
                    $tmpfiles[] = $tmpfile;
465
                    $parts = explode('/', str_replace('\\', '/', $tmpfile));
466
                }
467
            }
468
            $drive = '\\' === DIRECTORY_SEPARATOR ? array_shift($parts).'/' : '';
469
            $location = 'file:///'.$drive.implode('/', array_map('rawurlencode', $parts));
470
471
            $imports .= sprintf('  <xsd:import namespace="%s" schemaLocation="%s" />'."\n", $namespace, $location);
472
        }
473
474
        $source = <<<EOF
475
<?xml version="1.0" encoding="utf-8" ?>
476
<xsd:schema xmlns="http://symfony.com/schema"
477
    xmlns:xsd="http://www.w3.org/2001/XMLSchema"
478
    targetNamespace="http://symfony.com/schema"
479
    elementFormDefault="qualified">
480
481
    <xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
482
$imports
483
</xsd:schema>
484
EOF
485
        ;
486
487
        $valid = @$dom->schemaValidateSource($source);
488
489
        foreach ($tmpfiles as $tmpfile) {
490
            @unlink($tmpfile);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
491
        }
492
493
        return $valid;
494
    }
495
496
    /**
497
     * Validates an extension.
498
     *
499
     * @param \DOMDocument $dom
500
     * @param string       $file
501
     *
502
     * @throws InvalidArgumentException When no extension is found corresponding to a tag
503
     */
504
    private function validateExtensions(\DOMDocument $dom, $file)
505
    {
506
        foreach ($dom->documentElement->childNodes as $node) {
507
            if (!$node instanceof \DOMElement || 'http://symfony.com/schema/dic/services' === $node->namespaceURI) {
508
                continue;
509
            }
510
511
            // can it be handled by an extension?
512 View Code Duplication
            if (!$this->container->hasExtension($node->namespaceURI)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
513
                $extensionNamespaces = array_filter(array_map(function ($ext) { return $ext->getNamespace(); }, $this->container->getExtensions()));
514
                throw new InvalidArgumentException(sprintf(
515
                    'There is no extension able to load the configuration for "%s" (in %s). Looked for namespace "%s", found %s',
516
                    $node->tagName,
517
                    $file,
518
                    $node->namespaceURI,
519
                    $extensionNamespaces ? sprintf('"%s"', implode('", "', $extensionNamespaces)) : 'none'
520
                ));
521
            }
522
        }
523
    }
524
525
    /**
526
     * Loads from an extension.
527
     *
528
     * @param \DOMDocument $xml
529
     */
530
    private function loadFromExtensions(\DOMDocument $xml)
531
    {
532
        foreach ($xml->documentElement->childNodes as $node) {
533
            if (!$node instanceof \DOMElement || $node->namespaceURI === self::NS) {
534
                continue;
535
            }
536
537
            $values = static::convertDomElementToArray($node);
538
            if (!is_array($values)) {
539
                $values = array();
540
            }
541
542
            $this->container->loadFromExtension($node->namespaceURI, $values);
543
        }
544
    }
545
546
    /**
547
     * Converts a \DomElement object to a PHP array.
548
     *
549
     * The following rules applies during the conversion:
550
     *
551
     *  * Each tag is converted to a key value or an array
552
     *    if there is more than one "value"
553
     *
554
     *  * The content of a tag is set under a "value" key (<foo>bar</foo>)
555
     *    if the tag also has some nested tags
556
     *
557
     *  * The attributes are converted to keys (<foo foo="bar"/>)
558
     *
559
     *  * The nested-tags are converted to keys (<foo><foo>bar</foo></foo>)
560
     *
561
     * @param \DomElement $element A \DomElement instance
562
     *
563
     * @return array A PHP array
564
     */
565
    public static function convertDomElementToArray(\DomElement $element)
566
    {
567
        return XmlUtils::convertDomElementToArray($element);
568
    }
569
}
570