瀏覽代碼

[Routing] Fixes to handle spaces in route pattern

- The route compiler does not add extra space or line-feed,
- The generated regex does not use the 'x' modified any more,
- The PHP and apache matchers do not need to strip any chars (vs space and line feed before),
- The space characters are escaped according to the apache format
Victor Berchet 13 年之前
父節點
當前提交
6465a6987a

+ 44 - 3
src/Symfony/Component/Routing/Matcher/Dumper/ApacheMatcherDumper.php

@@ -31,6 +31,8 @@ class ApacheMatcherDumper extends MatcherDumper
      * @param array $options An array of options
      *
      * @return string A string to be used as Apache rewrite rules
+     *
+     * @throws \LogicException When the route regex is invalid
      */
     public function dump(array $options = array())
     {
@@ -39,6 +41,8 @@ class ApacheMatcherDumper extends MatcherDumper
             'base_uri'    => '',
         ), $options);
 
+        $options['script_name'] = self::escape($options['script_name'], ' ', '\\');
+
         $rules = array("# skip \"real\" requests\nRewriteCond %{REQUEST_FILENAME} -f\nRewriteRule .* - [QSA,L]");
         $methodVars = array();
 
@@ -46,8 +50,14 @@ class ApacheMatcherDumper extends MatcherDumper
             $compiledRoute = $route->compile();
 
             // prepare the apache regex
-            $regex = preg_replace('/\?P<.+?>/', '', substr(str_replace(array("\n", ' '), '', $compiledRoute->getRegex()), 1, -3));
-            $regex = '^'.preg_quote($options['base_uri']).substr($regex, 1);
+            $regex = $compiledRoute->getRegex();
+            $delimiter = $regex[0];
+            $regexPatternEnd = strrpos($regex, $delimiter);
+            if (strlen($regex) < 2 || 0 === $regexPatternEnd) {
+                throw new \LogicException('The "%s" route regex "%s" is invalid', $name, $regex);
+            }
+            $regex = preg_replace('/\?P<.+?>/', '', substr($regex, 1, $regexPatternEnd - 1));
+            $regex = '^'.self::escape(preg_quote($options['base_uri']).substr($regex, 1), ' ', '\\');
 
             $hasTrailingSlash = '/$' == substr($regex, -2) && '^/$' != $regex;
 
