From 120b35c410e21397885348aaa10fc820abe64da9 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Tue, 15 Jul 2014 19:10:34 +0200 Subject: [PATCH] [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. --- src/Symfony/Component/Routing/CHANGELOG.md | 5 + .../Routing/Loader/XmlFileLoader.php | 103 +++++++++++- .../Loader/schema/routing/routing-1.0.xsd | 84 ++++++++- .../Routing/Tests/Fixtures/list_defaults.xml | 20 +++ .../Tests/Fixtures/list_in_list_defaults.xml | 22 +++ .../Tests/Fixtures/list_in_map_defaults.xml | 22 +++ .../Tests/Fixtures/list_null_values.xml | 22 +++ .../Routing/Tests/Fixtures/map_defaults.xml | 20 +++ .../Tests/Fixtures/map_in_list_defaults.xml | 22 +++ .../Tests/Fixtures/map_in_map_defaults.xml | 22 +++ .../Tests/Fixtures/map_null_values.xml | 22 +++ .../Tests/Fixtures/namespaceprefix.xml | 3 + .../Tests/Fixtures/scalar_defaults.xml | 33 ++++ .../Routing/Tests/Fixtures/validpattern.xml | 9 + .../Tests/Loader/XmlFileLoaderTest.php | 159 +++++++++++++++++- 15 files changed, 565 insertions(+), 3 deletions(-) create mode 100644 src/Symfony/Component/Routing/Tests/Fixtures/list_defaults.xml create mode 100644 src/Symfony/Component/Routing/Tests/Fixtures/list_in_list_defaults.xml create mode 100644 src/Symfony/Component/Routing/Tests/Fixtures/list_in_map_defaults.xml create mode 100644 src/Symfony/Component/Routing/Tests/Fixtures/list_null_values.xml create mode 100644 src/Symfony/Component/Routing/Tests/Fixtures/map_defaults.xml create mode 100644 src/Symfony/Component/Routing/Tests/Fixtures/map_in_list_defaults.xml create mode 100644 src/Symfony/Component/Routing/Tests/Fixtures/map_in_map_defaults.xml create mode 100644 src/Symfony/Component/Routing/Tests/Fixtures/map_null_values.xml create mode 100644 src/Symfony/Component/Routing/Tests/Fixtures/scalar_defaults.xml diff --git a/src/Symfony/Component/Routing/CHANGELOG.md b/src/Symfony/Component/Routing/CHANGELOG.md index 04ac1d3198..8c3d3e8601 100644 --- a/src/Symfony/Component/Routing/CHANGELOG.md +++ b/src/Symfony/Component/Routing/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +3.2.0 +----- + + * Added support for `boolean`, `integer`, `float`, `string`, `list` and `map` defaults. + 2.8.0 ----- diff --git a/src/Symfony/Component/Routing/Loader/XmlFileLoader.php b/src/Symfony/Component/Routing/Loader/XmlFileLoader.php index b0d7a38b7c..0ef43515e2 100644 --- a/src/Symfony/Component/Routing/Loader/XmlFileLoader.php +++ b/src/Symfony/Component/Routing/Loader/XmlFileLoader.php @@ -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'; diff --git a/src/Symfony/Component/Routing/Loader/schema/routing/routing-1.0.xsd b/src/Symfony/Component/Routing/Loader/schema/routing/routing-1.0.xsd index 8090c784c1..b98d3df919 100644 --- a/src/Symfony/Component/Routing/Loader/schema/routing/routing-1.0.xsd +++ b/src/Symfony/Component/Routing/Loader/schema/routing/routing-1.0.xsd @@ -26,7 +26,7 @@ - + @@ -54,6 +54,18 @@ + + + + + + + + + + + + @@ -61,4 +73,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/list_defaults.xml b/src/Symfony/Component/Routing/Tests/Fixtures/list_defaults.xml new file mode 100644 index 0000000000..40308945ff --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Fixtures/list_defaults.xml @@ -0,0 +1,20 @@ + + + + + + AcmeBlogBundle:Blog:index + + + + true + 1 + 3.5 + foo + + + + diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/list_in_list_defaults.xml b/src/Symfony/Component/Routing/Tests/Fixtures/list_in_list_defaults.xml new file mode 100644 index 0000000000..08c3c93d83 --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Fixtures/list_in_list_defaults.xml @@ -0,0 +1,22 @@ + + + + + + AcmeBlogBundle:Blog:index + + + + + true + 1 + 3.5 + foo + + + + + diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/list_in_map_defaults.xml b/src/Symfony/Component/Routing/Tests/Fixtures/list_in_map_defaults.xml new file mode 100644 index 0000000000..35fe31d313 --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Fixtures/list_in_map_defaults.xml @@ -0,0 +1,22 @@ + + + + + + AcmeBlogBundle:Blog:index + + + + + true + 1 + 3.5 + foo + + + + + diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/list_null_values.xml b/src/Symfony/Component/Routing/Tests/Fixtures/list_null_values.xml new file mode 100644 index 0000000000..c2930da10e --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Fixtures/list_null_values.xml @@ -0,0 +1,22 @@ + + + + + + AcmeBlogBundle:Blog:index + + + + + + + + + + + + + diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/map_defaults.xml b/src/Symfony/Component/Routing/Tests/Fixtures/map_defaults.xml new file mode 100644 index 0000000000..e6e22bd78c --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Fixtures/map_defaults.xml @@ -0,0 +1,20 @@ + + + + + + AcmeBlogBundle:Blog:index + + + + true + 1 + 3.5 + foo + + + + diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/map_in_list_defaults.xml b/src/Symfony/Component/Routing/Tests/Fixtures/map_in_list_defaults.xml new file mode 100644 index 0000000000..dec8d16afa --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Fixtures/map_in_list_defaults.xml @@ -0,0 +1,22 @@ + + + + + + AcmeBlogBundle:Blog:index + + + + + true + 1 + 3.5 + foo + + + + + diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/map_in_map_defaults.xml b/src/Symfony/Component/Routing/Tests/Fixtures/map_in_map_defaults.xml new file mode 100644 index 0000000000..5ccde07a09 --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Fixtures/map_in_map_defaults.xml @@ -0,0 +1,22 @@ + + + + + + AcmeBlogBundle:Blog:index + + + + + true + 1 + 3.5 + foo + + + + + diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/map_null_values.xml b/src/Symfony/Component/Routing/Tests/Fixtures/map_null_values.xml new file mode 100644 index 0000000000..424d67f850 --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Fixtures/map_null_values.xml @@ -0,0 +1,22 @@ + + + + + + AcmeBlogBundle:Blog:index + + + + + + + + + + + + + diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/namespaceprefix.xml b/src/Symfony/Component/Routing/Tests/Fixtures/namespaceprefix.xml index bdd6a47329..7980459009 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/namespaceprefix.xml +++ b/src/Symfony/Component/Routing/Tests/Fixtures/namespaceprefix.xml @@ -9,5 +9,8 @@ \w+ en|fr|de RouteCompiler + + 1 + diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/scalar_defaults.xml b/src/Symfony/Component/Routing/Tests/Fixtures/scalar_defaults.xml new file mode 100644 index 0000000000..b2f9c0aaab --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Fixtures/scalar_defaults.xml @@ -0,0 +1,33 @@ + + + + + + AcmeBlogBundle:Blog:index + + + + true + + + 1 + + + 3.5 + + + false + + + 1 + + + 0 + + + + + diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/validpattern.xml b/src/Symfony/Component/Routing/Tests/Fixtures/validpattern.xml index dbc72e46dd..e8d07350b7 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/validpattern.xml +++ b/src/Symfony/Component/Routing/Tests/Fixtures/validpattern.xml @@ -11,5 +11,14 @@ context.getMethod() == "GET" + + MyBundle:Blog:show + GET|POST|put|OpTiOnS + hTTps + \w+ + + context.getMethod() == "GET" + + diff --git a/src/Symfony/Component/Routing/Tests/Loader/XmlFileLoaderTest.php b/src/Symfony/Component/Routing/Tests/Loader/XmlFileLoaderTest.php index 048d3ae904..50741d3b4d 100644 --- a/src/Symfony/Component/Routing/Tests/Loader/XmlFileLoaderTest.php +++ b/src/Symfony/Component/Routing/Tests/Loader/XmlFileLoaderTest.php @@ -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') + ); + } }