2018-02-01 20:01:12 +00:00
< ? php
/*
* This file is part of the Symfony package .
*
* ( c ) Fabien Potencier < fabien @ symfony . com >
*
* For the full copyright and license information , please view the LICENSE
* file that was distributed with this source code .
*/
namespace Symfony\Component\Routing\Loader ;
use Symfony\Component\Config\Loader\FileLoader ;
2018-10-14 19:50:32 +00:00
use Symfony\Component\Config\Resource\FileResource ;
2018-02-01 20:01:12 +00:00
use Symfony\Component\Config\Util\XmlUtils ;
2022-03-10 11:54:29 +00:00
use Symfony\Component\Routing\Loader\Configurator\Traits\HostTrait ;
use Symfony\Component\Routing\Loader\Configurator\Traits\LocalizedRouteTrait ;
use Symfony\Component\Routing\Loader\Configurator\Traits\PrefixTrait ;
2018-10-14 19:50:32 +00:00
use Symfony\Component\Routing\RouteCollection ;
2018-02-01 20:01:12 +00:00
/**
* XmlFileLoader loads XML routing files .
*
* @ author Fabien Potencier < fabien @ symfony . com >
* @ author Tobias Schultze < http :// tobion . de >
*/
class XmlFileLoader extends FileLoader
{
2022-03-10 11:54:29 +00:00
use HostTrait ;
use LocalizedRouteTrait ;
use PrefixTrait ;
public const NAMESPACE_URI = 'http://symfony.com/schema/routing' ;
public const SCHEME_PATH = '/schema/routing/routing-1.0.xsd' ;
2018-02-01 20:01:12 +00:00
/**
* Loads an XML file .
*
* @ param string $file An XML file path
* @ param string | null $type The resource type
*
2022-03-10 11:54:29 +00:00
* @ return RouteCollection
2018-02-01 20:01:12 +00:00
*
* @ throws \InvalidArgumentException when the file cannot be loaded or when the XML cannot be
* parsed because it does not validate against the scheme
*/
2022-03-10 11:54:29 +00:00
public function load ( $file , string $type = null )
2018-02-01 20:01:12 +00:00
{
$path = $this -> locator -> locate ( $file );
$xml = $this -> loadFile ( $path );
$collection = new RouteCollection ();
$collection -> addResource ( new FileResource ( $path ));
// process routes and imports
foreach ( $xml -> documentElement -> childNodes as $node ) {
if ( ! $node instanceof \DOMElement ) {
continue ;
}
$this -> parseNode ( $collection , $node , $path , $file );
}
return $collection ;
}
/**
* Parses a node from a loaded XML file .
*
* @ throws \InvalidArgumentException When the XML is invalid
*/
2022-03-10 11:54:29 +00:00
protected function parseNode ( RouteCollection $collection , \DOMElement $node , string $path , string $file )
2018-02-01 20:01:12 +00:00
{
if ( self :: NAMESPACE_URI !== $node -> namespaceURI ) {
return ;
}
switch ( $node -> localName ) {
case 'route' :
$this -> parseRoute ( $collection , $node , $path );
break ;
case 'import' :
$this -> parseImport ( $collection , $node , $path , $file );
break ;
2022-03-10 11:54:29 +00:00
case 'when' :
if ( ! $this -> env || $node -> getAttribute ( 'env' ) !== $this -> env ) {
break ;
}
foreach ( $node -> childNodes as $node ) {
if ( $node instanceof \DOMElement ) {
$this -> parseNode ( $collection , $node , $path , $file );
}
}
break ;
2018-02-01 20:01:12 +00:00
default :
throw new \InvalidArgumentException ( sprintf ( 'Unknown tag "%s" used in file "%s". Expected "route" or "import".' , $node -> localName , $path ));
}
}
/**
* { @ inheritdoc }
*/
2022-03-10 11:54:29 +00:00
public function supports ( $resource , string $type = null )
2018-02-01 20:01:12 +00:00
{
2022-03-10 11:54:29 +00:00
return \is_string ( $resource ) && 'xml' === pathinfo ( $resource , \PATHINFO_EXTENSION ) && ( ! $type || 'xml' === $type );
2018-02-01 20:01:12 +00:00
}
/**
* Parses a route and adds it to the RouteCollection .
*
* @ throws \InvalidArgumentException When the XML is invalid
*/
2022-03-10 11:54:29 +00:00
protected function parseRoute ( RouteCollection $collection , \DOMElement $node , string $path )
2018-02-01 20:01:12 +00:00
{
2018-10-14 19:50:32 +00:00
if ( '' === $id = $node -> getAttribute ( 'id' )) {
throw new \InvalidArgumentException ( sprintf ( 'The <route> element in file "%s" must have an "id" attribute.' , $path ));
2018-02-01 20:01:12 +00:00
}
2022-03-10 11:54:29 +00:00
if ( '' !== $alias = $node -> getAttribute ( 'alias' )) {
$alias = $collection -> addAlias ( $id , $alias );
if ( $deprecationInfo = $this -> parseDeprecation ( $node , $path )) {
$alias -> setDeprecated ( $deprecationInfo [ 'package' ], $deprecationInfo [ 'version' ], $deprecationInfo [ 'message' ]);
}
return ;
}
$schemes = preg_split ( '/[\s,\|]++/' , $node -> getAttribute ( 'schemes' ), - 1 , \PREG_SPLIT_NO_EMPTY );
$methods = preg_split ( '/[\s,\|]++/' , $node -> getAttribute ( 'methods' ), - 1 , \PREG_SPLIT_NO_EMPTY );
2018-02-01 20:01:12 +00:00
2022-03-10 11:54:29 +00:00
[ $defaults , $requirements , $options , $condition , $paths , /* $prefixes */ , $hosts ] = $this -> parseConfigs ( $node , $path );
2018-02-01 20:01:12 +00:00
2018-10-14 19:50:32 +00:00
if ( ! $paths && '' === $node -> getAttribute ( 'path' )) {
throw new \InvalidArgumentException ( sprintf ( 'The <route> element in file "%s" must have a "path" attribute or <path> child nodes.' , $path ));
}
if ( $paths && '' !== $node -> getAttribute ( 'path' )) {
throw new \InvalidArgumentException ( sprintf ( 'The <route> element in file "%s" must not have both a "path" attribute and <path> child nodes.' , $path ));
}
2022-03-10 11:54:29 +00:00
$routes = $this -> createLocalizedRoute ( $collection , $id , $paths ? : $node -> getAttribute ( 'path' ));
$routes -> addDefaults ( $defaults );
$routes -> addRequirements ( $requirements );
$routes -> addOptions ( $options );
$routes -> setSchemes ( $schemes );
$routes -> setMethods ( $methods );
$routes -> setCondition ( $condition );
if ( null !== $hosts ) {
$this -> addHost ( $routes , $hosts );
2018-10-14 19:50:32 +00:00
}
2018-02-01 20:01:12 +00:00
}
/**
* Parses an import and adds the routes in the resource to the RouteCollection .
*
* @ throws \InvalidArgumentException When the XML is invalid
*/
2022-03-10 11:54:29 +00:00
protected function parseImport ( RouteCollection $collection , \DOMElement $node , string $path , string $file )
2018-02-01 20:01:12 +00:00
{
if ( '' === $resource = $node -> getAttribute ( 'resource' )) {
throw new \InvalidArgumentException ( sprintf ( 'The <import> element in file "%s" must have a "resource" attribute.' , $path ));
}
$type = $node -> getAttribute ( 'type' );
$prefix = $node -> getAttribute ( 'prefix' );
2022-03-10 11:54:29 +00:00
$schemes = $node -> hasAttribute ( 'schemes' ) ? preg_split ( '/[\s,\|]++/' , $node -> getAttribute ( 'schemes' ), - 1 , \PREG_SPLIT_NO_EMPTY ) : null ;
$methods = $node -> hasAttribute ( 'methods' ) ? preg_split ( '/[\s,\|]++/' , $node -> getAttribute ( 'methods' ), - 1 , \PREG_SPLIT_NO_EMPTY ) : null ;
2018-10-14 19:50:32 +00:00
$trailingSlashOnRoot = $node -> hasAttribute ( 'trailing-slash-on-root' ) ? XmlUtils :: phpize ( $node -> getAttribute ( 'trailing-slash-on-root' )) : true ;
2022-03-10 11:54:29 +00:00
$namePrefix = $node -> getAttribute ( 'name-prefix' ) ? : null ;
2018-02-01 20:01:12 +00:00
2022-03-10 11:54:29 +00:00
[ $defaults , $requirements , $options , $condition , /* $paths */ , $prefixes , $hosts ] = $this -> parseConfigs ( $node , $path );
2018-02-01 20:01:12 +00:00
2018-10-14 19:50:32 +00:00
if ( '' !== $prefix && $prefixes ) {
throw new \InvalidArgumentException ( sprintf ( 'The <route> element in file "%s" must not have both a "prefix" attribute and <prefix> child nodes.' , $path ));
}
2022-03-10 11:54:29 +00:00
$exclude = [];
foreach ( $node -> childNodes as $child ) {
if ( $child instanceof \DOMElement && $child -> localName === $exclude && self :: NAMESPACE_URI === $child -> namespaceURI ) {
$exclude [] = $child -> nodeValue ;
}
}
if ( $node -> hasAttribute ( 'exclude' )) {
if ( $exclude ) {
throw new \InvalidArgumentException ( 'You cannot use both the attribute "exclude" and <exclude> tags at the same time.' );
}
$exclude = [ $node -> getAttribute ( 'exclude' )];
}
2018-10-14 19:50:32 +00:00
$this -> setCurrentDir ( \dirname ( $path ));
2018-02-01 20:01:12 +00:00
2019-06-11 11:29:32 +00:00
/** @var RouteCollection[] $imported */
2022-03-10 11:54:29 +00:00
$imported = $this -> import ( $resource , ( '' !== $type ? $type : null ), false , $file , $exclude ) ? : [];
2018-06-13 18:35:28 +00:00
2018-10-14 19:50:32 +00:00
if ( ! \is_array ( $imported )) {
2019-06-11 11:29:32 +00:00
$imported = [ $imported ];
2018-02-01 20:01:12 +00:00
}
2018-06-13 18:35:28 +00:00
foreach ( $imported as $subCollection ) {
2022-03-10 11:54:29 +00:00
$this -> addPrefix ( $subCollection , $prefixes ? : $prefix , $trailingSlashOnRoot );
2018-10-14 19:50:32 +00:00
2022-03-10 11:54:29 +00:00
if ( null !== $hosts ) {
$this -> addHost ( $subCollection , $hosts );
2018-06-13 18:35:28 +00:00
}
2022-03-10 11:54:29 +00:00
2018-06-13 18:35:28 +00:00
if ( null !== $condition ) {
$subCollection -> setCondition ( $condition );
}
if ( null !== $schemes ) {
$subCollection -> setSchemes ( $schemes );
}
if ( null !== $methods ) {
$subCollection -> setMethods ( $methods );
}
2022-03-10 11:54:29 +00:00
if ( null !== $namePrefix ) {
$subCollection -> addNamePrefix ( $namePrefix );
}
2018-06-13 18:35:28 +00:00
$subCollection -> addDefaults ( $defaults );
$subCollection -> addRequirements ( $requirements );
$subCollection -> addOptions ( $options );
$collection -> addCollection ( $subCollection );
}
2018-02-01 20:01:12 +00:00
}
/**
* @ return \DOMDocument
*
* @ throws \InvalidArgumentException When loading of XML file fails because of syntax errors
* or when the XML structure is not as expected by the scheme -
* see validate ()
*/
2022-03-10 11:54:29 +00:00
protected function loadFile ( string $file )
2018-02-01 20:01:12 +00:00
{
return XmlUtils :: loadFile ( $file , __DIR__ . static :: SCHEME_PATH );
}
/**
* Parses the config elements ( default , requirement , option ) .
*
* @ throws \InvalidArgumentException When the XML is invalid
*/
2022-03-10 11:54:29 +00:00
private function parseConfigs ( \DOMElement $node , string $path ) : array
2018-02-01 20:01:12 +00:00
{
2019-06-11 11:29:32 +00:00
$defaults = [];
$requirements = [];
$options = [];
2018-02-01 20:01:12 +00:00
$condition = null ;
2019-06-11 11:29:32 +00:00
$prefixes = [];
$paths = [];
2022-03-10 11:54:29 +00:00
$hosts = [];
2018-02-01 20:01:12 +00:00
2019-06-11 11:29:32 +00:00
/** @var \DOMElement $n */
2018-02-01 20:01:12 +00:00
foreach ( $node -> getElementsByTagNameNS ( self :: NAMESPACE_URI , '*' ) as $n ) {
if ( $node !== $n -> parentNode ) {
continue ;
}
switch ( $n -> localName ) {
2018-10-14 19:50:32 +00:00
case 'path' :
$paths [ $n -> getAttribute ( 'locale' )] = trim ( $n -> textContent );
break ;
2022-03-10 11:54:29 +00:00
case 'host' :
$hosts [ $n -> getAttribute ( 'locale' )] = trim ( $n -> textContent );
break ;
2018-10-14 19:50:32 +00:00
case 'prefix' :
$prefixes [ $n -> getAttribute ( 'locale' )] = trim ( $n -> textContent );
break ;
2018-02-01 20:01:12 +00:00
case 'default' :
if ( $this -> isElementValueNull ( $n )) {
$defaults [ $n -> getAttribute ( 'key' )] = null ;
} else {
$defaults [ $n -> getAttribute ( 'key' )] = $this -> parseDefaultsConfig ( $n , $path );
}
break ;
case 'requirement' :
$requirements [ $n -> getAttribute ( 'key' )] = trim ( $n -> textContent );
break ;
case 'option' :
2019-06-11 11:29:32 +00:00
$options [ $n -> getAttribute ( 'key' )] = XmlUtils :: phpize ( trim ( $n -> textContent ));
2018-02-01 20:01:12 +00:00
break ;
case 'condition' :
$condition = trim ( $n -> textContent );
break ;
default :
throw new \InvalidArgumentException ( sprintf ( 'Unknown tag "%s" used in file "%s". Expected "default", "requirement", "option" or "condition".' , $n -> localName , $path ));
}
}
if ( $controller = $node -> getAttribute ( 'controller' )) {
if ( isset ( $defaults [ '_controller' ])) {
2022-03-10 11:54:29 +00:00
$name = $node -> hasAttribute ( 'id' ) ? sprintf ( '"%s".' , $node -> getAttribute ( 'id' )) : sprintf ( 'the "%s" tag.' , $node -> tagName );
2018-02-01 20:01:12 +00:00
2022-03-10 11:54:29 +00:00
throw new \InvalidArgumentException ( sprintf ( 'The routing file "%s" must not specify both the "controller" attribute and the defaults key "_controller" for ' , $path ) . $name );
2018-02-01 20:01:12 +00:00
}
$defaults [ '_controller' ] = $controller ;
}
2019-06-11 11:29:32 +00:00
if ( $node -> hasAttribute ( 'locale' )) {
$defaults [ '_locale' ] = $node -> getAttribute ( 'locale' );
}
if ( $node -> hasAttribute ( 'format' )) {
$defaults [ '_format' ] = $node -> getAttribute ( 'format' );
}
if ( $node -> hasAttribute ( 'utf8' )) {
$options [ 'utf8' ] = XmlUtils :: phpize ( $node -> getAttribute ( 'utf8' ));
}
2022-03-10 11:54:29 +00:00
if ( $stateless = $node -> getAttribute ( 'stateless' )) {
if ( isset ( $defaults [ '_stateless' ])) {
$name = $node -> hasAttribute ( 'id' ) ? sprintf ( '"%s".' , $node -> getAttribute ( 'id' )) : sprintf ( 'the "%s" tag.' , $node -> tagName );
throw new \InvalidArgumentException ( sprintf ( 'The routing file "%s" must not specify both the "stateless" attribute and the defaults key "_stateless" for ' , $path ) . $name );
}
$defaults [ '_stateless' ] = XmlUtils :: phpize ( $stateless );
}
2018-02-01 20:01:12 +00:00
2022-03-10 11:54:29 +00:00
if ( ! $hosts ) {
$hosts = $node -> hasAttribute ( 'host' ) ? $node -> getAttribute ( 'host' ) : null ;
}
return [ $defaults , $requirements , $options , $condition , $paths , $prefixes , $hosts ];
2018-02-01 20:01:12 +00:00
}
/**
* Parses the " default " elements .
*
2022-03-10 11:54:29 +00:00
* @ return array | bool | float | int | string | null
2018-02-01 20:01:12 +00:00
*/
2022-03-10 11:54:29 +00:00
private function parseDefaultsConfig ( \DOMElement $element , string $path )
2018-02-01 20:01:12 +00:00
{
if ( $this -> isElementValueNull ( $element )) {
2022-03-10 11:54:29 +00:00
return null ;
2018-02-01 20:01:12 +00:00
}
// Check for existing element nodes in the default element. There can
// only be a single element inside a default element. So this element
// (if one was found) can safely be returned.
foreach ( $element -> childNodes as $child ) {
if ( ! $child instanceof \DOMElement ) {
continue ;
}
if ( self :: NAMESPACE_URI !== $child -> namespaceURI ) {
continue ;
}
return $this -> parseDefaultNode ( $child , $path );
}
// If the default element doesn't contain a nested "bool", "int", "float",
// "string", "list", or "map" element, the element contents will be treated
// as the string value of the associated default option.
return trim ( $element -> textContent );
}
/**
* Recursively parses the value of a " default " element .
*
2022-03-10 11:54:29 +00:00
* @ return array | bool | float | int | string | null
2018-02-01 20:01:12 +00:00
*
* @ throws \InvalidArgumentException when the XML is invalid
*/
2022-03-10 11:54:29 +00:00
private function parseDefaultNode ( \DOMElement $node , string $path )
2018-02-01 20:01:12 +00:00
{
if ( $this -> isElementValueNull ( $node )) {
2022-03-10 11:54:29 +00:00
return null ;
2018-02-01 20:01:12 +00:00
}
switch ( $node -> localName ) {
case 'bool' :
return 'true' === trim ( $node -> nodeValue ) || '1' === trim ( $node -> nodeValue );
case 'int' :
return ( int ) trim ( $node -> nodeValue );
case 'float' :
return ( float ) trim ( $node -> nodeValue );
case 'string' :
return trim ( $node -> nodeValue );
case 'list' :
2019-06-11 11:29:32 +00:00
$list = [];
2018-02-01 20:01:12 +00:00
foreach ( $node -> childNodes as $element ) {
if ( ! $element instanceof \DOMElement ) {
continue ;
}
if ( self :: NAMESPACE_URI !== $element -> namespaceURI ) {
continue ;
}
$list [] = $this -> parseDefaultNode ( $element , $path );
}
return $list ;
case 'map' :
2019-06-11 11:29:32 +00:00
$map = [];
2018-02-01 20:01:12 +00:00
foreach ( $node -> childNodes as $element ) {
if ( ! $element instanceof \DOMElement ) {
continue ;
}
if ( self :: NAMESPACE_URI !== $element -> namespaceURI ) {
continue ;
}
$map [ $element -> getAttribute ( 'key' )] = $this -> parseDefaultNode ( $element , $path );
}
return $map ;
default :
throw new \InvalidArgumentException ( sprintf ( 'Unknown tag "%s" used in file "%s". Expected "bool", "int", "float", "string", "list", or "map".' , $node -> localName , $path ));
}
}
2022-03-10 11:54:29 +00:00
private function isElementValueNull ( \DOMElement $element ) : bool
2018-02-01 20:01:12 +00:00
{
$namespaceUri = 'http://www.w3.org/2001/XMLSchema-instance' ;
if ( ! $element -> hasAttributeNS ( $namespaceUri , 'nil' )) {
return false ;
}
return 'true' === $element -> getAttributeNS ( $namespaceUri , 'nil' ) || '1' === $element -> getAttributeNS ( $namespaceUri , 'nil' );
}
2022-03-10 11:54:29 +00:00
/**
* Parses the deprecation elements .
*
* @ throws \InvalidArgumentException When the XML is invalid
*/
private function parseDeprecation ( \DOMElement $node , string $path ) : array
{
$deprecatedNode = null ;
foreach ( $node -> childNodes as $child ) {
if ( ! $child instanceof \DOMElement || self :: NAMESPACE_URI !== $child -> namespaceURI ) {
continue ;
}
if ( 'deprecated' !== $child -> localName ) {
throw new \InvalidArgumentException ( sprintf ( 'Invalid child element "%s" defined for alias "%s" in "%s".' , $child -> localName , $node -> getAttribute ( 'id' ), $path ));
}
$deprecatedNode = $child ;
}
if ( null === $deprecatedNode ) {
return [];
}
if ( ! $deprecatedNode -> hasAttribute ( 'package' )) {
throw new \InvalidArgumentException ( sprintf ( 'The <deprecated> element in file "%s" must have a "package" attribute.' , $path ));
}
if ( ! $deprecatedNode -> hasAttribute ( 'version' )) {
throw new \InvalidArgumentException ( sprintf ( 'The <deprecated> element in file "%s" must have a "version" attribute.' , $path ));
}
return [
'package' => $deprecatedNode -> getAttribute ( 'package' ),
'version' => $deprecatedNode -> getAttribute ( 'version' ),
'message' => trim ( $deprecatedNode -> nodeValue ),
];
}
2018-02-01 20:01:12 +00:00
}