|
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 } |