[Routing] data type support for defaults

As pointed out in symfony/symfony-docs#4017, the XmlFileLoader was not
capable of defining array default values. Additionally, this commit
adds support for handling associative arrays, boolean, integer, float
and string data types.
This commit is contained in:
Christian Flothmann 2014-07-15 19:10:34 +02:00
parent 9af416d096
commit 120b35c410
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')
);
}
}