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