@@ -56,7 +66,6 @@ class ApacheMatcherDumper extends MatcherDumper
                 $variables[] = 'E=_ROUTING_'.$variable.':%'.($i + 1);
             }
             foreach ($route->getDefaults() as $key => $value) {
-                // todo: a more legit way to escape the value?
                 $variables[] = 'E=_ROUTING_'.$key.':'.strtr($value, array(
                     ':'  => '\\:',
                     '='  => '\\=',
@@ -112,4 +121,36 @@ class ApacheMatcherDumper extends MatcherDumper
 
         return implode("\n\n", $rules)."\n";
     }
+
+    /**
+     * Escapes a string.
+     *
+     * @param string $string The string to be escaped
+     * @param string $char   The character to be escaped
+     * @param string $with   The character to be used for escaping
+     *
+     * @return string The escaped string
+     */
+    static private function escape($string, $char, $with)
+    {
+        $escaped = false;
+        $output = '';
+        foreach(str_split($string) as $symbol) {
+            if ($escaped) {
+                $output .= $symbol;
+                $escaped = false;
+                continue;
+            }
+            if ($symbol === $char) {
+                $output .= $with.$char;
+                continue;
+            }
+            if ($symbol === $with) {
+                $escaped = true;
+            }
+            $output .= $symbol;
+        }
+
+        return $output;
+    }
 }

+ 2 - 2
src/Symfony/Component/Routing/Matcher/Dumper/PhpMatcherDumper.php

@@ -148,7 +148,7 @@ EOF;
         $conditions = array();
         $hasTrailingSlash = false;
         $matches = false;
-        if (!count($compiledRoute->getVariables()) && false !== preg_match('#^(.)\^(?P<url>.*?)\$\1#', str_replace(array("\n", ' '), '', $compiledRoute->getRegex()), $m)) {
+        if (!count($compiledRoute->getVariables()) && false !== preg_match('#^(.)\^(?P<url>.*?)\$\1#', $compiledRoute->getRegex(), $m)) {
             if ($supportsRedirections && substr($m['url'], -1) === '/') {
                 $conditions[] = sprintf("rtrim(\$pathinfo, '/') === %s", var_export(rtrim(str_replace('\\', '', $m['url']), '/'), true));
                 $hasTrailingSlash = true;
@@ -160,7 +160,7 @@ EOF;
                 $conditions[] = sprintf("0 === strpos(\$pathinfo, %s)", var_export($compiledRoute->getStaticPrefix(), true));
             }
 
-            $regex = str_replace(array("\n", ' '), '', $compiledRoute->getRegex());
+            $regex = $compiledRoute->getRegex();
             if ($supportsRedirections && $pos = strpos($regex, '/$')) {
                 $regex = substr($regex, 0, $pos).'/?$'.substr($regex, $pos + 2);
                 $hasTrailingSlash = true;

+ 42 - 24
src/Symfony/Component/Routing/RouteCompiler.php

@@ -66,7 +66,8 @@ class RouteCompiler implements RouteCompilerInterface
         // find the first optional token
         $firstOptional = INF;
         for ($i = count($tokens) - 1; $i >= 0; $i--) {
-            if ('variable' === $tokens[$i][0] && $route->hasDefault($tokens[$i][3])) {
+            $token = $tokens[$i];
+            if ('variable' === $token[0] && $route->hasDefault($token[3])) {
                 $firstOptional = $i;
             } else {
                 break;
@@ -74,36 +75,53 @@ class RouteCompiler implements RouteCompilerInterface
         }
 
         // compute the matching regexp
-        $regex = '';
-        $indent = 1;
-        if (1 === count($tokens) && 0 === $firstOptional) {
-            $token = $tokens[0];
-            ++$indent;
-            $regex .= str_repeat(' ', $indent * 4).sprintf("%s(?:\n", preg_quote($token[1], '#'));
-            $regex .= str_repeat(' ', $indent * 4).sprintf("(?P<%s>%s)\n", $token[3], $token[2]);
-        } else {
-            foreach ($tokens as $i => $token) {
-                if ('text' === $token[0]) {
-                    $regex .= str_repeat(' ', $indent * 4).preg_quote($token[1], '#')."\n";
-                } else {
-                    if ($i >= $firstOptional) {
-                        $regex .= str_repeat(' ', $indent * 4)."(?:\n";
-                        ++$indent;
-                    }
-                    $regex .= str_repeat(' ', $indent * 4).sprintf("%s(?P<%s>%s)\n", preg_quote($token[1], '#'), $token[3], $token[2]);
-                }
-            }
-        }
-        while (--$indent) {
-            $regex .= str_repeat(' ', $indent * 4).")?\n";
+        $regexp = '';
+        for ($i = 0, $nbToken = count($tokens); $i < $nbToken; $i++) {
+            $regexp .= $this->computeRegexp($tokens, $i, $firstOptional);
         }
 
         return new CompiledRoute(
             $route,
             'text' === $tokens[0][0] ? $tokens[0][1] : '',
-            sprintf("#^\n%s$#xs", $regex),
+            sprintf("#^%s$#s", $regexp),
             array_reverse($tokens),
             $variables
         );
     }
+
+    /**
+     * Computes the regexp used to match the token.
+     *
+     * @param array   $tokens        The route tokens
+     * @param integer $index         The index of the current token
+     * @param integer $firstOptional The index of the first optional token
+     *
+     * @return string The regexp
+     */
+    private function computeRegexp(array $tokens, $index, $firstOptional)
+    {
+        $token = $tokens[$index];
+        if('text' === $token[0]) {
+            // Text tokens
+            return preg_quote($token[1], '#');
+        } else {
+            // Variable tokens
+            if (0 === $index && 0 === $firstOptional && 1 == count($tokens)) {
+                // When the only token is an optional variable token, the separator is required
+                return sprintf('%s(?P<%s>%s)?', preg_quote($token[1], '#'), $token[3], $token[2]);
+            } else {
+                $nbTokens = count($tokens);
+                $regexp = sprintf('%s(?P<%s>%s)', preg_quote($token[1], '#'), $token[3], $token[2]);
+                if ($index >= $firstOptional) {
+                    // Enclose each optional tokens in a subpattern to make it optional
+                    $regexp = "(?:$regexp";
+                    if ($nbTokens - 1 == $index) {
+                        // Close the optional subpatterns
+                        $regexp .= str_repeat(")?", $nbTokens - $firstOptional);
+                    }
+                }
+                return $regexp;
+            }
+        }
+    }
 }

+ 4 - 0
tests/Symfony/Tests/Component/Routing/Fixtures/dumper/url_matcher1.apache

@@ -53,6 +53,10 @@ RewriteRule .* app.php [QSA,L,E=_ROUTING__route:baz5,E=_ROUTING_foo:%1]
 RewriteCond %{REQUEST_URI} ^/test/baz$
 RewriteRule .* app.php [QSA,L,E=_ROUTING__route:baz6,E=_ROUTING_foo:bar\ baz]
 
+# baz7
+RewriteCond %{REQUEST_URI} ^/te\ st/baz$
+RewriteRule .* app.php [QSA,L,E=_ROUTING__route:baz7]
+
 # 405 Method Not Allowed
 RewriteCond %{_ROUTING__allow_GET} !-z [OR]
 RewriteCond %{_ROUTING__allow_HEAD} !-z [OR]

+ 19 - 14
tests/Symfony/Tests/Component/Routing/Fixtures/dumper/url_matcher1.php

@@ -26,12 +26,12 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
         $pathinfo = urldecode($pathinfo);
 
         // foo
-        if (0 === strpos($pathinfo, '/foo') && preg_match('#^/foo/(?P<bar>baz|symfony)$#xs', $pathinfo, $matches)) {
+        if (0 === strpos($pathinfo, '/foo') && preg_match('#^/foo/(?P<bar>baz|symfony)$#s', $pathinfo, $matches)) {
             return array_merge($this->mergeDefaults($matches, array (  'def' => 'test',)), array('_route' => 'foo'));
         }
 
         // bar
-        if (0 === strpos($pathinfo, '/bar') && preg_match('#^/bar/(?P<foo>[^/]+?)$#xs', $pathinfo, $matches)) {
+        if (0 === strpos($pathinfo, '/bar') && preg_match('#^/bar/(?P<foo>[^/]+?)$#s', $pathinfo, $matches)) {
             if (!in_array($this->context->getMethod(), array('GET', 'HEAD'))) {
                 $allow = array_merge($allow, array('GET', 'HEAD'));
                 goto not_bar;
@@ -42,7 +42,7 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
         not_bar:
 
         // barhead
-        if (0 === strpos($pathinfo, '/barhead') && preg_match('#^/barhead/(?P<foo>[^/]+?)$#xs', $pathinfo, $matches)) {
+        if (0 === strpos($pathinfo, '/barhead') && preg_match('#^/barhead/(?P<foo>[^/]+?)$#s', $pathinfo, $matches)) {
             if (!in_array($this->context->getMethod(), array('GET', 'HEAD'))) {
                 $allow = array_merge($allow, array('GET', 'HEAD'));
                 goto not_barhead;
@@ -68,13 +68,13 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
         }
 
         // baz4
-        if (0 === strpos($pathinfo, '/test') && preg_match('#^/test/(?P<foo>[^/]+?)/$#xs', $pathinfo, $matches)) {
+        if (0 === strpos($pathinfo, '/test') && preg_match('#^/test/(?P<foo>[^/]+?)/$#s', $pathinfo, $matches)) {
             $matches['_route'] = 'baz4';
             return $matches;
         }
 
         // baz5
-        if (0 === strpos($pathinfo, '/test') && preg_match('#^/test/(?P<foo>[^/]+?)/$#xs', $pathinfo, $matches)) {
+        if (0 === strpos($pathinfo, '/test') && preg_match('#^/test/(?P<foo>[^/]+?)/$#s', $pathinfo, $matches)) {
             if ($this->context->getMethod() != 'POST') {
                 $allow[] = 'POST';
                 goto not_baz5;
@@ -85,7 +85,7 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
         not_baz5:
 
         // baz.baz6
-        if (0 === strpos($pathinfo, '/test') && preg_match('#^/test/(?P<foo>[^/]+?)/$#xs', $pathinfo, $matches)) {
+        if (0 === strpos($pathinfo, '/test') && preg_match('#^/test/(?P<foo>[^/]+?)/$#s', $pathinfo, $matches)) {
             if ($this->context->getMethod() != 'PUT') {
                 $allow[] = 'PUT';
                 goto not_bazbaz6;
@@ -101,33 +101,38 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
         }
 
         // quoter
-        if (preg_match('#^/(?P<quoter>[\']+)$#xs', $pathinfo, $matches)) {
+        if (preg_match('#^/(?P<quoter>[\']+)$#s', $pathinfo, $matches)) {
             $matches['_route'] = 'quoter';
             return $matches;
         }
 
+        // space
+        if ($pathinfo === '/spa ce') {
+            return array('_route' => 'space');
+        }
+
         if (0 === strpos($pathinfo, '/a')) {
             if (0 === strpos($pathinfo, '/a/b\'b')) {
                 // foo1
-                if (preg_match('#^/a/b\'b/(?P<foo>[^/]+?)$#xs', $pathinfo, $matches)) {
+                if (preg_match('#^/a/b\'b/(?P<foo>[^/]+?)$#s', $pathinfo, $matches)) {
                     $matches['_route'] = 'foo1';
                     return $matches;
                 }
 
                 // bar1
-                if (preg_match('#^/a/b\'b/(?P<bar>[^/]+?)$#xs', $pathinfo, $matches)) {
+                if (preg_match('#^/a/b\'b/(?P<bar>[^/]+?)$#s', $pathinfo, $matches)) {
                     $matches['_route'] = 'bar1';
                     return $matches;
                 }
 
                 // foo2
-                if (preg_match('#^/a/b\'b/(?P<foo1>[^/]+?)$#xs', $pathinfo, $matches)) {
+                if (preg_match('#^/a/b\'b/(?P<foo1>[^/]+?)$#s', $pathinfo, $matches)) {
                     $matches['_route'] = 'foo2';
                     return $matches;
                 }
 
                 // bar2
-                if (preg_match('#^/a/b\'b/(?P<bar1>[^/]+?)$#xs', $pathinfo, $matches)) {
+                if (preg_match('#^/a/b\'b/(?P<bar1>[^/]+?)$#s', $pathinfo, $matches)) {
                     $matches['_route'] = 'bar2';
                     return $matches;
                 }
@@ -145,7 +150,7 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
             }
 
             // foo4
-            if (preg_match('#^/aba/(?P<foo>[^/]+?)$#xs', $pathinfo, $matches)) {
+            if (preg_match('#^/aba/(?P<foo>[^/]+?)$#s', $pathinfo, $matches)) {
                 $matches['_route'] = 'foo4';
                 return $matches;
             }
@@ -153,13 +158,13 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
         }
 
         // foo3
-        if (preg_match('#^/(?P<_locale>[^/]+?)/b/(?P<foo>[^/]+?)$#xs', $pathinfo, $matches)) {
+        if (preg_match('#^/(?P<_locale>[^/]+?)/b/(?P<foo>[^/]+?)$#s', $pathinfo, $matches)) {
             $matches['_route'] = 'foo3';
             return $matches;
         }
 
         // bar3
-        if (preg_match('#^/(?P<_locale>[^/]+?)/b/(?P<bar>[^/]+?)$#xs', $pathinfo, $matches)) {
+        if (preg_match('#^/(?P<_locale>[^/]+?)/b/(?P<bar>[^/]+?)$#s', $pathinfo, $matches)) {
             $matches['_route'] = 'bar3';
             return $matches;
         }

+ 19 - 14
tests/Symfony/Tests/Component/Routing/Fixtures/dumper/url_matcher2.php

@@ -26,12 +26,12 @@ class ProjectUrlMatcher extends Symfony\Tests\Component\Routing\Fixtures\Redirec
         $pathinfo = urldecode($pathinfo);
 
         // foo
-        if (0 === strpos($pathinfo, '/foo') && preg_match('#^/foo/(?P<bar>baz|symfony)$#xs', $pathinfo, $matches)) {
+        if (0 === strpos($pathinfo, '/foo') && preg_match('#^/foo/(?P<bar>baz|symfony)$#s', $pathinfo, $matches)) {
             return array_merge($this->mergeDefaults($matches, array (  'def' => 'test',)), array('_route' => 'foo'));
         }
 
         // bar
-        if (0 === strpos($pathinfo, '/bar') && preg_match('#^/bar/(?P<foo>[^/]+?)$#xs', $pathinfo, $matches)) {
+        if (0 === strpos($pathinfo, '/bar') && preg_match('#^/bar/(?P<foo>[^/]+?)$#s', $pathinfo, $matches)) {
             if (!in_array($this->context->getMethod(), array('GET', 'HEAD'))) {
                 $allow = array_merge($allow, array('GET', 'HEAD'));
                 goto not_bar;
@@ -42,7 +42,7 @@ class ProjectUrlMatcher extends Symfony\Tests\Component\Routing\Fixtures\Redirec
         not_bar:
 
         // barhead
-        if (0 === strpos($pathinfo, '/barhead') && preg_match('#^/barhead/(?P<foo>[^/]+?)$#xs', $pathinfo, $matches)) {
+        if (0 === strpos($pathinfo, '/barhead') && preg_match('#^/barhead/(?P<foo>[^/]+?)$#s', $pathinfo, $matches)) {
             if (!in_array($this->context->getMethod(), array('GET', 'HEAD'))) {
                 $allow = array_merge($allow, array('GET', 'HEAD'));
                 goto not_barhead;
@@ -71,7 +71,7 @@ class ProjectUrlMatcher extends Symfony\Tests\Component\Routing\Fixtures\Redirec
         }
 
         // baz4
-        if (0 === strpos($pathinfo, '/test') && preg_match('#^/test/(?P<foo>[^/]+?)/?$#xs', $pathinfo, $matches)) {
+        if (0 === strpos($pathinfo, '/test') && preg_match('#^/test/(?P<foo>[^/]+?)/?$#s', $pathinfo, $matches)) {
             if (substr($pathinfo, -1) !== '/') {
                 return $this->redirect($pathinfo.'/', 'baz4');
             }
@@ -80,7 +80,7 @@ class ProjectUrlMatcher extends Symfony\Tests\Component\Routing\Fixtures\Redirec
         }
 
         // baz5
-        if (0 === strpos($pathinfo, '/test') && preg_match('#^/test/(?P<foo>[^/]+?)/?$#xs', $pathinfo, $matches)) {
+        if (0 === strpos($pathinfo, '/test') && preg_match('#^/test/(?P<foo>[^/]+?)/?$#s', $pathinfo, $matches)) {
             if ($this->context->getMethod() != 'POST') {
                 $allow[] = 'POST';
                 goto not_baz5;
@@ -94,7 +94,7 @@ class ProjectUrlMatcher extends Symfony\Tests\Component\Routing\Fixtures\Redirec
         not_baz5:
 
         // baz.baz6
-        if (0 === strpos($pathinfo, '/test') && preg_match('#^/test/(?P<foo>[^/]+?)/?$#xs', $pathinfo, $matches)) {
+        if (0 === strpos($pathinfo, '/test') && preg_match('#^/test/(?P<foo>[^/]+?)/?$#s', $pathinfo, $matches)) {
             if ($this->context->getMethod() != 'PUT') {
                 $allow[] = 'PUT';
                 goto not_bazbaz6;
@@ -113,33 +113,38 @@ class ProjectUrlMatcher extends Symfony\Tests\Component\Routing\Fixtures\Redirec
         }
 
         // quoter
-        if (preg_match('#^/(?P<quoter>[\']+)$#xs', $pathinfo, $matches)) {
+        if (preg_match('#^/(?P<quoter>[\']+)$#s', $pathinfo, $matches)) {
             $matches['_route'] = 'quoter';
             return $matches;
         }
 
+        // space
+        if ($pathinfo === '/spa ce') {
+            return array('_route' => 'space');
+        }
+
         if (0 === strpos($pathinfo, '/a')) {
             if (0 === strpos($pathinfo, '/a/b\'b')) {
                 // foo1
-                if (preg_match('#^/a/b\'b/(?P<foo>[^/]+?)$#xs', $pathinfo, $matches)) {
+                if (preg_match('#^/a/b\'b/(?P<foo>[^/]+?)$#s', $pathinfo, $matches)) {
                     $matches['_route'] = 'foo1';
                     return $matches;
                 }
 
                 // bar1
-                if (preg_match('#^/a/b\'b/(?P<bar>[^/]+?)$#xs', $pathinfo, $matches)) {
+                if (preg_match('#^/a/b\'b/(?P<bar>[^/]+?)$#s', $pathinfo, $matches)) {
                     $matches['_route'] = 'bar1';
                     return $matches;
                 }
 
                 // foo2
-                if (preg_match('#^/a/b\'b/(?P<foo1>[^/]+?)$#xs', $pathinfo, $matches)) {
+                if (preg_match('#^/a/b\'b/(?P<foo1>[^/]+?)$#s', $pathinfo, $matches)) {
                     $matches['_route'] = 'foo2';
                     return $matches;
                 }
 
                 // bar2
-                if (preg_match('#^/a/b\'b/(?P<bar1>[^/]+?)$#xs', $pathinfo, $matches)) {
+                if (preg_match('#^/a/b\'b/(?P<bar1>[^/]+?)$#s', $pathinfo, $matches)) {
                     $matches['_route'] = 'bar2';
                     return $matches;
                 }
@@ -157,7 +162,7 @@ class ProjectUrlMatcher extends Symfony\Tests\Component\Routing\Fixtures\Redirec
             }
 
             // foo4
-            if (preg_match('#^/aba/(?P<foo>[^/]+?)$#xs', $pathinfo, $matches)) {
+            if (preg_match('#^/aba/(?P<foo>[^/]+?)$#s', $pathinfo, $matches)) {
                 $matches['_route'] = 'foo4';
                 return $matches;
             }
@@ -165,13 +170,13 @@ class ProjectUrlMatcher extends Symfony\Tests\Component\Routing\Fixtures\Redirec
         }
 
         // foo3
-        if (preg_match('#^/(?P<_locale>[^/]+?)/b/(?P<foo>[^/]+?)$#xs', $pathinfo, $matches)) {
+        if (preg_match('#^/(?P<_locale>[^/]+?)/b/(?P<foo>[^/]+?)$#s', $pathinfo, $matches)) {
             $matches['_route'] = 'foo3';
             return $matches;
         }
 
         // bar3
-        if (preg_match('#^/(?P<_locale>[^/]+?)/b/(?P<bar>[^/]+?)$#xs', $pathinfo, $matches)) {
+        if (preg_match('#^/(?P<_locale>[^/]+?)/b/(?P<bar>[^/]+?)$#s', $pathinfo, $matches)) {
             $matches['_route'] = 'bar3';
             return $matches;
         }

+ 34 - 4
tests/Symfony/Tests/Component/Routing/Matcher/Dumper/ApacheMatcherDumperTest.php

@@ -25,6 +25,34 @@ class ApacheMatcherDumperTest extends \PHPUnit_Framework_TestCase
     }
 
     public function testDump()
+    {
+        $dumper = new ApacheMatcherDumper($this->getRouteCollection());
+
+        $this->assertStringEqualsFile(self::$fixturesPath.'/dumper/url_matcher1.apache', $dumper->dump(), '->dump() dumps basic routes to the correct apache format.');
+    }
+
+    /**
+     * @dataProvider provideEscapeFixtures
+     */
+    public function testEscape($src, $dest, $char, $with, $message)
+    {
+        $r = new \ReflectionMethod(new ApacheMatcherDumper($this->getRouteCollection()), 'escape');
+        $r->setAccessible(true);
+        $this->assertEquals($dest, $r->invoke(null, $src, $char, $with), $message);
+    }
+
+    public function provideEscapeFixtures()
+    {
+        return array(
+            array('foo', 'foo', ' ', '-', 'Preserve string that should not be escaped'),
+            array('fo-o', 'fo-o', ' ', '-', 'Preserve string that should not be escaped'),
+            array('fo o', 'fo- o', ' ', '-', 'Escape special characters'),
+            array('fo-- o', 'fo--- o', ' ', '-', 'Escape special characters'),
+            array('fo- o', 'fo- o', ' ', '-', 'Do not escape already escaped string'),
+        );
+    }
+
+    private function getRouteCollection()
     {
         $collection = new RouteCollection();
 
@@ -73,9 +101,11 @@ class ApacheMatcherDumperTest extends \PHPUnit_Framework_TestCase
             '/test/baz',
             array('foo' => 'bar baz')
         ));
+        // space in path
+        $collection->add('baz7', new Route(
+            '/te st/baz'
+        ));
 
-        $dumper = new ApacheMatcherDumper($collection);
-
-        $this->assertStringEqualsFile(self::$fixturesPath.'/dumper/url_matcher1.apache', $dumper->dump(), '->dump() dumps basic routes to the correct apache format.');
+        return $collection;
     }
-}
+}

+ 4 - 0
tests/Symfony/Tests/Component/Routing/Matcher/Dumper/PhpMatcherDumperTest.php

@@ -123,6 +123,10 @@ class PhpMatcherDumperTest extends \PHPUnit_Framework_TestCase
             array(),
             array('quoter' => '[\']+')
         ));
+        // space in pattern
+        $collection->add('space', new Route(
+            '/spa ce'
+        ));
 
         // prefixes
         $collection1 = new RouteCollection();

+ 9 - 9
tests/Symfony/Tests/Component/Routing/RouteCompilerTest.php

@@ -25,7 +25,7 @@ class RouteCompilerTest extends \PHPUnit_Framework_TestCase
 
         $compiled = $route->compile();
         $this->assertEquals($prefix, $compiled->getStaticPrefix(), $name.' (static prefix)');
-        $this->assertEquals($regex, str_replace(array("\n", ' '), '', $compiled->getRegex()), $name.' (regex)');
+        $this->assertEquals($regex, $compiled->getRegex(), $name.' (regex)');
         $this->assertEquals($variables, $compiled->getVariables(), $name.' (variables)');
         $this->assertEquals($tokens, $compiled->getTokens(), $name.' (tokens)');
     }
@@ -36,14 +36,14 @@ class RouteCompilerTest extends \PHPUnit_Framework_TestCase
             array(
                 'Static route',
                 array('/foo'),
-                '/foo', '#^/foo$#xs', array(), array(
+                '/foo', '#^/foo$#s', array(), array(
                     array('text', '/foo'),
                 )),
 
             array(
                 'Route with a variable',
                 array('/foo/{bar}'),
-                '/foo', '#^/foo/(?P<bar>[^/]+?)$#xs', array('bar'), array(
+                '/foo', '#^/foo/(?P<bar>[^/]+?)$#s', array('bar'), array(
                     array('variable', '/', '[^/]+?', 'bar'),
                     array('text', '/foo'),
                 )),
@@ -51,7 +51,7 @@ class RouteCompilerTest extends \PHPUnit_Framework_TestCase
             array(
                 'Route with a variable that has a default value',
                 array('/foo/{bar}', array('bar' => 'bar')),
-                '/foo', '#^/foo(?:/(?P<bar>[^/]+?))?$#xs', array('bar'), array(
+                '/foo', '#^/foo(?:/(?P<bar>[^/]+?))?$#s', array('bar'), array(
                     array('variable', '/', '[^/]+?', 'bar'),
                     array('text', '/foo'),
                 )),
@@ -59,7 +59,7 @@ class RouteCompilerTest extends \PHPUnit_Framework_TestCase
             array(
                 'Route with several variables',
                 array('/foo/{bar}/{foobar}'),
-                '/foo', '#^/foo/(?P<bar>[^/]+?)/(?P<foobar>[^/]+?)$#xs', array('bar', 'foobar'), array(
+                '/foo', '#^/foo/(?P<bar>[^/]+?)/(?P<foobar>[^/]+?)$#s', array('bar', 'foobar'), array(
                     array('variable', '/', '[^/]+?', 'foobar'),
                     array('variable', '/', '[^/]+?', 'bar'),
                     array('text', '/foo'),
@@ -68,7 +68,7 @@ class RouteCompilerTest extends \PHPUnit_Framework_TestCase
             array(
                 'Route with several variables that have default values',
                 array('/foo/{bar}/{foobar}', array('bar' => 'bar', 'foobar' => '')),
-                '/foo', '#^/foo(?:/(?P<bar>[^/]+?)(?:/(?P<foobar>[^/]+?))?)?$#xs', array('bar', 'foobar'), array(
+                '/foo', '#^/foo(?:/(?P<bar>[^/]+?)(?:/(?P<foobar>[^/]+?))?)?$#s', array('bar', 'foobar'), array(
                     array('variable', '/', '[^/]+?', 'foobar'),
                     array('variable', '/', '[^/]+?', 'bar'),
                     array('text', '/foo'),
@@ -77,7 +77,7 @@ class RouteCompilerTest extends \PHPUnit_Framework_TestCase
             array(
                 'Route with several variables but some of them have no default values',
                 array('/foo/{bar}/{foobar}', array('bar' => 'bar')),
-                '/foo', '#^/foo/(?P<bar>[^/]+?)/(?P<foobar>[^/]+?)$#xs', array('bar', 'foobar'), array(
+                '/foo', '#^/foo/(?P<bar>[^/]+?)/(?P<foobar>[^/]+?)$#s', array('bar', 'foobar'), array(
                     array('variable', '/', '[^/]+?', 'foobar'),
                     array('variable', '/', '[^/]+?', 'bar'),
                     array('text', '/foo'),
@@ -86,14 +86,14 @@ class RouteCompilerTest extends \PHPUnit_Framework_TestCase
             array(
                 'Route with an optional variable as the first segment',
                 array('/{bar}', array('bar' => 'bar')),
-                '', '#^/(?:(?P<bar>[^/]+?))?$#xs', array('bar'), array(
+                '', '#^/(?P<bar>[^/]+?)?$#s', array('bar'), array(
                     array('variable', '/', '[^/]+?', 'bar'),
                 )),
 
             array(
                 'Route with an optional variable as the first segment with requirements',
                 array('/{bar}', array('bar' => 'bar'), array('bar' => '(foo|bar)')),
-                '', '#^/(?:(?P<bar>(foo|bar)))?$#xs', array('bar'), array(
+                '', '#^/(?P<bar>(foo|bar))?$#s', array('bar'), array(
                     array('variable', '/', '(foo|bar)', 'bar'),
                 )),
         );