diff --git a/src/Symfony/Component/Routing/Annotation/Route.php b/src/Symfony/Component/Routing/Annotation/Route.php index 62ad8c8b8624c..e1d621e806064 100644 --- a/src/Symfony/Component/Routing/Annotation/Route.php +++ b/src/Symfony/Component/Routing/Annotation/Route.php @@ -31,6 +31,7 @@ class Route private $methods = array(); private $schemes = array(); private $condition; + private $explicitDefaults = false; /** * @param array $data An array of key/value parameters @@ -161,4 +162,14 @@ public function getCondition() { return $this->condition; } + + public function setExplicitDefaults($explicitDefaults) + { + $this->explicitDefaults = $explicitDefaults; + } + + public function getExplicitDefaults() + { + return $this->explicitDefaults; + } } diff --git a/src/Symfony/Component/Routing/Generator/Dumper/PhpGeneratorDumper.php b/src/Symfony/Component/Routing/Generator/Dumper/PhpGeneratorDumper.php index 12dd3f28faf0f..363e0806584f6 100644 --- a/src/Symfony/Component/Routing/Generator/Dumper/PhpGeneratorDumper.php +++ b/src/Symfony/Component/Routing/Generator/Dumper/PhpGeneratorDumper.php @@ -91,6 +91,7 @@ private function generateDeclaredRoutes() $properties[] = $compiledRoute->getTokens(); $properties[] = $compiledRoute->getHostTokens(); $properties[] = $route->getSchemes(); + $properties[] = $route->getExplicitDefaults(); $routes .= sprintf(" '%s' => %s,\n", $name, PhpMatcherDumper::export($properties)); } @@ -127,9 +128,9 @@ public function generate($name, $parameters = array(), $referenceType = self::AB throw new RouteNotFoundException(sprintf('Unable to generate a URL for the named route "%s" as such route does not exist.', $name)); } - list($variables, $defaults, $requirements, $tokens, $hostTokens, $requiredSchemes) = self::$declaredRoutes[$name]; + list($variables, $defaults, $requirements, $tokens, $hostTokens, $requiredSchemes, $explicitDefaults) = self::$declaredRoutes[$name]; - return $this->doGenerate($variables, $defaults, $requirements, $tokens, $parameters, $name, $referenceType, $hostTokens, $requiredSchemes); + return $this->doGenerate($variables, $defaults, $requirements, $tokens, $parameters, $name, $referenceType, $hostTokens, $requiredSchemes, $explicitDefaults); } EOF; } diff --git a/src/Symfony/Component/Routing/Generator/UrlGenerator.php b/src/Symfony/Component/Routing/Generator/UrlGenerator.php index 7eea2472f75d8..27396c0a4eb5d 100644 --- a/src/Symfony/Component/Routing/Generator/UrlGenerator.php +++ b/src/Symfony/Component/Routing/Generator/UrlGenerator.php @@ -141,7 +141,7 @@ public function generate($name, $parameters = array(), $referenceType = self::AB * @throws InvalidParameterException When a parameter value for a placeholder is not correct because * it does not match the requirement */ - protected function doGenerate($variables, $defaults, $requirements, $tokens, $parameters, $name, $referenceType, $hostTokens, array $requiredSchemes = array()) + protected function doGenerate($variables, $defaults, $requirements, $tokens, $parameters, $name, $referenceType, $hostTokens, array $requiredSchemes = array(), $explicitDefaults = false) { $variables = array_flip($variables); $mergedParams = array_replace($defaults, $this->context->getParameters(), $parameters); @@ -156,7 +156,7 @@ protected function doGenerate($variables, $defaults, $requirements, $tokens, $pa $message = 'Parameter "{parameter}" for route "{route}" must match "{expected}" ("{given}" given) to generate a corresponding URL.'; foreach ($tokens as $token) { if ('variable' === $token[0]) { - if (!$optional || !array_key_exists($token[3], $defaults) || null !== $mergedParams[$token[3]] && (string) $mergedParams[$token[3]] !== (string) $defaults[$token[3]]) { + if (!$optional || !array_key_exists($token[3], $defaults) || null !== $mergedParams[$token[3]] && ((string) $mergedParams[$token[3]] !== (string) $defaults[$token[3]]) || true === $explicitDefaults) { // check requirement if (null !== $this->strictRequirements && !preg_match('#^'.$token[2].'$#'.(empty($token[4]) ? '' : 'u'), $mergedParams[$token[3]])) { if ($this->strictRequirements) { diff --git a/src/Symfony/Component/Routing/Loader/XmlFileLoader.php b/src/Symfony/Component/Routing/Loader/XmlFileLoader.php index 54b0c65c0cd26..d75351595757c 100644 --- a/src/Symfony/Component/Routing/Loader/XmlFileLoader.php +++ b/src/Symfony/Component/Routing/Loader/XmlFileLoader.php @@ -159,6 +159,7 @@ protected function parseImport(RouteCollection $collection, \DOMElement $node, $ $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; $trailingSlashOnRoot = $node->hasAttribute('trailing-slash-on-root') ? XmlUtils::phpize($node->getAttribute('trailing-slash-on-root')) : true; + $explicitDefaults = $node->hasAttribute('explicit-defaults') ? XmlUtils::phpize($node->getAttribute('explicit-defaults')) : false; list($defaults, $requirements, $options, $condition, /* $paths */, $prefixes) = $this->parseConfigs($node, $path); @@ -222,6 +223,7 @@ protected function parseImport(RouteCollection $collection, \DOMElement $node, $ $subCollection->setMethods($methods); } $subCollection->addDefaults($defaults); + $subCollection->setExplicitDefaults($explicitDefaults); $subCollection->addRequirements($requirements); $subCollection->addOptions($options); @@ -262,6 +264,7 @@ protected function loadFile($file) private function parseConfigs(\DOMElement $node, $path) { $defaults = array(); + $explicitDefaults = false; $requirements = array(); $options = array(); $condition = null; diff --git a/src/Symfony/Component/Routing/Loader/YamlFileLoader.php b/src/Symfony/Component/Routing/Loader/YamlFileLoader.php index ca2e313aa4358..34489c0d4c551 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', 'path', 'host', 'schemes', 'methods', 'defaults', 'requirements', 'options', 'condition', 'controller', 'name_prefix', 'trailing_slash_on_root', + 'resource', 'type', 'prefix', 'path', 'host', 'schemes', 'methods', 'defaults', 'explicit_defaults', 'requirements', 'options', 'condition', 'controller', 'name_prefix', 'trailing_slash_on_root', ); private $yamlParser; @@ -109,6 +109,7 @@ public function supports($resource, $type = null) protected function parseRoute(RouteCollection $collection, $name, array $config, $path) { $defaults = isset($config['defaults']) ? $config['defaults'] : array(); + $explicitDefaults = isset($config['explicit_defaults']) ? $config['explicit_defaults'] : false; $requirements = isset($config['requirements']) ? $config['requirements'] : array(); $options = isset($config['options']) ? $config['options'] : array(); $host = isset($config['host']) ? $config['host'] : ''; @@ -127,7 +128,7 @@ protected function parseRoute(RouteCollection $collection, $name, array $config, } if (\is_array($config['path'])) { - $route = new Route('', $defaults, $requirements, $options, $host, $schemes, $methods, $condition); + $route = new Route('', $defaults, $requirements, $options, $host, $schemes, $methods, $condition, $explicitDefaults); foreach ($config['path'] as $locale => $path) { $localizedRoute = clone $route; @@ -137,7 +138,7 @@ protected function parseRoute(RouteCollection $collection, $name, array $config, $collection->add($name.'.'.$locale, $localizedRoute); } } else { - $route = new Route($config['path'], $defaults, $requirements, $options, $host, $schemes, $methods, $condition); + $route = new Route($config['path'], $defaults, $requirements, $options, $host, $schemes, $methods, $condition, $explicitDefaults); $collection->add($name, $route); } } 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 1ea4651c3ac2b..c385afa4577d8 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 @@ -35,6 +35,7 @@ + diff --git a/src/Symfony/Component/Routing/Route.php b/src/Symfony/Component/Routing/Route.php index 1b060a869dd70..eb8b744aa15fa 100644 --- a/src/Symfony/Component/Routing/Route.php +++ b/src/Symfony/Component/Routing/Route.php @@ -27,6 +27,7 @@ class Route implements \Serializable private $requirements = array(); private $options = array(); private $condition = ''; + private $explicitDefaults = false; /** * @var null|CompiledRoute @@ -49,8 +50,9 @@ class Route implements \Serializable * @param string|string[] $schemes A required URI scheme or an array of restricted schemes * @param string|string[] $methods A required HTTP method or an array of restricted methods * @param string $condition A condition that should evaluate to true for the route to match + * @param bool $explicitDefaults */ - public function __construct(string $path, array $defaults = array(), array $requirements = array(), array $options = array(), ?string $host = '', $schemes = array(), $methods = array(), ?string $condition = '') + public function __construct(string $path, array $defaults = array(), array $requirements = array(), array $options = array(), ?string $host = '', $schemes = array(), $methods = array(), ?string $condition = '', bool $explicitDefaults = false) { $this->setPath($path); $this->addDefaults($defaults); @@ -60,6 +62,7 @@ public function __construct(string $path, array $defaults = array(), array $requ $this->setSchemes($schemes); $this->setMethods($methods); $this->setCondition($condition); + $this->setExplicitDefaults($explicitDefaults); } /** @@ -527,6 +530,33 @@ public function setCondition($condition) return $this; } + /** + * Returns the explicitDefaults. + * + * @return string The explicitDefaults + */ + public function getExplicitDefaults() + { + return $this->explicitDefaults; + } + + /** + * Sets the explicitDefaults. + * + * This method implements a fluent interface. + * + * @param string $explicitDefaults The explicitDefaults + * + * @return $this + */ + public function setExplicitDefaults($explicitDefaults) + { + $this->explicitDefaults = (bool) $explicitDefaults; + $this->compiled = null; + + return $this; + } + /** * Compiles the route. * diff --git a/src/Symfony/Component/Routing/RouteCollection.php b/src/Symfony/Component/Routing/RouteCollection.php index 4525404d6d030..02b47d010527b 100644 --- a/src/Symfony/Component/Routing/RouteCollection.php +++ b/src/Symfony/Component/Routing/RouteCollection.php @@ -200,6 +200,20 @@ public function setCondition($condition) } } + /** + * Sets explicit defaults on all routes. + * + * Existing explicit defaults will be overridden. + * + * @param bool $explicitDefaults + */ + public function setExplicitDefaults($explicitDefaults) + { + foreach ($this->routes as $route) { + $route->setExplicitDefaults($explicitDefaults); + } + } + /** * Adds defaults to all routes. * diff --git a/src/Symfony/Component/Routing/Tests/Annotation/RouteTest.php b/src/Symfony/Component/Routing/Tests/Annotation/RouteTest.php index 4d6b2a9c8b45e..6e5c4a04b6f6f 100644 --- a/src/Symfony/Component/Routing/Tests/Annotation/RouteTest.php +++ b/src/Symfony/Component/Routing/Tests/Annotation/RouteTest.php @@ -54,6 +54,7 @@ public function getValidParameters() array('host', '{locale}.example.com', 'getHost'), array('condition', 'context.getMethod() == "GET"', 'getCondition'), array('value', array('nl' => '/hier', 'en' => '/here'), 'getLocalizedPaths'), + array('explicit_defaults', true, 'getExplicitDefaults'), ); } } diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/validresource.php b/src/Symfony/Component/Routing/Tests/Fixtures/validresource.php index 482c80b29e919..19d61f028fe83 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/validresource.php +++ b/src/Symfony/Component/Routing/Tests/Fixtures/validresource.php @@ -6,6 +6,7 @@ $collection->addDefaults(array( 'foo' => 123, )); +$collection->setExplicitDefaults(true); $collection->addRequirements(array( 'foo' => '\d+', )); diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/validresource.yml b/src/Symfony/Component/Routing/Tests/Fixtures/validresource.yml index faf2263ae52f4..a2ba1d50020cf 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/validresource.yml +++ b/src/Symfony/Component/Routing/Tests/Fixtures/validresource.yml @@ -2,6 +2,7 @@ _blog: resource: validpattern.yml prefix: /{foo} defaults: { 'foo': '123' } + explicit_defaults: true requirements: { 'foo': '\d+' } options: { 'foo': 'bar' } host: "" diff --git a/src/Symfony/Component/Routing/Tests/Generator/Dumper/PhpGeneratorDumperTest.php b/src/Symfony/Component/Routing/Tests/Generator/Dumper/PhpGeneratorDumperTest.php index 311cd12bd7594..5a5f1a042c40f 100644 --- a/src/Symfony/Component/Routing/Tests/Generator/Dumper/PhpGeneratorDumperTest.php +++ b/src/Symfony/Component/Routing/Tests/Generator/Dumper/PhpGeneratorDumperTest.php @@ -230,6 +230,19 @@ public function testDumpForRouteWithDefaults() $this->assertEquals('/testing', $url); } + public function testDumpForRouteWithExplicitDefaults() + { + $this->routeCollection->add('Test', (new Route('/testing/{foo}.{_format}', array('foo' => 'bar', '_format' => 'baz')))->setExplicitDefaults(true)); + + file_put_contents($this->testTmpFilepath, $this->generatorDumper->dump(array('class' => 'ExplicitDefaultsRoutesUrlGenerator'))); + include $this->testTmpFilepath; + + $projectUrlGenerator = new \ExplicitDefaultsRoutesUrlGenerator(new RequestContext()); + $url = $projectUrlGenerator->generate('Test', array()); + + $this->assertEquals('/testing/bar.baz', $url); + } + public function testDumpWithSchemeRequirement() { $this->routeCollection->add('Test1', new Route('/testing', array(), array(), array(), '', array('ftp', 'https'))); diff --git a/src/Symfony/Component/Routing/Tests/Generator/UrlGeneratorTest.php b/src/Symfony/Component/Routing/Tests/Generator/UrlGeneratorTest.php index d4bf18ccac929..7e6220aafc18f 100644 --- a/src/Symfony/Component/Routing/Tests/Generator/UrlGeneratorTest.php +++ b/src/Symfony/Component/Routing/Tests/Generator/UrlGeneratorTest.php @@ -291,6 +291,13 @@ public function testNoTrailingSlashForMultipleOptionalParameters() $this->assertEquals('/app.php/category/foo', $this->getGenerator($routes)->generate('test', array('slug1' => 'foo'))); } + public function testExplicitDefaults() + { + $routes = $this->getRoutes('test', (new Route('/category/{slug1}'))->setExplicitDefaults(true)); + + $this->assertEquals('/app.php/category/foo', $this->getGenerator($routes)->generate('test', array('slug1' => 'foo'))); + } + public function testWithAnIntegerAsADefaultValue() { $routes = $this->getRoutes('test', new Route('/{default}', array('default' => 0))); diff --git a/src/Symfony/Component/Routing/Tests/RouteTest.php b/src/Symfony/Component/Routing/Tests/RouteTest.php index e28cdaf59315e..2f47b824b874c 100644 --- a/src/Symfony/Component/Routing/Tests/RouteTest.php +++ b/src/Symfony/Component/Routing/Tests/RouteTest.php @@ -73,6 +73,12 @@ public function testOption() $this->assertTrue($route->hasOption('foo'), '->hasOption() return true if option is set'); } + public function testExplicitDefaults() + { + $route = new Route('/{foo}'); + $this->assertFalse($route->getExplicitDefaults(), '->getExplicitDefaults() return false by default'); + } + public function testDefaults() { $route = new Route('/{foo}');