feature #11394 [Routing] support for array values in route defaults (xabbuh)

This PR was merged into the 3.2-dev branch.

Discussion
----------

[Routing] support for array values in route defaults

| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets |
| License       | MIT
| Doc PR        |

As pointed out in symfony/symfony-docs#4017, the ``XmlFileLoader`` was not capable of defining array default values.

- [x] array values
- [x] integer values
- [x] float values
- [x] boolean

Commits
-------

120b35c [Routing] data type support for defaults
This commit is contained in:
Fabien Potencier 2016-06-23 15:25:46 +02:00
commit a80b45de51
15 changed files with 565 additions and 3 deletions

View File

@ -1,6 +1,11 @@
CHANGELOG
=========
3.2.0
-----
* Added support for `boolean`, `integer`, `float`, `string`, `list` and `map` defaults.
2.8.0
-----

View File

@ -202,12 +202,16 @@ class XmlFileLoader extends FileLoader
$condition = null;
foreach ($node->getElementsByTagNameNS(self::NAMESPACE_URI, '*') as $n) {
if ($node !== $n->parentNode) {
continue;
}
switch ($n->localName) {
case 'default':
if ($this->isElementValueNull($n)) {
$defaults[$n->getAttribute('key')] = null;
} else {
$defaults[$n->getAttribute('key')] = trim($n->textContent);
$defaults[$n->getAttribute('key')] = $this->parseDefaultsConfig($n, $path);
}
break;
@ -228,6 +232,103 @@ class XmlFileLoader extends FileLoader
return array($defaults, $requirements, $options, $condition);
}
/**
* Parses the "default" elements.
*
* @param \DOMElement $element The "default" element to parse
* @param string $path Full path of the XML file being processed
*
* @return array|bool|float|int|string|null The parsed value of the "default" element
*/
private function parseDefaultsConfig(\DOMElement $element, $path)
{
if ($this->isElementValueNull($element)) {
return;
}
// 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 "boolean", "integer",
// "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.
*
* @param \DOMElement $node The node value
* @param string $path Full path of the XML file being processed
*
* @return array|bool|float|int|string The parsed value
*
* @throws \InvalidArgumentException when the XML is invalid
*/
private function parseDefaultNode(\DOMElement $node, $path)
{
if ($this->isElementValueNull($node)) {
return;
}
switch ($node->localName) {
case 'boolean':
return 'true' === trim($node->nodeValue) || '1' === trim($node->nodeValue);
case 'integer':
return (int) trim($node->nodeValue);
case 'float':
return (float) trim($node->nodeValue);
case 'string':
return trim($node->nodeValue);
case 'list':
$list = array();
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':
$map = array();
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 "boolean", "integer", "float", "string", "list" or "map".', $node->localName, $path));
}
}
private function isElementValueNull(\DOMElement $element)
{
$namespaceUri = 'http://www.w3.org/2001/XMLSchema-instance';

View File

@ -26,7 +26,7 @@
<xsd:group name="configs">
<xsd:choice>
<xsd:element name="default" nillable="true" type="element" />
<xsd:element name="default" nillable="true" type="default" />
<xsd:element name="requirement" type="element" />
<xsd:element name="option" type="element" />
<xsd:element name="condition" type="xsd:string" />
@ -54,6 +54,18 @@
<xsd:attribute name="methods" type="xsd:string" />
</xsd:complexType>
<xsd:complexType name="default" mixed="true">
<xsd:choice minOccurs="0" maxOccurs="1">
<xsd:element name="boolean" type="xsd:boolean" />
<xsd:element name="integer" type="xsd:integer" />
<xsd:element name="float" type="xsd:float" />
<xsd:element name="string" type="xsd:string" />
<xsd:element name="list" type="list" />
<xsd:element name="map" type="map" />
</xsd:choice>
<xsd:attribute name="key" type="xsd:string" use="required" />
</xsd:complexType>
<xsd:complexType name="element">
<xsd:simpleContent>
<xsd:extension base="xsd:string">
@ -61,4 +73,74 @@
</xsd:extension>
</xsd:simpleContent>
</xsd:complexType>
<xsd:complexType name="list">
<xsd:choice minOccurs="0" maxOccurs="unbounded">
<xsd:element name="boolean" nillable="true" type="xsd:boolean" />
<xsd:element name="integer" nillable="true" type="xsd:integer" />
<xsd:element name="float" nillable="true" type="xsd:float" />
<xsd:element name="string" nillable="true" type="xsd:string" />
<xsd:element name="list" nillable="true" type="list" />
<xsd:element name="map" nillable="true" type="map" />
</xsd:choice>
</xsd:complexType>
<xsd:complexType name="map">
<xsd:choice minOccurs="0" maxOccurs="unbounded">
<xsd:element name="boolean" nillable="true" type="map-boolean-entry" />
<xsd:element name="integer" nillable="true" type="map-integer-entry" />
<xsd:element name="float" nillable="true" type="map-float-entry" />
<xsd:element name="string" nillable="true" type="map-string-entry" />
<xsd:element name="list" nillable="true" type="map-list-entry" />
<xsd:element name="map" nillable="true" type="map-map-entry" />
</xsd:choice>
</xsd:complexType>
<xsd:complexType name="map-boolean-entry">
<xsd:simpleContent>
<xsd:extension base="xsd:boolean">
<xsd:attribute name="key" type="xsd:string" use="required" />
</xsd:extension>
</xsd:simpleContent>
</xsd:complexType>
<xsd:complexType name="map-integer-entry">
<xsd:simpleContent>
<xsd:extension base="xsd:integer">
<xsd:attribute name="key" type="xsd:string" use="required" />
</xsd:extension>
</xsd:simpleContent>
</xsd:complexType>
<xsd:complexType name="map-float-entry">
<xsd:simpleContent>
<xsd:extension base="xsd:float">
<xsd:attribute name="key" type="xsd:string" use="required" />
</xsd:extension>
</xsd:simpleContent>
</xsd:complexType>
<xsd:complexType name="map-string-entry">
<xsd:simpleContent>
<xsd:extension base="xsd:string">
<xsd:attribute name="key" type="xsd:string" use="required" />
</xsd:extension>
</xsd:simpleContent>
</xsd:complexType>
<xsd:complexType name="map-list-entry">
<xsd:complexContent>
<xsd:extension base="list">
<xsd:attribute name="key" type="xsd:string" use="required" />
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
<xsd:complexType name="map-map-entry">
<xsd:complexContent>
<xsd:extension base="map">
<xsd:attribute name="key" type="xsd:string" use="required" />
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
</xsd:schema>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing
http://symfony.com/schema/routing/routing-1.0.xsd">
<route id="blog" path="/blog">
<default key="_controller">
<string>AcmeBlogBundle:Blog:index</string>
</default>
<default key="values">
<list>
<boolean>true</boolean>
<integer>1</integer>
<float>3.5</float>
<string>foo</string>
</list>
</default>
</route>
</routes>

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing
http://symfony.com/schema/routing/routing-1.0.xsd">
<route id="blog" path="/blog">
<default key="_controller">
<string>AcmeBlogBundle:Blog:index</string>
</default>
<default key="values">
<list>
<list>
<boolean>true</boolean>
<integer>1</integer>
<float>3.5</float>
<string>foo</string>
</list>
</list>
</default>
</route>
</routes>

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing
http://symfony.com/schema/routing/routing-1.0.xsd">
<route id="blog" path="/blog">
<default key="_controller">
<string>AcmeBlogBundle:Blog:index</string>
</default>
<default key="values">
<map>
<list key="list">
<boolean>true</boolean>
<integer>1</integer>
<float>3.5</float>
<string>foo</string>
</list>
</map>
</default>
</route>
</routes>

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing
http://symfony.com/schema/routing/routing-1.0.xsd">
<route id="blog" path="/blog">
<default key="_controller">
<string>AcmeBlogBundle:Blog:index</string>
</default>
<default key="list">
<list>
<boolean xsi:nil="true" />
<integer xsi:nil="true" />
<float xsi:nil="1" />
<string xsi:nil="true" />
<list xsi:nil="true" />
<map xsi:nil="true" />
</list>
</default>
</route>
</routes>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing
http://symfony.com/schema/routing/routing-1.0.xsd">
<route id="blog" path="/blog">
<default key="_controller">
<string>AcmeBlogBundle:Blog:index</string>
</default>
<default key="values">
<map>
<boolean key="public">true</boolean>
<integer key="page">1</integer>
<float key="price">3.5</float>
<string key="title">foo</string>
</map>
</default>
</route>
</routes>

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing
http://symfony.com/schema/routing/routing-1.0.xsd">
<route id="blog" path="/blog">
<default key="_controller">
<string>AcmeBlogBundle:Blog:index</string>
</default>
<default key="values">
<list>
<map>
<boolean key="public">true</boolean>
<integer key="page">1</integer>
<float key="price">3.5</float>
<string key="title">foo</string>
</map>
</list>
</default>
</route>
</routes>

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing
http://symfony.com/schema/routing/routing-1.0.xsd">
<route id="blog" path="/blog">
<default key="_controller">
<string>AcmeBlogBundle:Blog:index</string>
</default>
<default key="values">
<map>
<map key="map">
<boolean key="public">true</boolean>
<integer key="page">1</integer>
<float key="price">3.5</float>
<string key="title">foo</string>
</map>
</map>
</default>
</route>
</routes>

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing
http://symfony.com/schema/routing/routing-1.0.xsd">
<route id="blog" path="/blog">
<default key="_controller">
<string>AcmeBlogBundle:Blog:index</string>
</default>
<default key="map">
<map>
<boolean key="boolean" xsi:nil="true" />
<integer key="integer" xsi:nil="true" />
<float key="float" xsi:nil="true" />
<string key="string" xsi:nil="1" />
<list key="list" xsi:nil="true" />
<map key="map" xsi:nil="true" />
</map>
</default>
</route>
</routes>

View File

@ -9,5 +9,8 @@
<requirement xmlns="http://symfony.com/schema/routing" key="slug">\w+</requirement>
<r2:requirement xmlns:r2="http://symfony.com/schema/routing" key="_locale">en|fr|de</r2:requirement>
<r:option key="compiler_class">RouteCompiler</r:option>
<r:default key="page">
<r3:integer xmlns:r3="http://symfony.com/schema/routing">1</r3:integer>
</r:default>
</r:route>
</r:routes>

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing
http://symfony.com/schema/routing/routing-1.0.xsd">
<route id="blog" path="/blog">
<default key="_controller">
<string>AcmeBlogBundle:Blog:index</string>
</default>
<default key="slug" xsi:nil="true" />
<default key="published">
<boolean>true</boolean>
</default>
<default key="page">
<integer>1</integer>
</default>
<default key="price">
<float>3.5</float>
</default>
<default key="archived">
<boolean>false</boolean>
</default>
<default key="free">
<boolean>1</boolean>
</default>
<default key="locked">
<boolean>0</boolean>
</default>
<default key="foo" xsi:nil="true" />
<default key="bar" xsi:nil="1" />
</route>
</routes>

View File

@ -11,5 +11,14 @@
<condition>context.getMethod() == "GET"</condition>
</route>
<route id="blog_show_legacy" path="/blog/{slug}" host="{locale}.example.com">
<default key="_controller">MyBundle:Blog:show</default>
<requirement key="_method">GET|POST|put|OpTiOnS</requirement>
<requirement key="_scheme">hTTps</requirement>
<requirement key="locale">\w+</requirement>
<option key="compiler_class">RouteCompiler</option>
<condition>context.getMethod() == "GET"</condition>
</route>
<route id="blog_show_inherited" path="/blog/{slug}" />
</routes>

View File

@ -60,6 +60,7 @@ class XmlFileLoaderTest extends \PHPUnit_Framework_TestCase
$this->assertSame('en|fr|de', $route->getRequirement('_locale'));
$this->assertNull($route->getDefault('slug'));
$this->assertSame('RouteCompiler', $route->getOption('compiler_class'));
$this->assertSame(1, $route->getDefault('page'));
}
public function testLoadWithImport()
@ -68,7 +69,7 @@ class XmlFileLoaderTest extends \PHPUnit_Framework_TestCase
$routeCollection = $loader->load('validresource.xml');
$routes = $routeCollection->all();
$this->assertCount(2, $routes, 'Two routes are loaded');
$this->assertCount(3, $routes, 'Two routes are loaded');
$this->assertContainsOnly('Symfony\Component\Routing\Route', $routes);
foreach ($routes as $route) {
@ -129,4 +130,160 @@ class XmlFileLoaderTest extends \PHPUnit_Framework_TestCase
$this->assertEquals('foo', $route->getDefault('foobar'));
$this->assertEquals('bar', $route->getDefault('baz'));
}
public function testScalarDataTypeDefaults()
{
$loader = new XmlFileLoader(new FileLocator(array(__DIR__.'/../Fixtures')));
$routeCollection = $loader->load('scalar_defaults.xml');
$route = $routeCollection->get('blog');
$this->assertSame(
array(
'_controller' => 'AcmeBlogBundle:Blog:index',
'slug' => null,
'published' => true,
'page' => 1,
'price' => 3.5,
'archived' => false,
'free' => true,
'locked' => false,
'foo' => null,
'bar' => null,
),
$route->getDefaults()
);
}
public function testListDefaults()
{
$loader = new XmlFileLoader(new FileLocator(array(__DIR__.'/../Fixtures')));
$routeCollection = $loader->load('list_defaults.xml');
$route = $routeCollection->get('blog');
$this->assertSame(
array(
'_controller' => 'AcmeBlogBundle:Blog:index',
'values' => array(true, 1, 3.5, 'foo'),
),
$route->getDefaults()
);
}
public function testListInListDefaults()
{
$loader = new XmlFileLoader(new FileLocator(array(__DIR__.'/../Fixtures')));
$routeCollection = $loader->load('list_in_list_defaults.xml');
$route = $routeCollection->get('blog');
$this->assertSame(
array(
'_controller' => 'AcmeBlogBundle:Blog:index',
'values' => array(array(true, 1, 3.5, 'foo')),
),
$route->getDefaults()
);
}
public function testListInMapDefaults()
{
$loader = new XmlFileLoader(new FileLocator(array(__DIR__.'/../Fixtures')));
$routeCollection = $loader->load('list_in_map_defaults.xml');
$route = $routeCollection->get('blog');
$this->assertSame(
array(
'_controller' => 'AcmeBlogBundle:Blog:index',
'values' => array('list' => array(true, 1, 3.5, 'foo')),
),
$route->getDefaults()
);
}
public function testMapDefaults()
{
$loader = new XmlFileLoader(new FileLocator(array(__DIR__.'/../Fixtures')));
$routeCollection = $loader->load('map_defaults.xml');
$route = $routeCollection->get('blog');
$this->assertSame(
array(
'_controller' => 'AcmeBlogBundle:Blog:index',
'values' => array(
'public' => true,
'page' => 1,
'price' => 3.5,
'title' => 'foo',
),
),
$route->getDefaults()
);
}
public function testMapInListDefaults()
{
$loader = new XmlFileLoader(new FileLocator(array(__DIR__.'/../Fixtures')));
$routeCollection = $loader->load('map_in_list_defaults.xml');
$route = $routeCollection->get('blog');
$this->assertSame(
array(
'_controller' => 'AcmeBlogBundle:Blog:index',
'values' => array(array(
'public' => true,
'page' => 1,
'price' => 3.5,
'title' => 'foo',
)),
),
$route->getDefaults()
);
}
public function testMapInMapDefaults()
{
$loader = new XmlFileLoader(new FileLocator(array(__DIR__.'/../Fixtures')));
$routeCollection = $loader->load('map_in_map_defaults.xml');
$route = $routeCollection->get('blog');
$this->assertSame(
array(
'_controller' => 'AcmeBlogBundle:Blog:index',
'values' => array('map' => array(
'public' => true,
'page' => 1,
'price' => 3.5,
'title' => 'foo',
)),
),
$route->getDefaults()
);
}
public function testNullValuesInList()
{
$loader = new XmlFileLoader(new FileLocator(array(__DIR__.'/../Fixtures')));
$routeCollection = $loader->load('list_null_values.xml');
$route = $routeCollection->get('blog');
$this->assertSame(array(null, null, null, null, null, null), $route->getDefault('list'));
}
public function testNullValuesInMap()
{
$loader = new XmlFileLoader(new FileLocator(array(__DIR__.'/../Fixtures')));
$routeCollection = $loader->load('map_null_values.xml');
$route = $routeCollection->get('blog');
$this->assertSame(
array(
'boolean' => null,
'integer' => null,
'float' => null,
'string' => null,
'list' => null,
'map' => null,
),
$route->getDefault('map')
);
}
}