8000 [FrameworkBundle] Allow using kernel parameters in routes · symfony/symfony@0555913 · GitHub
[go: up one dir, main page]

Skip to content

Commit 0555913

Browse files
committed
[FrameworkBundle] Allow using kernel parameters in routes
Kernel parameters can now be used at any position in patterns, defaults and requirements.
1 parent e71149b commit 0555913

File tree

3 files changed

+182
-63
lines changed

3 files changed

+182
-63
lines changed

src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,5 @@ CHANGELOG
3838
create the class cache.
3939
* [BC BREAK] TemplateNameParser::parseFromFilename() has been moved to a dedicated
4040
parser: TemplateFilenameParser::parse().
41+
* [BC BREAK] Kernel parameters are replaced by their value whereever they appear
42+
in Route patterns, requirements and defaults. Use '%%' as the escaped value for '%'.

src/Symfony/Bundle/FrameworkBundle/Routing/Router.php

Lines changed: 56 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
use Symfony\Component\DependencyInjection\ContainerInterface;
1717
use Symfony\Component\Routing\RouteCollection;
1818
use Symfony\Component\HttpKernel\CacheWarmer\WarmableInterface;
19+
use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException;
20+
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
1921

2022
/**
2123
* This Router only creates the Loader only when the cache is empty.
@@ -72,9 +74,12 @@ public function warmUp($cacheDir)
7274
}
7375

7476
/**
75-
* Replaces placeholders with service container parameter values in route defaults and requirements.
77+
* Replaces placeholders with service container parameter values in:
78+
* - the route defaults,
79+
* - the route requirements,
80+
* - the route pattern.
7681
*
77-
* @param $collection
82+
* @param RouteCollection $collection
7883
*/
7984
private function resolveParameters(RouteCollection $collection)
8085
{
@@ -83,27 +88,60 @@ private function resolveParameters(RouteCollection $collection)
8388
$this->resolveParameters($route);
8489
} else {
8590
foreach ($route->getDefaults() as $name => $value) {
86-
if (!$value || '%' !== $value[0] || '%' !== substr($value, -1)) {
87-
continue;
88-
}
89-
90-
$key = substr($value, 1, -1);
91-
if ($this->container->hasParameter($key)) {
92-
$route->setDefault($name, $this->container->getParameter($key));
93-
}
91+
$route->setDefault($name, $this->resolveString($value));
9492
}
9593

9694
foreach ($route->getRequirements() as $name => $value) {
97-
if (!$value || '%' !== $value[0] || '%' !== substr($value, -1)) {
98-
continue;
99-
}
100-
101-
$key = substr($value, 1, -1);
102-
if ($this->container->hasParameter($key)) {
103-
$route->setRequirement($name, $this->container->getParameter($key));
104-
}
95+
$route->setRequirement($name, $this->resolveString($value));
10596
}
97+
98+
$route->setPattern($this->resolveString($route->getPattern()));
10699
}
107100
}
108101
}
102+
103+
/**
104+
* Replaces placeholders with the service container parameters in the given string.
105+
*
106+
* @param string $value The source string which might contain %placeholders%
107+
*
108+
* @return string A string where the placeholders have been replaced.
109+
*
110+
* @throws ParameterNotFoundException When a placeholder does not exist as a container parameter
111+
* @throws RuntimeException When a container value is not a string or a numeric value
112+
*/
113+
private function resolveString($value)
114+
{
115+
$container = $this->container;
116+
117+
$escapedValue = preg_replace_callback('/%%|%([^%\s]+)%/', function ($match) use ($container, $value) {
118+
// skip %%
119+
if (!isset($match[1])) {
120+
return '%%';
121+
}
122+
123+
$key = strtolower($match[1]);
124+
125+
if (!$container->hasParameter($key)) {
126< F438 span class="diff-text-marker">+
throw new ParameterNotFoundException($key);
127+
}
128+
129+
$resolved = $container->getParameter($key);
130+
131+
if (is_string($resolved) || is_numeric($resolved)) {
132+
return (string) $resolved;
133+
}
134+
135+
throw new RuntimeException(sprintf(
136+
'A string value must be composed of strings and/or numbers,' .
137+
'but found parameter "%s" of type %s inside string value "%s".',
138+
$key,
139+
gettype($resolved),
140+
$value)
141+
);
142+
143+
}, $value);
144+
145+
return str_replace('%%', '%', $escapedValue);
146+
}
109147
}

