Skip to content
This repository
Browse code

[FrameworkBundle] Allow using kernel parameters in routes

Kernel parameters can now be used at any position in patterns, defaults and requirements.
  • Loading branch information...
commit 0555913fbbad5ce29b7dd836155f832d5cfc288f 1 parent e71149b
Victor Berchet authored July 04, 2012
2  src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md
Source Rendered
@@ -38,3 +38,5 @@ CHANGELOG
38 38
    create the class cache.
39 39
  * [BC BREAK] TemplateNameParser::parseFromFilename() has been moved to a dedicated
40 40
    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 '%'.
74  src/Symfony/Bundle/FrameworkBundle/Routing/Router.php
@@ -16,6 +16,8 @@
16 16
 use Symfony\Component\DependencyInjection\ContainerInterface;
17 17
 use Symfony\Component\Routing\RouteCollection;
18 18
 use Symfony\Component\HttpKernel\CacheWarmer\WarmableInterface;
  19
+use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException;
  20
+use Symfony\Component\DependencyInjection\Exception\RuntimeException;
19 21
 
20 22
 /**
21 23
  * This Router only creates the Loader only when the cache is empty.
@@ -72,9 +74,12 @@ public function warmUp($cacheDir)
72 74
     }
73 75
 
74 76
     /**
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.
76 81
      *
77  
-     * @param $collection
  82
+     * @param RouteCollection $collection
78 83
      */
79 84
     private function resolveParameters(RouteCollection $collection)
80 85
     {
@@ -83,27 +88,60 @@ private function resolveParameters(RouteCollection $collection)
83 88
                 $this->resolveParameters($route);
84 89
             } else {
85 90
                 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));
94 92
                 }
95 93
 
96 94
                 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));
105 96
                 }
  97
+
  98
+                $route->setPattern($this->resolveString($route->getPattern()));
106 99
             }
107 100
         }
108 101
     }
  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
+                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
+    }
109 147
 }
169  src/Symfony/Bundle/FrameworkBundle/Tests/Routing/RouterTest.php
@@ -17,72 +17,151 @@
17 17
 
18 18
 class RoutingTest extends \PHPUnit_Framework_TestCase
19 19
 {
20  
-    public function testPlaceholders()
  20
+    public function testDefaultsPlaceholders()
21 21
     {
22 22
         $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
+        ));
38 34
 
39 35
         $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'));
46 41
 
47 42
         $router = new Router($sc, 'foo');
48 43
         $route = $router->getRouteCollection()->get('foo');
49 44
 
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
+        );
63 53
     }
64 54
 
65  
-    private function getServiceContainer(RouteCollection $routes)
  55
+    public function testRequirementsPlaceholders()
66 56
     {
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();
73 58
 
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
+        );
75 108
     }
76 109
 
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)
78 148
     {
79 149
         $loader = $this->getMock('Symfony\Component\Config\Loader\LoaderInterface');
  150
+
80 151
         $loader
81 152
             ->expects($this->any())
82 153
             ->method('load')
83 154
             ->will($this->returnValue($routes))
84 155
         ;
85 156
 
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;
87 166
     }
88 167
 }

0 notes on commit 0555913

Please sign in to comment.
Something went wrong with that request. Please try again.