vendor/symfony/routing/Loader/XmlFileLoader.php line 48

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of the Symfony package.
  4.  *
  5.  * (c) Fabien Potencier <fabien@symfony.com>
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. namespace Symfony\Component\Routing\Loader;
  11. use Symfony\Component\Config\Loader\FileLoader;
  12. use Symfony\Component\Config\Resource\FileResource;
  13. use Symfony\Component\Config\Util\XmlUtils;
  14. use Symfony\Component\Routing\Loader\Configurator\Traits\HostTrait;
  15. use Symfony\Component\Routing\Loader\Configurator\Traits\LocalizedRouteTrait;
  16. use Symfony\Component\Routing\Loader\Configurator\Traits\PrefixTrait;
  17. use Symfony\Component\Routing\RouteCollection;
  18. /**
  19.  * XmlFileLoader loads XML routing files.
  20.  *
  21.  * @author Fabien Potencier <fabien@symfony.com>
  22.  * @author Tobias Schultze <http://tobion.de>
  23.  */
  24. class XmlFileLoader extends FileLoader
  25. {
  26.     use HostTrait;
  27.     use LocalizedRouteTrait;
  28.     use PrefixTrait;
  29.     public const NAMESPACE_URI 'http://symfony.com/schema/routing';
  30.     public const SCHEME_PATH '/schema/routing/routing-1.0.xsd';
  31.     /**
  32.      * Loads an XML file.
  33.      *
  34.      * @param string      $file An XML file path
  35.      * @param string|null $type The resource type
  36.      *
  37.      * @return RouteCollection
  38.      *
  39.      * @throws \InvalidArgumentException when the file cannot be loaded or when the XML cannot be
  40.      *                                   parsed because it does not validate against the scheme
  41.      */
  42.     public function load($filestring $type null)
  43.     {
  44.         $path $this->locator->locate($file);
  45.         $xml $this->loadFile($path);
  46.         $collection = new RouteCollection();
  47.         $collection->addResource(new FileResource($path));
  48.         // process routes and imports
  49.         foreach ($xml->documentElement->childNodes as $node) {
  50.             if (!$node instanceof \DOMElement) {
  51.                 continue;
  52.             }
  53.             $this->parseNode($collection$node$path$file);
  54.         }
  55.         return $collection;
  56.     }
  57.     /**
  58.      * Parses a node from a loaded XML file.
  59.      *
  60.      * @throws \InvalidArgumentException When the XML is invalid
  61.      */
  62.     protected function parseNode(RouteCollection $collection\DOMElement $nodestring $pathstring $file)
  63.     {
  64.         if (self::NAMESPACE_URI !== $node->namespaceURI) {
  65.             return;
  66.         }
  67.         switch ($node->localName) {
  68.             case 'route':
  69.                 $this->parseRoute($collection$node$path);
  70.                 break;
  71.             case 'import':
  72.                 $this->parseImport($collection$node$path$file);
  73.                 break;
  74.             case 'when':
  75.                 if (!$this->env || $node->getAttribute('env') !== $this->env) {
  76.                     break;
  77.                 }
  78.                 foreach ($node->childNodes as $node) {
  79.                     if ($node instanceof \DOMElement) {
  80.                         $this->parseNode($collection$node$path$file);
  81.                     }
  82.                 }
  83.                 break;
  84.             default:
  85.                 throw new \InvalidArgumentException(sprintf('Unknown tag "%s" used in file "%s". Expected "route" or "import".'$node->localName$path));
  86.         }
  87.     }
  88.     /**
  89.      * {@inheritdoc}
  90.      */
  91.     public function supports($resourcestring $type null)
  92.     {
  93.         return \is_string($resource) && 'xml' === pathinfo($resource\PATHINFO_EXTENSION) && (!$type || 'xml' === $type);
  94.     }
  95.     /**
  96.      * Parses a route and adds it to the RouteCollection.
  97.      *
  98.      * @throws \InvalidArgumentException When the XML is invalid
  99.      */
  100.     protected function parseRoute(RouteCollection $collection\DOMElement $nodestring $path)
  101.     {
  102.         if ('' === $id $node->getAttribute('id')) {
  103.             throw new \InvalidArgumentException(sprintf('The <route> element in file "%s" must have an "id" attribute.'$path));
  104.         }
  105.         if ('' !== $alias $node->getAttribute('alias')) {
  106.             $alias $collection->addAlias($id$alias);
  107.             if ($deprecationInfo $this->parseDeprecation($node$path)) {
  108.                 $alias->setDeprecated($deprecationInfo['package'], $deprecationInfo['version'], $deprecationInfo['message']);
  109.             }
  110.             return;
  111.         }
  112.         $schemes preg_split('/[\s,\|]++/'$node->getAttribute('schemes'), -1\PREG_SPLIT_NO_EMPTY);
  113.         $methods preg_split('/[\s,\|]++/'$node->getAttribute('methods'), -1\PREG_SPLIT_NO_EMPTY);
  114.         [$defaults$requirements$options$condition$paths/* $prefixes */$hosts] = $this->parseConfigs($node$path);
  115.         if (!$paths && '' === $node->getAttribute('path')) {
  116.             throw new \InvalidArgumentException(sprintf('The <route> element in file "%s" must have a "path" attribute or <path> child nodes.'$path));
  117.         }
  118.         if ($paths && '' !== $node->getAttribute('path')) {
  119.             throw new \InvalidArgumentException(sprintf('The <route> element in file "%s" must not have both a "path" attribute and <path> child nodes.'$path));
  120.         }
  121.         $routes $this->createLocalizedRoute($collection$id$paths ?: $node->getAttribute('path'));
  122.         $routes->addDefaults($defaults);
  123.         $routes->addRequirements($requirements);
  124.         $routes->addOptions($options);
  125.         $routes->setSchemes($schemes);
  126.         $routes->setMethods($methods);
  127.         $routes->setCondition($condition);
  128.         if (null !== $hosts) {
  129.             $this->addHost($routes$hosts);
  130.         }
  131.     }
  132.     /**
  133.      * Parses an import and adds the routes in the resource to the RouteCollection.
  134.      *
  135.      * @throws \InvalidArgumentException When the XML is invalid
  136.      */
  137.     protected function parseImport(RouteCollection $collection\DOMElement $nodestring $pathstring $file)
  138.     {
  139.         if ('' === $resource $node->getAttribute('resource')) {
  140.             throw new \InvalidArgumentException(sprintf('The <import> element in file "%s" must have a "resource" attribute.'$path));
  141.         }
  142.         $type $node->getAttribute('type');
  143.         $prefix $node->getAttribute('prefix');
  144.         $schemes $node->hasAttribute('schemes') ? preg_split('/[\s,\|]++/'$node->getAttribute('schemes'), -1\PREG_SPLIT_NO_EMPTY) : null;
  145.         $methods $node->hasAttribute('methods') ? preg_split('/[\s,\|]++/'$node->getAttribute('methods'), -1\PREG_SPLIT_NO_EMPTY) : null;
  146.         $trailingSlashOnRoot $node->hasAttribute('trailing-slash-on-root') ? XmlUtils::phpize($node->getAttribute('trailing-slash-on-root')) : true;
  147.         $namePrefix $node->getAttribute('name-prefix') ?: null;
  148.         [$defaults$requirements$options$condition/* $paths */$prefixes$hosts] = $this->parseConfigs($node$path);
  149.         if ('' !== $prefix && $prefixes) {
  150.             throw new \InvalidArgumentException(sprintf('The <route> element in file "%s" must not have both a "prefix" attribute and <prefix> child nodes.'$path));
  151.         }
  152.         $exclude = [];
  153.         foreach ($node->childNodes as $child) {
  154.             if ($child instanceof \DOMElement && $child->localName === $exclude && self::NAMESPACE_URI === $child->namespaceURI) {
  155.                 $exclude[] = $child->nodeValue;
  156.             }
  157.         }
  158.         if ($node->hasAttribute('exclude')) {
  159.             if ($exclude) {
  160.                 throw new \InvalidArgumentException('You cannot use both the attribute "exclude" and <exclude> tags at the same time.');
  161.             }
  162.             $exclude = [$node->getAttribute('exclude')];
  163.         }
  164.         $this->setCurrentDir(\dirname($path));
  165.         /** @var RouteCollection[] $imported */
  166.         $imported $this->import($resource'' !== $type $type nullfalse$file$exclude) ?: [];
  167.         if (!\is_array($imported)) {
  168.             $imported = [$imported];
  169.         }
  170.         foreach ($imported as $subCollection) {
  171.             $this->addPrefix($subCollection$prefixes ?: $prefix$trailingSlashOnRoot);
  172.             if (null !== $hosts) {
  173.                 $this->addHost($subCollection$hosts);
  174.             }
  175.             if (null !== $condition) {
  176.                 $subCollection->setCondition($condition);
  177.             }
  178.             if (null !== $schemes) {
  179.                 $subCollection->setSchemes($schemes);
  180.             }
  181.             if (null !== $methods) {
  182.                 $subCollection->setMethods($methods);
  183.             }
  184.             if (null !== $namePrefix) {
  185.                 $subCollection->addNamePrefix($namePrefix);
  186.             }
  187.             $subCollection->addDefaults($defaults);
  188.             $subCollection->addRequirements($requirements);
  189.             $subCollection->addOptions($options);
  190.             $collection->addCollection($subCollection);
  191.         }
  192.     }
  193.     /**
  194.      * @return \DOMDocument
  195.      *
  196.      * @throws \InvalidArgumentException When loading of XML file fails because of syntax errors
  197.      *                                   or when the XML structure is not as expected by the scheme -
  198.      *                                   see validate()
  199.      */
  200.     protected function loadFile(string $file)
  201.     {
  202.         return XmlUtils::loadFile($file__DIR__.static::SCHEME_PATH);
  203.     }
  204.     /**
  205.      * Parses the config elements (default, requirement, option).
  206.      *
  207.      * @throws \InvalidArgumentException When the XML is invalid
  208.      */
  209.     private function parseConfigs(\DOMElement $nodestring $path): array
  210.     {
  211.         $defaults = [];
  212.         $requirements = [];
  213.         $options = [];
  214.         $condition null;
  215.         $prefixes = [];
  216.         $paths = [];
  217.         $hosts = [];
  218.         /** @var \DOMElement $n */
  219.         foreach ($node->getElementsByTagNameNS(self::NAMESPACE_URI'*') as $n) {
  220.             if ($node !== $n->parentNode) {
  221.                 continue;
  222.             }
  223.             switch ($n->localName) {
  224.                 case 'path':
  225.                     $paths[$n->getAttribute('locale')] = trim($n->textContent);
  226.                     break;
  227.                 case 'host':
  228.                     $hosts[$n->getAttribute('locale')] = trim($n->textContent);
  229.                     break;
  230.                 case 'prefix':
  231.                     $prefixes[$n->getAttribute('locale')] = trim($n->textContent);
  232.                     break;
  233.                 case 'default':
  234.                     if ($this->isElementValueNull($n)) {
  235.                         $defaults[$n->getAttribute('key')] = null;
  236.                     } else {
  237.                         $defaults[$n->getAttribute('key')] = $this->parseDefaultsConfig($n$path);
  238.                     }
  239.                     break;
  240.                 case 'requirement':
  241.                     $requirements[$n->getAttribute('key')] = trim($n->textContent);
  242.                     break;
  243.                 case 'option':
  244.                     $options[$n->getAttribute('key')] = XmlUtils::phpize(trim($n->textContent));
  245.                     break;
  246.                 case 'condition':
  247.                     $condition trim($n->textContent);
  248.                     break;
  249.                 default:
  250.                     throw new \InvalidArgumentException(sprintf('Unknown tag "%s" used in file "%s". Expected "default", "requirement", "option" or "condition".'$n->localName$path));
  251.             }
  252.         }
  253.         if ($controller $node->getAttribute('controller')) {
  254.             if (isset($defaults['_controller'])) {
  255.                 $name $node->hasAttribute('id') ? sprintf('"%s".'$node->getAttribute('id')) : sprintf('the "%s" tag.'$node->tagName);
  256.                 throw new \InvalidArgumentException(sprintf('The routing file "%s" must not specify both the "controller" attribute and the defaults key "_controller" for '$path).$name);
  257.             }
  258.             $defaults['_controller'] = $controller;
  259.         }
  260.         if ($node->hasAttribute('locale')) {
  261.             $defaults['_locale'] = $node->getAttribute('locale');
  262.         }
  263.         if ($node->hasAttribute('format')) {
  264.             $defaults['_format'] = $node->getAttribute('format');
  265.         }
  266.         if ($node->hasAttribute('utf8')) {
  267.             $options['utf8'] = XmlUtils::phpize($node->getAttribute('utf8'));
  268.         }
  269.         if ($stateless $node->getAttribute('stateless')) {
  270.             if (isset($defaults['_stateless'])) {
  271.                 $name $node->hasAttribute('id') ? sprintf('"%s".'$node->getAttribute('id')) : sprintf('the "%s" tag.'$node->tagName);
  272.                 throw new \InvalidArgumentException(sprintf('The routing file "%s" must not specify both the "stateless" attribute and the defaults key "_stateless" for '$path).$name);
  273.             }
  274.             $defaults['_stateless'] = XmlUtils::phpize($stateless);
  275.         }
  276.         if (!$hosts) {
  277.             $hosts $node->hasAttribute('host') ? $node->getAttribute('host') : null;
  278.         }
  279.         return [$defaults$requirements$options$condition$paths$prefixes$hosts];
  280.     }
  281.     /**
  282.      * Parses the "default" elements.
  283.      *
  284.      * @return array|bool|float|int|string|null
  285.      */
  286.     private function parseDefaultsConfig(\DOMElement $elementstring $path)
  287.     {
  288.         if ($this->isElementValueNull($element)) {
  289.             return null;
  290.         }
  291.         // Check for existing element nodes in the default element. There can
  292.         // only be a single element inside a default element. So this element
  293.         // (if one was found) can safely be returned.
  294.         foreach ($element->childNodes as $child) {
  295.             if (!$child instanceof \DOMElement) {
  296.                 continue;
  297.             }
  298.             if (self::NAMESPACE_URI !== $child->namespaceURI) {
  299.                 continue;
  300.             }
  301.             return $this->parseDefaultNode($child$path);
  302.         }
  303.         // If the default element doesn't contain a nested "bool", "int", "float",
  304.         // "string", "list", or "map" element, the element contents will be treated
  305.         // as the string value of the associated default option.
  306.         return trim($element->textContent);
  307.     }
  308.     /**
  309.      * Recursively parses the value of a "default" element.
  310.      *
  311.      * @return array|bool|float|int|string|null
  312.      *
  313.      * @throws \InvalidArgumentException when the XML is invalid
  314.      */
  315.     private function parseDefaultNode(\DOMElement $nodestring $path)
  316.     {
  317.         if ($this->isElementValueNull($node)) {
  318.             return null;
  319.         }
  320.         switch ($node->localName) {
  321.             case 'bool':
  322.                 return 'true' === trim($node->nodeValue) || '1' === trim($node->nodeValue);
  323.             case 'int':
  324.                 return (int) trim($node->nodeValue);
  325.             case 'float':
  326.                 return (float) trim($node->nodeValue);
  327.             case 'string':
  328.                 return trim($node->nodeValue);
  329.             case 'list':
  330.                 $list = [];
  331.                 foreach ($node->childNodes as $element) {
  332.                     if (!$element instanceof \DOMElement) {
  333.                         continue;
  334.                     }
  335.                     if (self::NAMESPACE_URI !== $element->namespaceURI) {
  336.                         continue;
  337.                     }
  338.                     $list[] = $this->parseDefaultNode($element$path);
  339.                 }
  340.                 return $list;
  341.             case 'map':
  342.                 $map = [];
  343.                 foreach ($node->childNodes as $element) {
  344.                     if (!$element instanceof \DOMElement) {
  345.                         continue;
  346.                     }
  347.                     if (self::NAMESPACE_URI !== $element->namespaceURI) {
  348.                         continue;
  349.                     }
  350.                     $map[$element->getAttribute('key')] = $this->parseDefaultNode($element$path);
  351.                 }
  352.                 return $map;
  353.             default:
  354.                 throw new \InvalidArgumentException(sprintf('Unknown tag "%s" used in file "%s". Expected "bool", "int", "float", "string", "list", or "map".'$node->localName$path));
  355.         }
  356.     }
  357.     private function isElementValueNull(\DOMElement $element): bool
  358.     {
  359.         $namespaceUri 'http://www.w3.org/2001/XMLSchema-instance';
  360.         if (!$element->hasAttributeNS($namespaceUri'nil')) {
  361.             return false;
  362.         }
  363.         return 'true' === $element->getAttributeNS($namespaceUri'nil') || '1' === $element->getAttributeNS($namespaceUri'nil');
  364.     }
  365.     /**
  366.      * Parses the deprecation elements.
  367.      *
  368.      * @throws \InvalidArgumentException When the XML is invalid
  369.      */
  370.     private function parseDeprecation(\DOMElement $nodestring $path): array
  371.     {
  372.         $deprecatedNode null;
  373.         foreach ($node->childNodes as $child) {
  374.             if (!$child instanceof \DOMElement || self::NAMESPACE_URI !== $child->namespaceURI) {
  375.                 continue;
  376.             }
  377.             if ('deprecated' !== $child->localName) {
  378.                 throw new \InvalidArgumentException(sprintf('Invalid child element "%s" defined for alias "%s" in "%s".'$child->localName$node->getAttribute('id'), $path));
  379.             }
  380.             $deprecatedNode $child;
  381.         }
  382.         if (null === $deprecatedNode) {
  383.             return [];
  384.         }
  385.         if (!$deprecatedNode->hasAttribute('package')) {
  386.             throw new \InvalidArgumentException(sprintf('The <deprecated> element in file "%s" must have a "package" attribute.'$path));
  387.         }
  388.         if (!$deprecatedNode->hasAttribute('version')) {
  389.             throw new \InvalidArgumentException(sprintf('The <deprecated> element in file "%s" must have a "version" attribute.'$path));
  390.         }
  391.         return [
  392.             'package' => $deprecatedNode->getAttribute('package'),
  393.             'version' => $deprecatedNode->getAttribute('version'),
  394.             'message' => trim($deprecatedNode->nodeValue),
  395.         ];
  396.     }
  397. }