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\Util\XmlUtils; 15use Symfony\Component\DependencyInjection\Alias; 16use Symfony\Component\DependencyInjection\Argument\BoundArgument; 17use Symfony\Component\DependencyInjection\Argument\IteratorArgument; 18use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; 19use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument; 20use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument; 21use Symfony\Component\DependencyInjection\ChildDefinition; 22use Symfony\Component\DependencyInjection\ContainerBuilder; 23use Symfony\Component\DependencyInjection\ContainerInterface; 24use Symfony\Component\DependencyInjection\Definition; 25use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; 26use Symfony\Component\DependencyInjection\Exception\RuntimeException; 27use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; 28use Symfony\Component\DependencyInjection\Reference; 29use Symfony\Component\ExpressionLanguage\Expression; 30 31/** 32 * XmlFileLoader loads XML files service definitions. 33 * 34 * @author Fabien Potencier <fabien@symfony.com> 35 */ 36class XmlFileLoader extends FileLoader 37{ 38 public const NS = 'http://symfony.com/schema/dic/services'; 39 40 protected $autoRegisterAliasesForSinglyImplementedInterfaces = false; 41 42 /** 43 * {@inheritdoc} 44 */ 45 public function load($resource, $type = null) 46 { 47 $path = $this->locator->locate($resource); 48 49 $xml = $this->parseFileToDOM($path); 50 51 $this->container->fileExists($path); 52 53 $defaults = $this->getServiceDefaults($xml, $path); 54 55 // anonymous services 56 $this->processAnonymousServices($xml, $path); 57 58 // imports 59 $this->parseImports($xml, $path); 60 61 // parameters 62 $this->parseParameters($xml, $path); 63 64 // extensions 65 $this->loadFromExtensions($xml); 66 67 // services 68 try { 69 $this->parseDefinitions($xml, $path, $defaults); 70 } finally { 71 $this->instanceof = []; 72 $this->registerAliasesForSinglyImplementedInterfaces(); 73 } 74 } 75 76 /** 77 * {@inheritdoc} 78 */ 79 public function supports($resource, $type = null) 80 { 81 if (!\is_string($resource)) { 82 return false; 83 } 84 85 if (null === $type && 'xml' === pathinfo($resource, \PATHINFO_EXTENSION)) { 86 return true; 87 } 88 89 return 'xml' === $type; 90 } 91 92 private function parseParameters(\DOMDocument $xml, string $file) 93 { 94 if ($parameters = $this->getChildren($xml->documentElement, 'parameters')) { 95 $this->container->getParameterBag()->add($this->getArgumentsAsPhp($parameters[0], 'parameter', $file)); 96 } 97 } 98 99 private function parseImports(\DOMDocument $xml, string $file) 100 { 101 $xpath = new \DOMXPath($xml); 102 $xpath->registerNamespace('container', self::NS); 103 104 if (false === $imports = $xpath->query('//container:imports/container:import')) { 105 return; 106 } 107 108 $defaultDirectory = \dirname($file); 109 foreach ($imports as $import) { 110 $this->setCurrentDir($defaultDirectory); 111 $this->import($import->getAttribute('resource'), XmlUtils::phpize($import->getAttribute('type')) ?: null, XmlUtils::phpize($import->getAttribute('ignore-errors')) ?: false, $file); 112 } 113 } 114 115 private function parseDefinitions(\DOMDocument $xml, string $file, array $defaults) 116 { 117 $xpath = new \DOMXPath($xml); 118 $xpath->registerNamespace('container', self::NS); 119 120 if (false === $services = $xpath->query('//container:services/container:service|//container:services/container:prototype')) { 121 return; 122 } 123 $this->setCurrentDir(\dirname($file)); 124 125 $this->instanceof = []; 126 $this->isLoadingInstanceof = true; 127 $instanceof = $xpath->query('//container:services/container:instanceof'); 128 foreach ($instanceof as $service) { 129 $this->setDefinition((string) $service->getAttribute('id'), $this->parseDefinition($service, $file, [])); 130 } 131 132 $this->isLoadingInstanceof = false; 133 foreach ($services as $service) { 134 if (null !== $definition = $this->parseDefinition($service, $file, $defaults)) { 135 if ('prototype' === $service->tagName) { 136 $excludes = array_column($this->getChildren($service, 'exclude'), 'nodeValue'); 137 if ($service->hasAttribute('exclude')) { 138 if (\count($excludes) > 0) { 139 throw new InvalidArgumentException('You cannot use both the attribute "exclude" and <exclude> tags at the same time.'); 140 } 141 $excludes = [$service->getAttribute('exclude')]; 142 } 143 $this->registerClasses($definition, (string) $service->getAttribute('namespace'), (string) $service->getAttribute('resource'), $excludes); 144 } else { 145 $this->setDefinition((string) $service->getAttribute('id'), $definition); 146 } 147 } 148 } 149 } 150 151 /** 152 * Get service defaults. 153 */ 154 private function getServiceDefaults(\DOMDocument $xml, string $file): array 155 { 156 $xpath = new \DOMXPath($xml); 157 $xpath->registerNamespace('container', self::NS); 158 159 if (null === $defaultsNode = $xpath->query('//container:services/container:defaults')->item(0)) { 160 return []; 161 } 162 163 $bindings = []; 164 foreach ($this->getArgumentsAsPhp($defaultsNode, 'bind', $file) as $argument => $value) { 165 $bindings[$argument] = new BoundArgument($value, true, BoundArgument::DEFAULTS_BINDING, $file); 166 } 167 168 $defaults = [ 169 'tags' => $this->getChildren($defaultsNode, 'tag'), 170 'bind' => $bindings, 171 ]; 172 173 foreach ($defaults['tags'] as $tag) { 174 if ('' === $tag->getAttribute('name')) { 175 throw new InvalidArgumentException(sprintf('The tag name for tag "<defaults>" in "%s" must be a non-empty string.', $file)); 176 } 177 } 178 179 if ($defaultsNode->hasAttribute('autowire')) { 180 $defaults['autowire'] = XmlUtils::phpize($defaultsNode->getAttribute('autowire')); 181 } 182 if ($defaultsNode->hasAttribute('public')) { 183 $defaults['public'] = XmlUtils::phpize($defaultsNode->getAttribute('public')); 184 } 185 if ($defaultsNode->hasAttribute('autoconfigure')) { 186 $defaults['autoconfigure'] = XmlUtils::phpize($defaultsNode->getAttribute('autoconfigure')); 187 } 188 189 return $defaults; 190 } 191 192 /** 193 * Parses an individual Definition. 194 */ 195 private function parseDefinition(\DOMElement $service, string $file, array $defaults): ?Definition 196 { 197 if ($alias = $service->getAttribute('alias')) { 198 $this->validateAlias($service, $file); 199 200 $this->container->setAlias((string) $service->getAttribute('id'), $alias = new Alias($alias)); 201 if ($publicAttr = $service->getAttribute('public')) { 202 $alias->setPublic(XmlUtils::phpize($publicAttr)); 203 } elseif (isset($defaults['public'])) { 204 $alias->setPublic($defaults['public']); 205 } 206 207 if ($deprecated = $this->getChildren($service, 'deprecated')) { 208 $alias->setDeprecated(true, $deprecated[0]->nodeValue ?: null); 209 } 210 211 return null; 212 } 213 214 if ($this->isLoadingInstanceof) { 215 $definition = new ChildDefinition(''); 216 } elseif ($parent = $service->getAttribute('parent')) { 217 if (!empty($this->instanceof)) { 218 throw new InvalidArgumentException(sprintf('The service "%s" cannot use the "parent" option in the same file where "instanceof" configuration is defined as using both is not supported. Move your child definitions to a separate file.', $service->getAttribute('id'))); 219 } 220 221 foreach ($defaults as $k => $v) { 222 if ('tags' === $k) { 223 // since tags are never inherited from parents, there is no confusion 224 // thus we can safely add them as defaults to ChildDefinition 225 continue; 226 } 227 if ('bind' === $k) { 228 if ($defaults['bind']) { 229 throw new InvalidArgumentException(sprintf('Bound values on service "%s" cannot be inherited from "defaults" when a "parent" is set. Move your child definitions to a separate file.', $service->getAttribute('id'))); 230 } 231 232 continue; 233 } 234 if (!$service->hasAttribute($k)) { 235 throw new InvalidArgumentException(sprintf('Attribute "%s" on service "%s" cannot be inherited from "defaults" when a "parent" is set. Move your child definitions to a separate file or define this attribute explicitly.', $k, $service->getAttribute('id'))); 236 } 237 } 238 239 $definition = new ChildDefinition($parent); 240 } else { 241 $definition = new Definition(); 242 243 if (isset($defaults['public'])) { 244 $definition->setPublic($defaults['public']); 245 } 246 if (isset($defaults['autowire'])) { 247 $definition->setAutowired($defaults['autowire']); 248 } 249 if (isset($defaults['autoconfigure'])) { 250 $definition->setAutoconfigured($defaults['autoconfigure']); 251 } 252 253 $definition->setChanges([]); 254 } 255 256 foreach (['class', 'public', 'shared', 'synthetic', 'abstract'] as $key) { 257 if ($value = $service->getAttribute($key)) { 258 $method = 'set'.$key; 259 $definition->$method($value = XmlUtils::phpize($value)); 260 } 261 } 262 263 if ($value = $service->getAttribute('lazy')) { 264 $definition->setLazy((bool) $value = XmlUtils::phpize($value)); 265 if (\is_string($value)) { 266 $definition->addTag('proxy', ['interface' => $value]); 267 } 268 } 269 270 if ($value = $service->getAttribute('autowire')) { 271 $definition->setAutowired(XmlUtils::phpize($value)); 272 } 273 274 if ($value = $service->getAttribute('autoconfigure')) { 275 if (!$definition instanceof ChildDefinition) { 276 $definition->setAutoconfigured(XmlUtils::phpize($value)); 277 } elseif ($value = XmlUtils::phpize($value)) { 278 throw new InvalidArgumentException(sprintf('The service "%s" cannot have a "parent" and also have "autoconfigure". Try setting autoconfigure="false" for the service.', $service->getAttribute('id'))); 279 } 280 } 281 282 if ($files = $this->getChildren($service, 'file')) { 283 $definition->setFile($files[0]->nodeValue); 284 } 285 286 if ($deprecated = $this->getChildren($service, 'deprecated')) { 287 $definition->setDeprecated(true, $deprecated[0]->nodeValue ?: null); 288 } 289 290 $definition->setArguments($this->getArgumentsAsPhp($service, 'argument', $file, $definition instanceof ChildDefinition)); 291 $definition->setProperties($this->getArgumentsAsPhp($service, 'property', $file)); 292 293 if ($factories = $this->getChildren($service, 'factory')) { 294 $factory = $factories[0]; 295 if ($function = $factory->getAttribute('function')) { 296 $definition->setFactory($function); 297 } else { 298 if ($childService = $factory->getAttribute('service')) { 299 $class = new Reference($childService, ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE); 300 } else { 301 $class = $factory->hasAttribute('class') ? $factory->getAttribute('class') : null; 302 } 303 304 $definition->setFactory([$class, $factory->getAttribute('method') ?: '__invoke']); 305 } 306 } 307 308 if ($configurators = $this->getChildren($service, 'configurator')) { 309 $configurator = $configurators[0]; 310 if ($function = $configurator->getAttribute('function')) { 311 $definition->setConfigurator($function); 312 } else { 313 if ($childService = $configurator->getAttribute('service')) { 314 $class = new Reference($childService, ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE); 315 } else { 316 $class = $configurator->getAttribute('class'); 317 } 318 319 $definition->setConfigurator([$class, $configurator->getAttribute('method') ?: '__invoke']); 320 } 321 } 322 323 foreach ($this->getChildren($service, 'call') as $call) { 324 $definition->addMethodCall($call->getAttribute('method'), $this->getArgumentsAsPhp($call, 'argument', $file), XmlUtils::phpize($call->getAttribute('returns-clone'))); 325 } 326 327 $tags = $this->getChildren($service, 'tag'); 328 329 if (!empty($defaults['tags'])) { 330 $tags = array_merge($tags, $defaults['tags']); 331 } 332 333 foreach ($tags as $tag) { 334 $parameters = []; 335 foreach ($tag->attributes as $name => $node) { 336 if ('name' === $name) { 337 continue; 338 } 339 340 if (false !== strpos($name, '-') && false === strpos($name, '_') && !\array_key_exists($normalizedName = str_replace('-', '_', $name), $parameters)) { 341 $parameters[$normalizedName] = XmlUtils::phpize($node->nodeValue); 342 } 343 // keep not normalized key 344 $parameters[$name] = XmlUtils::phpize($node->nodeValue); 345 } 346 347 if ('' === $tag->getAttribute('name')) { 348 throw new InvalidArgumentException(sprintf('The tag name for service "%s" in "%s" must be a non-empty string.', (string) $service->getAttribute('id'), $file)); 349 } 350 351 $definition->addTag($tag->getAttribute('name'), $parameters); 352 } 353 354 $bindings = $this->getArgumentsAsPhp($service, 'bind', $file); 355 $bindingType = $this->isLoadingInstanceof ? BoundArgument::INSTANCEOF_BINDING : BoundArgument::SERVICE_BINDING; 356 foreach ($bindings as $argument => $value) { 357 $bindings[$argument] = new BoundArgument($value, true, $bindingType, $file); 358 } 359 360 if (isset($defaults['bind'])) { 361 // deep clone, to avoid multiple process of the same instance in the passes 362 $bindings = array_merge(unserialize(serialize($defaults['bind'])), $bindings); 363 } 364 if ($bindings) { 365 $definition->setBindings($bindings); 366 } 367 368 if ($decorates = $service->getAttribute('decorates')) { 369 $decorationOnInvalid = $service->getAttribute('decoration-on-invalid') ?: 'exception'; 370 if ('exception' === $decorationOnInvalid) { 371 $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE; 372 } elseif ('ignore' === $decorationOnInvalid) { 373 $invalidBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE; 374 } elseif ('null' === $decorationOnInvalid) { 375 $invalidBehavior = ContainerInterface::NULL_ON_INVALID_REFERENCE; 376 } else { 377 throw new InvalidArgumentException(sprintf('Invalid value "%s" for attribute "decoration-on-invalid" on service "%s". Did you mean "exception", "ignore" or "null" in "%s"?', $decorationOnInvalid, (string) $service->getAttribute('id'), $file)); 378 } 379 380 $renameId = $service->hasAttribute('decoration-inner-name') ? $service->getAttribute('decoration-inner-name') : null; 381 $priority = $service->hasAttribute('decoration-priority') ? $service->getAttribute('decoration-priority') : 0; 382 383 $definition->setDecoratedService($decorates, $renameId, $priority, $invalidBehavior); 384 } 385 386 return $definition; 387 } 388 389 /** 390 * Parses an XML file to a \DOMDocument. 391 * 392 * @throws InvalidArgumentException When loading of XML file returns error 393 */ 394 private function parseFileToDOM(string $file): \DOMDocument 395 { 396 try { 397 $dom = XmlUtils::loadFile($file, [$this, 'validateSchema']); 398 } catch (\InvalidArgumentException $e) { 399 throw new InvalidArgumentException(sprintf('Unable to parse file "%s": ', $file).$e->getMessage(), $e->getCode(), $e); 400 } 401 402 $this->validateExtensions($dom, $file); 403 404 return $dom; 405 } 406 407 /** 408 * Processes anonymous services. 409 */ 410 private function processAnonymousServices(\DOMDocument $xml, string $file) 411 { 412 $definitions = []; 413 $count = 0; 414 $suffix = '~'.ContainerBuilder::hash($file); 415 416 $xpath = new \DOMXPath($xml); 417 $xpath->registerNamespace('container', self::NS); 418 419 // anonymous services as arguments/properties 420 if (false !== $nodes = $xpath->query('//container:argument[@type="service"][not(@id)]|//container:property[@type="service"][not(@id)]|//container:bind[not(@id)]|//container:factory[not(@service)]|//container:configurator[not(@service)]')) { 421 foreach ($nodes as $node) { 422 if ($services = $this->getChildren($node, 'service')) { 423 // give it a unique name 424 $id = sprintf('.%d_%s', ++$count, preg_replace('/^.*\\\\/', '', $services[0]->getAttribute('class')).$suffix); 425 $node->setAttribute('id', $id); 426 $node->setAttribute('service', $id); 427 428 $definitions[$id] = [$services[0], $file]; 429 $services[0]->setAttribute('id', $id); 430 431 // anonymous services are always private 432 // we could not use the constant false here, because of XML parsing 433 $services[0]->setAttribute('public', 'false'); 434 } 435 } 436 } 437 438 // anonymous services "in the wild" 439 if (false !== $nodes = $xpath->query('//container:services/container:service[not(@id)]')) { 440 foreach ($nodes as $node) { 441 throw new InvalidArgumentException(sprintf('Top-level services must have "id" attribute, none found in "%s" at line %d.', $file, $node->getLineNo())); 442 } 443 } 444 445 // resolve definitions 446 uksort($definitions, 'strnatcmp'); 447 foreach (array_reverse($definitions) as $id => [$domElement, $file]) { 448 if (null !== $definition = $this->parseDefinition($domElement, $file, [])) { 449 $this->setDefinition($id, $definition); 450 } 451 } 452 } 453 454 private function getArgumentsAsPhp(\DOMElement $node, string $name, string $file, bool $isChildDefinition = false): array 455 { 456 $arguments = []; 457 foreach ($this->getChildren($node, $name) as $arg) { 458 if ($arg->hasAttribute('name')) { 459 $arg->setAttribute('key', $arg->getAttribute('name')); 460 } 461 462 // this is used by ChildDefinition to overwrite a specific 463 // argument of the parent definition 464 if ($arg->hasAttribute('index')) { 465 $key = ($isChildDefinition ? 'index_' : '').$arg->getAttribute('index'); 466 } elseif (!$arg->hasAttribute('key')) { 467 // Append an empty argument, then fetch its key to overwrite it later 468 $arguments[] = null; 469 $keys = array_keys($arguments); 470 $key = array_pop($keys); 471 } else { 472 $key = $arg->getAttribute('key'); 473 } 474 475 $onInvalid = $arg->getAttribute('on-invalid'); 476 $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE; 477 if ('ignore' == $onInvalid) { 478 $invalidBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE; 479 } elseif ('ignore_uninitialized' == $onInvalid) { 480 $invalidBehavior = ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE; 481 } elseif ('null' == $onInvalid) { 482 $invalidBehavior = ContainerInterface::NULL_ON_INVALID_REFERENCE; 483 } 484 485 switch ($arg->getAttribute('type')) { 486 case 'service': 487 if ('' === $arg->getAttribute('id')) { 488 throw new InvalidArgumentException(sprintf('Tag "<%s>" with type="service" has no or empty "id" attribute in "%s".', $name, $file)); 489 } 490 491 $arguments[$key] = new Reference($arg->getAttribute('id'), $invalidBehavior); 492 break; 493 case 'expression': 494 if (!class_exists(Expression::class)) { 495 throw new \LogicException('The type="expression" attribute cannot be used without the ExpressionLanguage component. Try running "composer require symfony/expression-language".'); 496 } 497 498 $arguments[$key] = new Expression($arg->nodeValue); 499 break; 500 case 'collection': 501 $arguments[$key] = $this->getArgumentsAsPhp($arg, $name, $file); 502 break; 503 case 'iterator': 504 $arg = $this->getArgumentsAsPhp($arg, $name, $file); 505 try { 506 $arguments[$key] = new IteratorArgument($arg); 507 } catch (InvalidArgumentException $e) { 508 throw new InvalidArgumentException(sprintf('Tag "<%s>" with type="iterator" only accepts collections of type="service" references in "%s".', $name, $file)); 509 } 510 break; 511 case 'service_closure': 512 if ('' === $arg->getAttribute('id')) { 513 throw new InvalidArgumentException(sprintf('Tag "<%s>" with type="service_closure" has no or empty "id" attribute in "%s".', $name, $file)); 514 } 515 516 $arguments[$key] = new ServiceClosureArgument(new Reference($arg->getAttribute('id'), $invalidBehavior)); 517 break; 518 case 'service_locator': 519 $arg = $this->getArgumentsAsPhp($arg, $name, $file); 520 try { 521 $arguments[$key] = new ServiceLocatorArgument($arg); 522 } catch (InvalidArgumentException $e) { 523 throw new InvalidArgumentException(sprintf('Tag "<%s>" with type="service_locator" only accepts maps of type="service" references in "%s".', $name, $file)); 524 } 525 break; 526 case 'tagged': 527 case 'tagged_iterator': 528 case 'tagged_locator': 529 $type = $arg->getAttribute('type'); 530 $forLocator = 'tagged_locator' === $type; 531 532 if (!$arg->getAttribute('tag')) { 533 throw new InvalidArgumentException(sprintf('Tag "<%s>" with type="%s" has no or empty "tag" attribute in "%s".', $name, $type, $file)); 534 } 535 536 $arguments[$key] = new TaggedIteratorArgument($arg->getAttribute('tag'), $arg->getAttribute('index-by') ?: null, $arg->getAttribute('default-index-method') ?: null, $forLocator, $arg->getAttribute('default-priority-method') ?: null); 537 538 if ($forLocator) { 539 $arguments[$key] = new ServiceLocatorArgument($arguments[$key]); 540 } 541 break; 542 case 'binary': 543 if (false === $value = base64_decode($arg->nodeValue)) { 544 throw new InvalidArgumentException(sprintf('Tag "<%s>" with type="binary" is not a valid base64 encoded string.', $name)); 545 } 546 $arguments[$key] = $value; 547 break; 548 case 'string': 549 $arguments[$key] = $arg->nodeValue; 550 break; 551 case 'constant': 552 $arguments[$key] = \constant(trim($arg->nodeValue)); 553 break; 554 default: 555 $arguments[$key] = XmlUtils::phpize($arg->nodeValue); 556 } 557 } 558 559 return $arguments; 560 } 561 562 /** 563 * Get child elements by name. 564 * 565 * @return \DOMElement[] 566 */ 567 private function getChildren(\DOMNode $node, string $name): array 568 { 569 $children = []; 570 foreach ($node->childNodes as $child) { 571 if ($child instanceof \DOMElement && $child->localName === $name && self::NS === $child->namespaceURI) { 572 $children[] = $child; 573 } 574 } 575 576 return $children; 577 } 578 579 /** 580 * Validates a documents XML schema. 581 * 582 * @return bool 583 * 584 * @throws RuntimeException When extension references a non-existent XSD file 585 */ 586 public function validateSchema(\DOMDocument $dom) 587 { 588 $schemaLocations = ['http://symfony.com/schema/dic/services' => str_replace('\\', '/', __DIR__.'/schema/dic/services/services-1.0.xsd')]; 589 590 if ($element = $dom->documentElement->getAttributeNS('http://www.w3.org/2001/XMLSchema-instance', 'schemaLocation')) { 591 $items = preg_split('/\s+/', $element); 592 for ($i = 0, $nb = \count($items); $i < $nb; $i += 2) { 593 if (!$this->container->hasExtension($items[$i])) { 594 continue; 595 } 596 597 if (($extension = $this->container->getExtension($items[$i])) && false !== $extension->getXsdValidationBasePath()) { 598 $ns = $extension->getNamespace(); 599 $path = str_replace([$ns, str_replace('http://', 'https://', $ns)], str_replace('\\', '/', $extension->getXsdValidationBasePath()).'/', $items[$i + 1]); 600 601 if (!is_file($path)) { 602 throw new RuntimeException(sprintf('Extension "%s" references a non-existent XSD file "%s".', \get_class($extension), $path)); 603 } 604 605 $schemaLocations[$items[$i]] = $path; 606 } 607 } 608 } 609 610 $tmpfiles = []; 611 $imports = ''; 612 foreach ($schemaLocations as $namespace => $location) { 613 $parts = explode('/', $location); 614 $locationstart = 'file:///'; 615 if (0 === stripos($location, 'phar://')) { 616 $tmpfile = tempnam(sys_get_temp_dir(), 'symfony'); 617 if ($tmpfile) { 618 copy($location, $tmpfile); 619 $tmpfiles[] = $tmpfile; 620 $parts = explode('/', str_replace('\\', '/', $tmpfile)); 621 } else { 622 array_shift($parts); 623 $locationstart = 'phar:///'; 624 } 625 } elseif ('\\' === \DIRECTORY_SEPARATOR && 0 === strpos($location, '\\\\')) { 626 $locationstart = ''; 627 } 628 $drive = '\\' === \DIRECTORY_SEPARATOR ? array_shift($parts).'/' : ''; 629 $location = $locationstart.$drive.implode('/', array_map('rawurlencode', $parts)); 630 631 $imports .= sprintf(' <xsd:import namespace="%s" schemaLocation="%s" />'."\n", $namespace, $location); 632 } 633 634 $source = <<<EOF 635<?xml version="1.0" encoding="utf-8" ?> 636<xsd:schema xmlns="http://symfony.com/schema" 637 xmlns:xsd="http://www.w3.org/2001/XMLSchema" 638 targetNamespace="http://symfony.com/schema" 639 elementFormDefault="qualified"> 640 641 <xsd:import namespace="http://www.w3.org/XML/1998/namespace"/> 642$imports 643</xsd:schema> 644EOF 645 ; 646 647 if ($this->shouldEnableEntityLoader()) { 648 $disableEntities = libxml_disable_entity_loader(false); 649 $valid = @$dom->schemaValidateSource($source); 650 libxml_disable_entity_loader($disableEntities); 651 } else { 652 $valid = @$dom->schemaValidateSource($source); 653 } 654 foreach ($tmpfiles as $tmpfile) { 655 @unlink($tmpfile); 656 } 657 658 return $valid; 659 } 660 661 private function shouldEnableEntityLoader(): bool 662 { 663 // Version prior to 8.0 can be enabled without deprecation 664 if (\PHP_VERSION_ID < 80000) { 665 return true; 666 } 667 668 static $dom, $schema; 669 if (null === $dom) { 670 $dom = new \DOMDocument(); 671 $dom->loadXML('<?xml version="1.0"?><test/>'); 672 673 $tmpfile = tempnam(sys_get_temp_dir(), 'symfony'); 674 register_shutdown_function(static function () use ($tmpfile) { 675 @unlink($tmpfile); 676 }); 677 $schema = '<?xml version="1.0" encoding="utf-8"?> 678<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"> 679 <xsd:include schemaLocation="file:///'.str_replace('\\', '/', $tmpfile).'" /> 680</xsd:schema>'; 681 file_put_contents($tmpfile, '<?xml version="1.0" encoding="utf-8"?> 682<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"> 683 <xsd:element name="test" type="testType" /> 684 <xsd:complexType name="testType"/> 685</xsd:schema>'); 686 } 687 688 return !@$dom->schemaValidateSource($schema); 689 } 690 691 private function validateAlias(\DOMElement $alias, string $file) 692 { 693 foreach ($alias->attributes as $name => $node) { 694 if (!\in_array($name, ['alias', 'id', 'public'])) { 695 throw new InvalidArgumentException(sprintf('Invalid attribute "%s" defined for alias "%s" in "%s".', $name, $alias->getAttribute('id'), $file)); 696 } 697 } 698 699 foreach ($alias->childNodes as $child) { 700 if (!$child instanceof \DOMElement || self::NS !== $child->namespaceURI) { 701 continue; 702 } 703 if (!\in_array($child->localName, ['deprecated'], true)) { 704 throw new InvalidArgumentException(sprintf('Invalid child element "%s" defined for alias "%s" in "%s".', $child->localName, $alias->getAttribute('id'), $file)); 705 } 706 } 707 } 708 709 /** 710 * Validates an extension. 711 * 712 * @throws InvalidArgumentException When no extension is found corresponding to a tag 713 */ 714 private function validateExtensions(\DOMDocument $dom, string $file) 715 { 716 foreach ($dom->documentElement->childNodes as $node) { 717 if (!$node instanceof \DOMElement || 'http://symfony.com/schema/dic/services' === $node->namespaceURI) { 718 continue; 719 } 720 721 // can it be handled by an extension? 722 if (!$this->container->hasExtension($node->namespaceURI)) { 723 $extensionNamespaces = array_filter(array_map(function (ExtensionInterface $ext) { return $ext->getNamespace(); }, $this->container->getExtensions())); 724 throw new InvalidArgumentException(sprintf('There is no extension able to load the configuration for "%s" (in "%s"). Looked for namespace "%s", found "%s".', $node->tagName, $file, $node->namespaceURI, $extensionNamespaces ? implode('", "', $extensionNamespaces) : 'none')); 725 } 726 } 727 } 728 729 /** 730 * Loads from an extension. 731 */ 732 private function loadFromExtensions(\DOMDocument $xml) 733 { 734 foreach ($xml->documentElement->childNodes as $node) { 735 if (!$node instanceof \DOMElement || self::NS === $node->namespaceURI) { 736 continue; 737 } 738 739 $values = static::convertDomElementToArray($node); 740 if (!\is_array($values)) { 741 $values = []; 742 } 743 744 $this->container->loadFromExtension($node->namespaceURI, $values); 745 } 746 } 747 748 /** 749 * Converts a \DOMElement object to a PHP array. 750 * 751 * The following rules applies during the conversion: 752 * 753 * * Each tag is converted to a key value or an array 754 * if there is more than one "value" 755 * 756 * * The content of a tag is set under a "value" key (<foo>bar</foo>) 757 * if the tag also has some nested tags 758 * 759 * * The attributes are converted to keys (<foo foo="bar"/>) 760 * 761 * * The nested-tags are converted to keys (<foo><foo>bar</foo></foo>) 762 * 763 * @param \DOMElement $element A \DOMElement instance 764 * 765 * @return mixed 766 */ 767 public static function convertDomElementToArray(\DOMElement $element) 768 { 769 return XmlUtils::convertDomElementToArray($element); 770 } 771} 772