src/Symfony/Bundle/FrameworkBundle/Tests/Routing/RouterTest.php

Lines changed: 124 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -17,72 +17,151 @@
1717

1818
class RoutingTest extends \PHPUnit_Framework_TestCase
1919
{
20-
public function testPlaceholders()
20+
public function testDefaultsPlaceholders()
2121
{
2222
$routes = new RouteCollection();
23-
$routes->add('foo', new Route('/foo', array(
24-
'foo' => '%foo%',
25-
'bar' => '%bar%',
26-
'foobar' => 'foobar',
27-
'foo1' => '%foo',
28-
'foo2' => 'foo%',
29-
'foo3' => 'f%o%o',
30-
), array(
31-
'foo' => '%foo%',
32-
'bar' => '%bar%',
33-
'foobar' => 'foobar',
34-
'foo1' => '%foo',
35-
'foo2' => 'foo%',
36-
'foo3' => 'f%o%o',
37-
)));
23+
24+
$routes->add('foo', new Route(
25+
'/foo',
26+
array(
27+
'foo' => 'before_%parameter.foo%',
28+
'bar' => '%parameter.bar%_after',
29+
'baz' => '%%unescaped%%',
30+
),
31+
array(
32+
)
33+
));
3834

3935
$sc = $this->getServiceContainer($routes);
40-
$sc->expects($this->at(1))->method('hasParameter')->will($this->returnValue(false));
41-
$sc->expects($this->at(2))->method('hasParameter')->will($this->returnValue(true));
42-
$sc->expects($this->at(3))->method('getParameter')->will($this->returnValue('bar'));
43-
$sc->expects($this->at(4))->method('hasParameter')->will($this->returnValue(false));
44-
$sc->expects($this->at(5))->method('hasParameter')->will($this->returnValue(true));
45-
$sc->expects($this->at(6))->method('getParameter')->will($this->returnValue('bar'));
36+
37+
$sc->expects($this->at(1))->method('hasParameter')->will($this->returnValue(true));
38+
$sc->expects($this->at(2))->method('getParameter')->will($this->returnValue('foo'));
39+
$sc->expects($this->at(3))->method('hasParameter')->will($this->returnValue(true));
40+
$sc->expects($this->at(4))->method('getParameter')->will($this->returnValue('bar'));
4641

4742
$router = new Router($sc, 'foo');
4843
$route = $router->getRouteCollection()->get('foo');
4944

50-
$this->assertEquals('%foo%', $route->getDefault('foo'));
51-
$this->assertEquals('bar', $route->getDefault('bar'));
52-
$this->assertEquals('foobar', $route->getDefault('foobar'));
53-
$this->assertEquals('%foo', $route->getDefault('foo1'));
54-
$this->assertEquals('foo%', $route->getDefault('foo2'));
55-
$this->assertEquals('f%o%o', $route->getDefault('foo3'));
56-
57-
$this->assertEquals('%foo%', $route->getRequirement('foo'));
58-
$this->assertEquals('bar', $route->getRequirement('bar'));
59-
$this->assertEquals('foobar', $route->getRequirement('foobar'));
60-
$this->assertEquals('%foo', $route->getRequirement('foo1'));
61-
$this->assertEquals('foo%', $route->getRequirement('foo2'));
62-
$this->assertEquals('f%o%o', $route->getRequirement('foo3'));
45+
$this->assertEquals(
46+
array(
47+
'foo' => 'before_foo',
48+
'bar' => 'bar_after',
49+
'baz' => '%unescaped%',
50+
),
51+
$route->getDefaults()
52+
);
6353
}
6454

65-
private function getServiceContainer(RouteCollection $routes)
55+
public function testRequirementsPlaceholders()
6656
{
67-
$sc = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface');
68-
$sc
69-
->expects($this->once())
70-
->method('get')
71-
->will($this->returnValue($this->getLoader($routes)))
72-
;
57+
$routes = new RouteCollection();
7358

74-
return $sc;
59+
$routes->add('foo', new Route(
60+
'/foo',
61+
array(
62+
),
63+
array(
64+
'foo' => 'before_%parameter.foo%',
65+
'bar' => '%parameter.bar%_after',
66+
'baz' => '%%unescaped%%',
67+
)
68+
));
69+
70+
$sc = $this->getServiceContainer($routes);
71+
72+
$sc->expects($this->at(1))->method('hasParameter')->with('parameter.foo')->will($this->returnValue(true));
73+
$sc->expects($this->at(2))->method('getParameter')->with('parameter.foo')->will($this->returnValue('foo'));
74+
$sc->expects($this->at(3))->method('hasParameter')->with('parameter.bar')->will($this->returnValue(true));
75+
$sc->expects($this->at(4))->method('getParameter')->with('parameter.bar')->will($this->returnValue('bar'));
76+
77+
$router = new Router($sc, 'foo');
78+
$route = $router->getRouteCollection()->get('foo');
79+
80+
$this->assertEquals(
81+
array(
82+
'foo' => 'before_foo',
83+
'bar' => 'bar_after',
84+
'baz' => '%unescaped%',
85+
),
86+
$route->getRequirements()
87+
);
88+
}
89+
90+
public function testPatternPlaceholders()
91+
{
92+
$routes = new RouteCollection();
93+
94+
$routes->add('foo', new Route('/before/%parameter.foo%/after/%%unescaped%%'));
95+
96+
$sc = $this->getServiceContainer($routes);
97+
98+
$sc->expects($this->at(1))->method('hasParameter')->with('parameter.foo')->will($this->returnValue(true));
99+
$sc->expects($this->at(2))->method('getParameter')->with('parameter.foo')->will($this->returnValue('foo'));
100+
101+
$router = new Router($sc, 'foo');
102+
$route = $router->getRouteCollection()->get('foo');
103+
104+
$this->assertEquals(
105+
'/before/foo/after/%unescaped%',
106+
$route->getPattern()
107+
);
75108
}
76109

77-
private function getLoader(RouteCollection $routes)
110+
/**
111+
* @expectedException \Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException
112+
* @expectedExceptionMessage You have requested a non-existent parameter "nope".
113+
*/
114+
public function testExceptionOnNonExistentParameter()
115+
{
116+
$routes = new RouteCollection();
117+
118+
$routes->add('foo', new Route('/%nope%'));
119+
120+
$sc = $this->getServiceContainer($routes);
121+
122+
$sc->expects($this->at(1))->method('hasParameter')->with('nope')->will($this->returnValue(false));
123+
124+
$router = new Router($sc, 'foo');
125+
$router->getRouteCollection()->get('foo');
126+
}
127+
128+
/**
129+
* @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException
130+
* @expectedExceptionMessage A string value must be composed of strings and/or numbers,but found parameter "object" of type object inside string value "/%object%".
131+
*/
132+
public function testExceptionOnNonStringParameter()
133+
{
134+
$routes = new RouteCollection();
135+
136+
$routes->add('foo', new Route('/%object%'));
137+
138+
$sc = $this->getServiceContainer($routes);
139+
140+
$sc->expects($this->at(1))->method('hasParameter')->with('object')->will($this->returnValue(true));
141+
$sc->expects($this->at(2))->method('getParameter')->with('object')->will($this->returnValue(new \stdClass()));
142+
143+
$router = new Router($sc, 'foo');
144+
$router->getRouteCollection()->get('foo');
145+
}
146+
147+
private function getServiceContainer(RouteCollection $routes)
78148
{
79149
$loader = $this->getMock('Symfony\Component\Config\Loader\LoaderInterface');
150+
80151
$loader
81152
->expects($this->any())
82153
->method('load')
83154
->will($this->returnValue($routes))
84155
;
85156

86-
return $loader;
157+
$sc = $this->getMock('Symfony\\Component\\DependencyInjection\\ContainerInterface');
158+
159+
$sc
160+
->expects($this->once())
161+
->method('get')
162+
->will($this->returnValue($loader))
163+
;
164+
165+
return $sc;
87166
}
88167
}

0 commit comments

Comments
 (0)
0