From 10183ded6844626eadf6fae6c53e307b7450aa4d Mon Sep 17 00:00:00 2001 From: Tobias Schultze Date: Tue, 13 Nov 2012 23:39:53 +0100 Subject: [PATCH 1/4] make scheme and method requirements first-class citizen in Route --- src/Symfony/Component/Routing/Route.php | 121 +++++++++++++++++- .../Component/Routing/Tests/RouteTest.php | 52 ++++++++ 2 files changed, 166 insertions(+), 7 deletions(-) diff --git a/src/Symfony/Component/Routing/Route.php b/src/Symfony/Component/Routing/Route.php index 281dedff13182..4f49352fa05b4 100644 --- a/src/Symfony/Component/Routing/Route.php +++ b/src/Symfony/Component/Routing/Route.php @@ -15,6 +15,7 @@ * A Route describes a route and its parameters. * * @author Fabien Potencier + * @author Tobias Schultze * * @api */ @@ -30,7 +31,17 @@ class Route implements \Serializable */ private $hostnamePattern = ''; - /** + /** + * @var array + */ + private $schemes = array(); + + /** + * @var array + */ + private $methods = array(); + + /** * @var array */ private $defaults = array(); @@ -59,21 +70,32 @@ class Route implements \Serializable * * * compiler_class: A class name able to compile this route instance (RouteCompiler by default) * - * @param string $pattern The path pattern to match - * @param array $defaults An array of default parameter values - * @param array $requirements An array of requirements for parameters (regexes) - * @param array $options An array of options - * @param string $hostnamePattern The hostname pattern to match + * @param string $pattern The path pattern to match + * @param array $defaults An array of default parameter values + * @param array $requirements An array of requirements for parameters (regexes) + * @param array $options An array of options + * @param string $hostnamePattern The hostname pattern to match + * @param string|array $schemes A required URI scheme or an array of restricted schemes + * @param string|array $methods A required HTTP method or an array of restricted methods * * @api */ - public function __construct($pattern, array $defaults = array(), array $requirements = array(), array $options = array(), $hostnamePattern = '') + public function __construct($pattern, array $defaults = array(), array $requirements = array(), array $options = array(), + $hostnamePattern = '', $schemes = array(), $methods = array()) { $this->setPattern($pattern); $this->setDefaults($defaults); $this->setRequirements($requirements); $this->setOptions($options); $this->setHostnamePattern($hostnamePattern); + // The conditions make sure that an initial empty $schemes/$methods does not override the corresponding requirement. + // They can be removed when the BC layer is removed. + if ($schemes) { + $this->setSchemes($schemes); + } + if ($methods) { + $this->setMethods($methods); + } } public function serialize() @@ -84,6 +106,8 @@ public function serialize() 'defaults' => $this->defaults, 'requirements' => $this->requirements, 'options' => $this->options, + 'schemes' => $this->schemes, + 'methods' => $this->methods, )); } @@ -95,6 +119,8 @@ public function unserialize($data) $this->defaults = $data['defaults']; $this->requirements = $data['requirements']; $this->options = $data['options']; + $this->schemes = $data['schemes']; + $this->methods = $data['methods']; } /** @@ -149,6 +175,80 @@ public function setHostnamePattern($pattern) return $this; } + /** + * Returns the lowercased schemes this route is restricted to. + * So an empty array means that any scheme is allowed. + * + * @return array The schemes + */ + public function getSchemes() + { + return $this->schemes; + } + + /** + * Sets the schemes (e.g. 'https') this route is restricted to. + * So an empty array means that any scheme is allowed. + * + * This method implements a fluent interface. + * + * @param string|array $schemes The scheme or an array of schemes + * + * @return Route The current Route instance + */ + public function setSchemes($schemes) + { + $this->schemes = array_map('strtolower', (array) $schemes); + + // this is to keep BC and will be removed in a future version + if ($this->schemes) { + $this->requirements['_scheme'] = implode('|', $this->schemes); + } else { + unset($this->requirements['_scheme']); + } + + $this->compiled = null; + + return $this; + } + + /** + * Returns the uppercased HTTP methods this route is restricted to. + * So an empty array means that any method is allowed. + * + * @return array The schemes + */ + public function getMethods() + { + return $this->methods; + } + + /** + * Sets the HTTP methods (e.g. 'POST') this route is restricted to. + * So an empty array means that any method is allowed. + * + * This method implements a fluent interface. + * + * @param string|array $methods The method or an array of methods + * + * @return Route The current Route instance + */ + public function setMethods($methods) + { + $this->methods = array_map('strtoupper', (array) $methods); + + // this is to keep BC and will be removed in a future version + if ($this->methods) { + $this->requirements['_method'] = implode('|', $this->methods); + } else { + unset($this->requirements['_method']); + } + + $this->compiled = null; + + return $this; + } + /** * Returns the options. * @@ -454,6 +554,13 @@ private function sanitizeRequirement($key, $regex) throw new \InvalidArgumentException(sprintf('Routing requirement for "%s" cannot be empty.', $key)); } + // this is to keep BC and will be removed in a future version + if ('_scheme' === $key) { + $this->setSchemes(explode('|', $regex)); + } elseif ('_method' === $key) { + $this->setMethods(explode('|', $regex)); + } + return $regex; } } diff --git a/src/Symfony/Component/Routing/Tests/RouteTest.php b/src/Symfony/Component/Routing/Tests/RouteTest.php index 1a11d7440e666..a3e4fdcdb2b0c 100644 --- a/src/Symfony/Component/Routing/Tests/RouteTest.php +++ b/src/Symfony/Component/Routing/Tests/RouteTest.php @@ -23,6 +23,14 @@ public function testConstructor() $this->assertEquals(array('foo' => '\d+'), $route->getRequirements(), '__construct() takes requirements as its third argument'); $this->assertEquals('bar', $route->getOption('foo'), '__construct() takes options as its fourth argument'); $this->assertEquals('{locale}.example.com', $route->getHostnamePattern(), '__construct() takes a hostname pattern as its fifth argument'); + + $route = new Route('/', array(), array(), array(), '', array('Https'), array('POST', 'put')); + $this->assertEquals(array('https'), $route->getSchemes(), '__construct() takes schemes as its sixth argument and lowercases it'); + $this->assertEquals(array('POST', 'PUT'), $route->getMethods(), '__construct() takes methods as its seventh argument and uppercases it'); + + $route = new Route('/', array(), array(), array(), '', 'Https', 'Post'); + $this->assertEquals(array('https'), $route->getSchemes(), '__construct() takes a single scheme as its sixth argument'); + $this->assertEquals(array('POST'), $route->getMethods(), '__construct() takes a single method as its seventh argument'); } public function testPattern() @@ -129,6 +137,50 @@ public function testHostnamePattern() $this->assertEquals('{locale}.example.net', $route->getHostnamePattern(), '->setHostnamePattern() sets the hostname pattern'); } + public function testScheme() + { + $route = new Route('/'); + $this->assertEquals(array(), $route->getSchemes(), 'schemes is initialized with array()'); + $route->setSchemes('hTTp'); + $this->assertEquals(array('http'), $route->getSchemes(), '->setSchemes() accepts a single scheme string and lowercases it'); + $route->setSchemes(array('HttpS', 'hTTp')); + $this->assertEquals(array('https', 'http'), $route->getSchemes(), '->setSchemes() accepts an array of schemes and lowercases them'); + } + + public function testSchemeIsBC() + { + $route = new Route('/'); + $route->setRequirement('_scheme', 'http|https'); + $this->assertEquals('http|https', $route->getRequirement('_scheme')); + $this->assertEquals(array('http', 'https'), $route->getSchemes()); + $route->setSchemes(array('hTTp')); + $this->assertEquals('http', $route->getRequirement('_scheme')); + $route->setSchemes(array()); + $this->assertNull($route->getRequirement('_scheme')); + } + + public function testMethod() + { + $route = new Route('/'); + $this->assertEquals(array(), $route->getMethods(), 'methods is initialized with array()'); + $route->setMethods('gEt'); + $this->assertEquals(array('GET'), $route->getMethods(), '->setMethods() accepts a single method string and uppercases it'); + $route->setMethods(array('gEt', 'PosT')); + $this->assertEquals(array('GET', 'POST'), $route->getMethods(), '->setMethods() accepts an array of methods and uppercases them'); + } + + public function testMethodIsBC() + { + $route = new Route('/'); + $route->setRequirement('_method', 'GET|POST'); + $this->assertEquals('GET|POST', $route->getRequirement('_method')); + $this->assertEquals(array('GET', 'POST'), $route->getMethods()); + $route->setMethods(array('gEt')); + $this->assertEquals('GET', $route->getRequirement('_method')); + $route->setMethods(array()); + $this->assertNull($route->getRequirement('_method')); + } + public function testCompile() { $route = new Route('/{foo}'); From 2834e7ef21b9f6e9d9d1e6beeb1b0a2bd77f5f7e Mon Sep 17 00:00:00 2001 From: Tobias Schultze Date: Thu, 15 Nov 2012 12:26:47 +0100 Subject: [PATCH 2/4] added scheme and method setter in RouteCollection --- .../Component/Routing/RouteCollection.php | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/Symfony/Component/Routing/RouteCollection.php b/src/Symfony/Component/Routing/RouteCollection.php index 4df6f7d145edf..af14134c65fc2 100644 --- a/src/Symfony/Component/Routing/RouteCollection.php +++ b/src/Symfony/Component/Routing/RouteCollection.php @@ -318,6 +318,30 @@ public function addOptions(array $options) } } + /** + * Sets the schemes (e.g. 'https') all child routes are restricted to. + * + * @param string|array $schemes The scheme or an array of schemes + */ + public function setSchemes($schemes) + { + foreach ($this->routes as $route) { + $route->setSchemes($schemes); + } + } + + /** + * Sets the HTTP methods (e.g. 'POST') all child routes are restricted to. + * + * @param string|array $methods The method or an array of methods + */ + public function setMethods($methods) + { + foreach ($this->routes as $route) { + $route->setMethods($methods); + } + } + /** * Returns an array of resources loaded to build this collection. * From d374e70f7e30d9be050adf20f7a93e6c78f5d31a Mon Sep 17 00:00:00 2001 From: Tobias Schultze Date: Thu, 15 Nov 2012 13:20:40 +0100 Subject: [PATCH 3/4] made schemes and methods available in YamlFileLoader --- .../Component/Routing/Loader/YamlFileLoader.php | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/Symfony/Component/Routing/Loader/YamlFileLoader.php b/src/Symfony/Component/Routing/Loader/YamlFileLoader.php index 7a5e268b27cb3..cfc2ac3701868 100644 --- a/src/Symfony/Component/Routing/Loader/YamlFileLoader.php +++ b/src/Symfony/Component/Routing/Loader/YamlFileLoader.php @@ -28,7 +28,7 @@ class YamlFileLoader extends FileLoader { private static $availableKeys = array( - 'resource', 'type', 'prefix', 'pattern', 'hostname_pattern', 'defaults', 'requirements', 'options', + 'resource', 'type', 'prefix', 'pattern', 'hostname_pattern', 'schemes', 'methods', 'defaults', 'requirements', 'options', ); /** @@ -98,9 +98,11 @@ protected function parseRoute(RouteCollection $collection, $name, array $config, $defaults = isset($config['defaults']) ? $config['defaults'] : array(); $requirements = isset($config['requirements']) ? $config['requirements'] : array(); $options = isset($config['options']) ? $config['options'] : array(); - $hostnamePattern = isset($config['hostname_pattern']) ? $config['hostname_pattern'] : null; + $hostnamePattern = isset($config['hostname_pattern']) ? $config['hostname_pattern'] : ''; + $schemes = isset($config['schemes']) ? $config['schemes'] : array(); + $methods = isset($config['methods']) ? $config['methods'] : array(); - $route = new Route($config['pattern'], $defaults, $requirements, $options, $hostnamePattern); + $route = new Route($config['pattern'], $defaults, $requirements, $options, $hostnamePattern, $schemes, $methods); $collection->add($name, $route); } @@ -121,6 +123,8 @@ protected function parseImport(RouteCollection $collection, array $config, $path $requirements = isset($config['requirements']) ? $config['requirements'] : array(); $options = isset($config['options']) ? $config['options'] : array(); $hostnamePattern = isset($config['hostname_pattern']) ? $config['hostname_pattern'] : null; + $schemes = isset($config['schemes']) ? $config['schemes'] : null; + $methods = isset($config['methods']) ? $config['methods'] : null; $this->setCurrentDir(dirname($path)); @@ -130,6 +134,12 @@ protected function parseImport(RouteCollection $collection, array $config, $path if (null !== $hostnamePattern) { $subCollection->setHostnamePattern($hostnamePattern); } + if (null !== $schemes) { + $subCollection->setSchemes($schemes); + } + if (null !== $methods) { + $subCollection->setMethods($methods); + } $subCollection->addDefaults($defaults); $subCollection->addRequirements($requirements); $subCollection->addOptions($options); From e803f4663c0b488b9a76f501d4fcb5cb6a89f51d Mon Sep 17 00:00:00 2001 From: Tobias Schultze Date: Fri, 30 Nov 2012 13:51:45 +0100 Subject: [PATCH 4/4] made schemes and methods available in XmlFileLoader it uses an attribute list instead of multiple scheme/method elements that I also experimented with --- .../Component/Routing/Loader/XmlFileLoader.php | 13 ++++++++++++- .../Routing/Loader/schema/routing/routing-1.0.xsd | 14 ++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Component/Routing/Loader/XmlFileLoader.php b/src/Symfony/Component/Routing/Loader/XmlFileLoader.php index a07c7f80a5547..3dec419ef0b50 100644 --- a/src/Symfony/Component/Routing/Loader/XmlFileLoader.php +++ b/src/Symfony/Component/Routing/Loader/XmlFileLoader.php @@ -116,9 +116,12 @@ protected function parseRoute(RouteCollection $collection, \DOMElement $node, $p throw new \InvalidArgumentException(sprintf('The element in file "%s" must have an "id" and a "pattern" attribute.', $path)); } + $schemes = array_filter(explode(' ', $node->getAttribute('schemes'))); + $methods = array_filter(explode(' ', $node->getAttribute('methods'))); + list($defaults, $requirements, $options) = $this->parseConfigs($node, $path); - $route = new Route($node->getAttribute('pattern'), $defaults, $requirements, $options, $node->getAttribute('hostname-pattern')); + $route = new Route($node->getAttribute('pattern'), $defaults, $requirements, $options, $node->getAttribute('hostname-pattern'), $schemes, $methods); $collection->add($id, $route); } @@ -141,6 +144,8 @@ protected function parseImport(RouteCollection $collection, \DOMElement $node, $ $type = $node->getAttribute('type'); $prefix = $node->getAttribute('prefix'); $hostnamePattern = $node->hasAttribute('hostname-pattern') ? $node->getAttribute('hostname-pattern') : null; + $schemes = $node->hasAttribute('schemes') ? array_filter(explode(' ', $node->getAttribute('schemes'))) : null; + $methods = $node->hasAttribute('methods') ? array_filter(explode(' ', $node->getAttribute('methods'))) : null; list($defaults, $requirements, $options) = $this->parseConfigs($node, $path); @@ -152,6 +157,12 @@ protected function parseImport(RouteCollection $collection, \DOMElement $node, $ if (null !== $hostnamePattern) { $subCollection->setHostnamePattern($hostnamePattern); } + if (null !== $schemes) { + $subCollection->setSchemes($schemes); + } + if (null !== $methods) { + $subCollection->setMethods($methods); + } $subCollection->addDefaults($defaults); $subCollection->addRequirements($requirements); $subCollection->addOptions($options); 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 3143b3ac72a92..65f63f466edd5 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 @@ -17,6 +17,16 @@ + + + + + + + + + + @@ -38,6 +48,8 @@ + + @@ -47,6 +59,8 @@ + +