Skip to content
This repository
Browse code

merged branch arnaud-lb/hostname-routes (PR #3378)

This PR was merged into the master branch.

Commits
-------

17f51a1 Merge pull request #6 from Tobion/hostname-routes
e120a7a fix API of RouteCollection
26e5684 some type fixes
514e27a [Routing] fix PhpMatcherDumper that returned numeric-indexed params that are returned besides named placeholders by preg_match
7ed3013 switch to array_replace instead of array_merge
94ec653 removed irrelevant string case in XmlFileLoader
9ffe3de synchronize the fixtures in different formats and fix default for numeric requirement
6cd3457 fixed CS
8366b8a [Routing] fixed validity check for hostname params in UrlGenerator
a8ce621 [Routing] added support for hostname in the apache matcher dumper
562174a [Routing] fixed indentation of dumped collections
1489021 fixed CS
a270458 [Routing] added some more unit tests
153fcf2 [Routing] added some unit tests for the PHP loader
68da6ad [Routing] added support for hostname in the XML loader
3dfca47 [Routing] added some unit tests for the YAML loader
92f9c15 [Routing] changed CompiledRoute signature to be more consistent
d91e5a2 [Routing] fixed Route annotation for hostname (should be hostname_pattern instead of hostnamePattern)
62de881 [Routing] clarified a variable content
11b4378 [Routing] added hostname support in UrlMatcher
fc015d5 [Routing] fixed route generation with a hostname pattern when the hostname is the same as the current one (no need to force the generated URL to be absolute)
462999d [Routing] display hostname pattern in router:debug output
805806a [Routing] added hostname matching support to UrlGenerator
7a15e00 [Routing] added hostname matching support to AnnotationClassLoader
cab450c [Routing] added hostname matching support to YamlFileLoader
85d11af [Routing] added hostname matching support to PhpMatcherDumper
402359b [Routing] added hostname matching support to RouteCompiler
add3658 [Routing] added hostname matching support to Route and RouteCollection
23feb37 [Routing] added hostname matching support to CompiledRoute

Discussion
----------

[2.2][Routing] hostname pattern for routes

Bug fix: no
Feature addition: yes
Fixes the following tickets: #1762, #3276
Backwards compatibility break: no
Symfony2 tests pass: yes

This adds a hostname_pattern property to routes. It works like the pattern property (hostname_pattern can have variables, requirements, etc). The hostname_pattern property can be set on both routes and route collections.

Yaml example:

``` yaml
# Setting the hostname_pattern for a whole collection of routes

AcmeBundle:
    resource: "@AcmeBundle/Controller/"
    type: annotation
    prefix: /
    hostname_pattern: {locale}.example.com
    requirements:
        locale: en|fr

# Setting the hostname_pattern for single route

some_route:
    pattern: /hello/{name}
    hostname_pattern: {locale}.example.com
    requirements:
        locale: en|fr
        name: \w+
    defaults:
        _controller: Foo:bar:baz
```

Annotations example:

``` php
<?php

/**
 * Inherits requirements and hostname pattern from the collection
 * @Route("/foo")
 */
public function fooAction();

/**
 * Set a specific hostnamePattern for this route only
 * @Route("/foo", hostnamePattern="{_locale}.example.com", requirements={"_locale="fr|en"})
 */
public function fooAction();

```

Performance:

Consecutive routes with the same hostname pattern are grouped, and a single test is made against the hostname for this group, so the overhead is very low:

```
@Route("/foo", hostnamePattern="a.example.com")
@Route("/bar", hostnamePattern="a.example.com")
@Route("/baz", hostnamePattern="b.example.com")
```

is compiled like this:

```
if (hostname matches a.example.com) {
    // test route "/foo"
    // test route "/bar"
}
if (hostname matches b.example.com) {
    // test route "/baz"
}
```

The PR also tries harder to optimize routes sharing the same prefix:

```
@Route("/cafe")
@Route("/cacao")
@Route("/coca")
```

is compiled like this:

```
if (url starts with /c) {
    if (url starts with /ca) {
        // test route "/cafe"
        // test route "/cacao"
    }
    // test route "/coca"
}
```

---------------------------------------------------------------------------

by Koc at 2012-02-16T14:14:19Z

Interesting. Have you looked at #3057, #3002?

Killer feature of #3057 : multiple hostnames per route.

---------------------------------------------------------------------------

by arnaud-lb at 2012-02-16T14:21:28Z

@Koc yes, the main difference is that this PR allows variables in the hostname pattern, with requirements, etc just like the path pattern. The other PRs use a `_host` requirement, which works like the `_method` requirement (takes a list of allowed hostnames separated by `|`).

> Killer feature of #3057 : multiple hostnames per route.

If you have multiple tlds you can easily do it like this:

``` yaml
hostbased_route:
  pattern:  /
  hostname_pattern: symfony.{tld}
  requirements:
     tld: org|com
```

Or with completely different domain names:

``` yaml
hostbased_route:
  pattern:  /
  hostname_pattern: {domain}
  requirements:
     domain: example\.com|symfony\.com
```

Requirements allow DIC %parameters%, so you can also put you domains in your config.yml.

---------------------------------------------------------------------------

by Koc at 2012-02-16T15:52:16Z

wow, nice! So looks like this PR closes my #3276 ticket?

---------------------------------------------------------------------------

by arnaud-lb at 2012-02-16T15:53:55Z

Yes, apparently :)

---------------------------------------------------------------------------

by Koc at 2012-02-16T15:56:53Z

I cann't find method `ParameterBag::resolveValue` calling in this PR, like here https://github.com/symfony/symfony/pull/3316/files

---------------------------------------------------------------------------

by arnaud-lb at 2012-02-16T16:03:48Z

I think it's in core already

---------------------------------------------------------------------------

by Koc at 2012-02-16T16:11:38Z

looks like yes
https://github.com/symfony/symfony/blob/master/src/Symfony/Bundle/FrameworkBundle/Routing/Router.php#L81

---------------------------------------------------------------------------

by dlsniper at 2012-02-16T19:37:57Z

This PR looks great, it's something like this I've been waiting for.

I know @fabpot said he's working on something similar but I think if he agrees with this it could be a great addition to the core.

@fabpot , @stof any objections about this PR if gets fully done?

---------------------------------------------------------------------------

by stof at 2012-02-16T20:00:21Z

Well, we already have 2 other implementations for this stuff in the PRs. @fabpot please take time to look at them

---------------------------------------------------------------------------

by stof at 2012-02-16T20:03:17Z

This one is absolutely not tested and seems to break the existing tests according to the description. So it cannot be reviewed as is.

---------------------------------------------------------------------------

by dlsniper at 2012-02-16T22:00:24Z

@stof I understand it's a WIP but the other PRs where ignored as well and like you've said, there's a bunch of PRs already on this issue all doing a thing or another. So an early feedback on this, or any other, could lead it to the right path in order to finally solve this issue.

---------------------------------------------------------------------------

by arnaud-lb at 2012-02-17T23:57:28Z

Added tests; others are passing now

---------------------------------------------------------------------------

by arnaud-lb at 2012-02-22T21:10:20Z

I'm going to add support for the Apache dumper and the XML loader; does this PR have a chance to be merged ? cc @fabpot @stof

---------------------------------------------------------------------------

by stof at 2012-02-22T22:05:23Z

@arnaud-lb We need to wait @fabpot's mind about the way he prefers to implement it to know which one can be merged.

---------------------------------------------------------------------------

by IjinPL at 2012-02-27T02:01:57Z

Forked @arnaud-lb *hostname_pattern* to add XML parasing support.

---------------------------------------------------------------------------

by stof at 2012-04-03T23:59:12Z

@arnaud-lb Please rebase your branch. It conflicts with master because of the move of the tests

@fabpot @vicb ping

---------------------------------------------------------------------------

by dlsniper at 2012-04-13T19:52:23Z

Hi,

If @arnaud-lb won't be able to rebase this I could help with some work on this but there's still the problem of actually choosing the right PR(s) for this issue. @blogsh says in his last commit that this PR is a bit better in his opinion but @fabpot needs to decide in the end.

---------------------------------------------------------------------------

by arnaud-lb at 2012-04-14T17:26:55Z

@stof rebased

---------------------------------------------------------------------------

by nomack84 at 2012-04-20T13:01:00Z

@fabpot Any final word about this pull request? It would be nice to have this feature ready for 2.1.

---------------------------------------------------------------------------

by asm89 at 2012-04-24T21:27:50Z

Using the `{_locale}` placeholder in the host would set the locale for the request just like it does now?

Another thing I'm wondering is how/if it should be possible to set the hostname pattern for all your routes, or at least when importing routes? Otherwise you'll end up repeating the same host pattern over and over again. I think this is also important when importing routes from third party bundles.

---------------------------------------------------------------------------

by fabpot at 2012-04-25T01:17:51Z

I'm reviewing this PR and I'm going to make some modifications. I will send a PR to @arnaud-lb soon.

---------------------------------------------------------------------------

by fabpot at 2012-04-25T03:10:18Z

I've sent a PR to @arnaud-lb arnaud-lb/symfony#3 that fixes some minor bugs and add support in more classes.

---------------------------------------------------------------------------

by fabpot at 2012-04-25T03:12:52Z

@asm89:

Placeholders in the hostname are managed in the same way as the ones from the URL pattern.

You can set a hostname pattern for a collection (like the prefix for URL patterns).

---------------------------------------------------------------------------

by Tobion at 2012-04-25T09:31:19Z

I think we need to change the contents of $variables, $tokens, and $hostnameTokens in the CompiledRoute. They contain redundant information and the content structure of these variables ist not documentation in any way. If we remove duplicated content and put it in a (single) well defined variable, it would also reduce the information that need to be saved in the generated class by the UrlGeneratorDumper.

---------------------------------------------------------------------------

by arnaud-lb at 2012-04-26T08:54:21Z

@fabpot thanks :) I've merged it

---------------------------------------------------------------------------

by stof at 2012-04-26T12:08:40Z

A rebase is needed

---------------------------------------------------------------------------

by fabpot at 2012-04-26T13:28:08Z

no need to rebase, I will resolve the conflicts when merging. I've still have some minor changes to do before merging though. Anyone willing to have a look at implementing the Apache dumper part?

---------------------------------------------------------------------------

by Tobion at 2012-04-26T14:59:00Z

@fabpot you want to merge this for 2.1 although it introduces big changes that need extensive review and testing? But #3958 is not considered for 2.1? I thought we are in some sort of feature freeze for the components in order to not postpone the release.

---------------------------------------------------------------------------

by fabpot at 2012-04-26T17:21:09Z

@Tobion: I never said it will be in 2.1. The plan is to create a 2.1 branch soon so that we can continue working on 2.2.

---------------------------------------------------------------------------

by Koc at 2012-04-26T19:46:43Z

https://twitter.com/#!/fabpot/status/178502663690915840
  • Loading branch information...
commit c94bdf6bdfefd3e81049a218bb3c2f7e6c2b941f 2 parents f4c05e3 + 17f51a1
Fabien Potencier fabpot authored

Showing 40 changed files with 1,210 additions and 239 deletions. Show diff stats Hide diff stats

  1. +15 14 src/Symfony/Bundle/FrameworkBundle/Command/RouterDebugCommand.php
  2. +12 1 src/Symfony/Component/Routing/Annotation/Route.php
  3. +59 6 src/Symfony/Component/Routing/CompiledRoute.php
  4. +3 2 src/Symfony/Component/Routing/Generator/Dumper/PhpGeneratorDumper.php
  5. +34 4 src/Symfony/Component/Routing/Generator/UrlGenerator.php
  6. +19 9 src/Symfony/Component/Routing/Loader/AnnotationClassLoader.php
  7. +13 12 src/Symfony/Component/Routing/Loader/XmlFileLoader.php
  8. +5 3 src/Symfony/Component/Routing/Loader/YamlFileLoader.php
  9. +2 0  src/Symfony/Component/Routing/Loader/schema/routing/routing-1.0.xsd
  10. +71 7 src/Symfony/Component/Routing/Matcher/Dumper/ApacheMatcherDumper.php
  11. +48 1 src/Symfony/Component/Routing/Matcher/Dumper/DumperCollection.php
  12. +86 19 src/Symfony/Component/Routing/Matcher/Dumper/PhpMatcherDumper.php
  13. +6 1 src/Symfony/Component/Routing/Matcher/UrlMatcher.php
  14. +26 1 src/Symfony/Component/Routing/Route.php
  15. +38 6 src/Symfony/Component/Routing/RouteCollection.php
  16. +64 12 src/Symfony/Component/Routing/RouteCompiler.php
  17. +2 1  src/Symfony/Component/Routing/Tests/Annotation/RouteTest.php
  18. +5 5 src/Symfony/Component/Routing/Tests/CompiledRouteTest.php
  19. +91 3 src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher1.apache
  20. +114 55 src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher1.php
  21. +114 55 src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher2.php
  22. +1 3 src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher3.php
  23. +3 2 src/Symfony/Component/Routing/Tests/Fixtures/validpattern.php
  24. +2 1  src/Symfony/Component/Routing/Tests/Fixtures/validpattern.xml
  25. +4 2 src/Symfony/Component/Routing/Tests/Fixtures/validpattern.yml
  26. +2 2 src/Symfony/Component/Routing/Tests/Fixtures/validresource.xml
  27. +6 5 src/Symfony/Component/Routing/Tests/Fixtures/validresource.yml
  28. +58 2 src/Symfony/Component/Routing/Tests/Generator/UrlGeneratorTest.php
  29. +4 0 src/Symfony/Component/Routing/Tests/Loader/PhpFileLoaderTest.php
  30. +9 1 src/Symfony/Component/Routing/Tests/Loader/XmlFileLoaderTest.php
  31. +9 1 src/Symfony/Component/Routing/Tests/Loader/YamlFileLoaderTest.php
  32. +14 0 src/Symfony/Component/Routing/Tests/Matcher/ApacheUrlMatcherTest.php
  33. +65 0 src/Symfony/Component/Routing/Tests/Matcher/Dumper/ApacheMatcherDumperTest.php
  34. +24 0 src/Symfony/Component/Routing/Tests/Matcher/Dumper/DumperCollectionTest.php
  35. +0 1  src/Symfony/Component/Routing/Tests/Matcher/Dumper/DumperPrefixCollectionTest.php
  36. +53 1 src/Symfony/Component/Routing/Tests/Matcher/Dumper/PhpMatcherDumperTest.php
  37. +35 0 src/Symfony/Component/Routing/Tests/Matcher/UrlMatcherTest.php
  38. +14 0 src/Symfony/Component/Routing/Tests/RouteCollectionTest.php
  39. +71 0 src/Symfony/Component/Routing/Tests/RouteCompilerTest.php
  40. +9 1 src/Symfony/Component/Routing/Tests/RouteTest.php
29 src/Symfony/Bundle/FrameworkBundle/Command/RouterDebugCommand.php
@@ -15,7 +15,6 @@
15 15 use Symfony\Component\Console\Input\InputInterface;
16 16 use Symfony\Component\Console\Output\OutputInterface;
17 17 use Symfony\Component\Routing\RouterInterface;
18   -use Symfony\Component\Console\Output\Output;
19 18
20 19 /**
21 20 * A console command for retrieving information about routes
@@ -82,8 +81,10 @@ protected function outputRoutes(OutputInterface $output, $routes = null)
82 81
83 82 $output->writeln($this->getHelper('formatter')->formatSection('router', 'Current routes'));
84 83
85   - $maxName = 4;
86   - $maxMethod = 6;
  84 + $maxName = strlen('name');
  85 + $maxMethod = strlen('method');
  86 + $maxHostname = strlen('hostname');
  87 +
87 88 foreach ($routes as $name => $route) {
88 89 $requirements = $route->getRequirements();
89 90 $method = isset($requirements['_method'])
@@ -91,20 +92,18 @@ protected function outputRoutes(OutputInterface $output, $routes = null)
91 92 ? implode(', ', $requirements['_method']) : $requirements['_method']
92 93 )
93 94 : 'ANY';
  95 + $hostname = '' !== $route->getHostnamePattern()
  96 + ? $route->getHostnamePattern() : 'ANY';
94 97
95   - if (strlen($name) > $maxName) {
96   - $maxName = strlen($name);
97   - }
98   -
99   - if (strlen($method) > $maxMethod) {
100   - $maxMethod = strlen($method);
101   - }
  98 + $maxName = max($maxName, strlen($name));
  99 + $maxMethod = max($maxMethod, strlen($method));
  100 + $maxHostname = max($maxHostname, strlen($hostname));
102 101 }
103   - $format = '%-'.$maxName.'s %-'.$maxMethod.'s %s';
  102 + $format = '%-'.$maxName.'s %-'.$maxMethod.'s %-'.$maxHostname.'s %s';
104 103
105 104 // displays the generated routes
106   - $format1 = '%-'.($maxName + 19).'s %-'.($maxMethod + 19).'s %s';
107   - $output->writeln(sprintf($format1, '<comment>Name</comment>', '<comment>Method</comment>', '<comment>Pattern</comment>'));
  105 + $format1 = '%-'.($maxName + 19).'s %-'.($maxMethod + 19).'s %-'.($maxHostname + 19).'s %s';
  106 + $output->writeln(sprintf($format1, '<comment>Name</comment>', '<comment>Method</comment>', '<comment>Hostname</comment>', '<comment>Pattern</comment>'));
108 107 foreach ($routes as $name => $route) {
109 108 $requirements = $route->getRequirements();
110 109 $method = isset($requirements['_method'])
@@ -112,7 +111,9 @@ protected function outputRoutes(OutputInterface $output, $routes = null)
112 111 ? implode(', ', $requirements['_method']) : $requirements['_method']
113 112 )
114 113 : 'ANY';
115   - $output->writeln(sprintf($format, $name, $method, $route->getPattern()));
  114 + $hostname = '' !== $route->getHostnamePattern()
  115 + ? $route->getHostnamePattern() : 'ANY';
  116 + $output->writeln(sprintf($format, $name, $method, $hostname, $route->getPattern()));
116 117 }
117 118 }
118 119
13 src/Symfony/Component/Routing/Annotation/Route.php
@@ -25,6 +25,7 @@ class Route
25 25 private $requirements;
26 26 private $options;
27 27 private $defaults;
  28 + private $hostnamePattern;
28 29
29 30 /**
30 31 * Constructor.
@@ -43,7 +44,7 @@ public function __construct(array $data)
43 44 }
44 45
45 46 foreach ($data as $key => $value) {
46   - $method = 'set'.$key;
  47 + $method = 'set'.str_replace('_', '', $key);
47 48 if (!method_exists($this, $method)) {
48 49 throw new \BadMethodCallException(sprintf("Unknown property '%s' on annotation '%s'.", $key, get_class($this)));
49 50 }
@@ -61,6 +62,16 @@ public function getPattern()
61 62 return $this->pattern;
62 63 }
63 64
  65 + public function setHostnamePattern($pattern)
  66 + {
  67 + $this->hostnamePattern = $pattern;
  68 + }
  69 +
  70 + public function getHostnamePattern()
  71 + {
  72 + return $this->hostnamePattern;
  73 + }
  74 +
64 75 public function setName($name)
65 76 {
66 77 $this->name = $name;
65 src/Symfony/Component/Routing/CompiledRoute.php
@@ -22,20 +22,32 @@ class CompiledRoute
22 22 private $tokens;
23 23 private $staticPrefix;
24 24 private $regex;
  25 + private $pathVariables;
  26 + private $hostnameVariables;
  27 + private $hostnameRegex;
  28 + private $hostnameTokens;
25 29
26 30 /**
27 31 * Constructor.
28 32 *
29   - * @param string $staticPrefix The static prefix of the compiled route
30   - * @param string $regex The regular expression to use to match this route
31   - * @param array $tokens An array of tokens to use to generate URL for this route
32   - * @param array $variables An array of variables
  33 + * @param string $staticPrefix The static prefix of the compiled route
  34 + * @param string $regex The regular expression to use to match this route
  35 + * @param array $tokens An array of tokens to use to generate URL for this route
  36 + * @param array $pathVariables An array of path variables
  37 + * @param string|null $hostnameRegex Hostname regex
  38 + * @param array $hostnameTokens Hostname tokens
  39 + * @param array $hostnameVariables An array of hostname variables
  40 + * @param array $variables An array of variables (variables defined in the path and in the hostname patterns)
33 41 */
34   - public function __construct($staticPrefix, $regex, array $tokens, array $variables)
  42 + public function __construct($staticPrefix, $regex, array $tokens, array $pathVariables, $hostnameRegex = null, array $hostnameTokens = array(), array $hostnameVariables = array(), array $variables = array())
35 43 {
36   - $this->staticPrefix = $staticPrefix;
  44 + $this->staticPrefix = (string) $staticPrefix;
37 45 $this->regex = $regex;
38 46 $this->tokens = $tokens;
  47 + $this->pathVariables = $pathVariables;
  48 + $this->hostnameRegex = $hostnameRegex;
  49 + $this->hostnameTokens = $hostnameTokens;
  50 + $this->hostnameVariables = $hostnameVariables;
39 51 $this->variables = $variables;
40 52 }
41 53
@@ -60,6 +72,16 @@ public function getRegex()
60 72 }
61 73
62 74 /**
  75 + * Returns the hostname regex
  76 + *
  77 + * @return string|null The hostname regex or null
  78 + */
  79 + public function getHostnameRegex()
  80 + {
  81 + return $this->hostnameRegex;
  82 + }
  83 +
  84 + /**
63 85 * Returns the tokens.
64 86 *
65 87 * @return array The tokens
@@ -70,6 +92,16 @@ public function getTokens()
70 92 }
71 93
72 94 /**
  95 + * Returns the hostname tokens.
  96 + *
  97 + * @return array The tokens
  98 + */
  99 + public function getHostnameTokens()
  100 + {
  101 + return $this->hostnameTokens;
  102 + }
  103 +
  104 + /**
73 105 * Returns the variables.
74 106 *
75 107 * @return array The variables
@@ -78,4 +110,25 @@ public function getVariables()
78 110 {
79 111 return $this->variables;
80 112 }
  113 +
  114 + /**
  115 + * Returns the path variables.
  116 + *
  117 + * @return array The variables
  118 + */
  119 + public function getPathVariables()
  120 + {
  121 + return $this->pathVariables;
  122 + }
  123 +
  124 + /**
  125 + * Returns the hostname variables.
  126 + *
  127 + * @return array The variables
  128 + */
  129 + public function getHostnameVariables()
  130 + {
  131 + return $this->hostnameVariables;
  132 + }
  133 +
81 134 }
5 src/Symfony/Component/Routing/Generator/Dumper/PhpGeneratorDumper.php
@@ -91,6 +91,7 @@ private function generateDeclaredRoutes()
91 91 $properties[] = $route->getDefaults();
92 92 $properties[] = $route->getRequirements();
93 93 $properties[] = $compiledRoute->getTokens();
  94 + $properties[] = $compiledRoute->getHostnameTokens();
94 95
95 96 $routes .= sprintf(" '%s' => %s,\n", $name, str_replace("\n", '', var_export($properties, true)));
96 97 }
@@ -113,9 +114,9 @@ public function generate(\$name, \$parameters = array(), \$absolute = false)
113 114 throw new RouteNotFoundException(sprintf('Route "%s" does not exist.', \$name));
114 115 }
115 116
116   - list(\$variables, \$defaults, \$requirements, \$tokens) = self::\$declaredRoutes[\$name];
  117 + list(\$variables, \$defaults, \$requirements, \$tokens, \$hostnameTokens) = self::\$declaredRoutes[\$name];
117 118
118   - return \$this->doGenerate(\$variables, \$defaults, \$requirements, \$tokens, \$parameters, \$name, \$absolute);
  119 + return \$this->doGenerate(\$variables, \$defaults, \$requirements, \$tokens, \$parameters, \$name, \$absolute, \$hostnameTokens);
119 120 }
120 121 EOF;
121 122 }
38 src/Symfony/Component/Routing/Generator/UrlGenerator.php
@@ -122,14 +122,14 @@ public function generate($name, $parameters = array(), $absolute = false)
122 122 // the Route has a cache of its own and is not recompiled as long as it does not get modified
123 123 $compiledRoute = $route->compile();
124 124
125   - return $this->doGenerate($compiledRoute->getVariables(), $route->getDefaults(), $route->getRequirements(), $compiledRoute->getTokens(), $parameters, $name, $absolute);
  125 + return $this->doGenerate($compiledRoute->getVariables(), $route->getDefaults(), $route->getRequirements(), $compiledRoute->getTokens(), $parameters, $name, $absolute, $compiledRoute->getHostnameTokens());
126 126 }
127 127
128 128 /**
129 129 * @throws MissingMandatoryParametersException When route has some missing mandatory parameters
130 130 * @throws InvalidParameterException When a parameter value is not correct
131 131 */
132   - protected function doGenerate($variables, $defaults, $requirements, $tokens, $parameters, $name, $absolute)
  132 + protected function doGenerate($variables, $defaults, $requirements, $tokens, $parameters, $name, $absolute, $hostnameTokens)
133 133 {
134 134 $variables = array_flip($variables);
135 135 $mergedParams = array_replace($defaults, $this->context->getParameters(), $parameters);
@@ -191,13 +191,43 @@ protected function doGenerate($variables, $defaults, $requirements, $tokens, $pa
191 191 $url .= '?'.$query;
192 192 }
193 193
194   - if ($this->context->getHost()) {
  194 + if ($host = $this->context->getHost()) {
195 195 $scheme = $this->context->getScheme();
196 196 if (isset($requirements['_scheme']) && ($req = strtolower($requirements['_scheme'])) && $scheme != $req) {
197 197 $absolute = true;
198 198 $scheme = $req;
199 199 }
200 200
  201 + if ($hostnameTokens) {
  202 + $routeHost = '';
  203 + foreach ($hostnameTokens as $token) {
  204 + if ('variable' === $token[0]) {
  205 + if (null !== $this->strictRequirements && !preg_match('#^'.$token[2].'$#', $mergedParams[$token[3]])) {
  206 + $message = sprintf('Parameter "%s" for route "%s" must match "%s" ("%s" given).', $token[3], $name, $token[2], $mergedParams[$token[3]]);
  207 +
  208 + if ($this->strictRequirements) {
  209 + throw new InvalidParameterException($message);
  210 + }
  211 +
  212 + if ($this->logger) {
  213 + $this->logger->err($message);
  214 + }
  215 +
  216 + return null;
  217 + }
  218 +
  219 + $routeHost = $token[1].$mergedParams[$token[3]].$routeHost;
  220 + } elseif ('text' === $token[0]) {
  221 + $routeHost = $token[1].$routeHost;
  222 + }
  223 + }
  224 +
  225 + if ($routeHost != $host) {
  226 + $host = $routeHost;
  227 + $absolute = true;
  228 + }
  229 + }
  230 +
201 231 if ($absolute) {
202 232 $port = '';
203 233 if ('http' === $scheme && 80 != $this->context->getHttpPort()) {
@@ -206,7 +236,7 @@ protected function doGenerate($variables, $defaults, $requirements, $tokens, $pa
206 236 $port = ':'.$this->context->getHttpsPort();
207 237 }
208 238
209   - $url = $scheme.'://'.$this->context->getHost().$port.$url;
  239 + $url = $scheme.'://'.$host.$port.$url;
210 240 }
211 241 }
212 242
28 src/Symfony/Component/Routing/Loader/AnnotationClassLoader.php
@@ -57,7 +57,7 @@
57 57 abstract class AnnotationClassLoader implements LoaderInterface
58 58 {
59 59 protected $reader;
60   - protected $routeAnnotationClass = 'Symfony\\Component\\Routing\\Annotation\\Route';
  60 + protected $routeAnnotationClass = 'Symfony\\Component\\Routing\\Annotation\\Route';
61 61 protected $defaultRouteIndex;
62 62
63 63 /**
@@ -97,10 +97,11 @@ public function load($class, $type = null)
97 97 }
98 98
99 99 $globals = array(
100   - 'pattern' => '',
101   - 'requirements' => array(),
102   - 'options' => array(),
103   - 'defaults' => array(),
  100 + 'pattern' => '',
  101 + 'requirements' => array(),
  102 + 'options' => array(),
  103 + 'defaults' => array(),
  104 + 'hostname_pattern' => '',
104 105 );
105 106
106 107 $class = new \ReflectionClass($class);
@@ -124,6 +125,10 @@ public function load($class, $type = null)
124 125 if (null !== $annot->getDefaults()) {
125 126 $globals['defaults'] = $annot->getDefaults();
126 127 }
  128 +
  129 + if (null !== $annot->getHostnamePattern()) {
  130 + $globals['hostname_pattern'] = $annot->getHostnamePattern();
  131 + }
127 132 }
128 133
129 134 $collection = new RouteCollection();
@@ -148,16 +153,21 @@ protected function addRoute(RouteCollection $collection, $annot, $globals, \Refl
148 153 $name = $this->getDefaultRouteName($class, $method);
149 154 }
150 155
151   - $defaults = array_merge($globals['defaults'], $annot->getDefaults());
  156 + $defaults = array_replace($globals['defaults'], $annot->getDefaults());
152 157 foreach ($method->getParameters() as $param) {
153 158 if ($param->isOptional()) {
154 159 $defaults[$param->getName()] = $param->getDefaultValue();
155 160 }
156 161 }
157   - $requirements = array_merge($globals['requirements'], $annot->getRequirements());
158   - $options = array_merge($globals['options'], $annot->getOptions());
  162 + $requirements = array_replace($globals['requirements'], $annot->getRequirements());
  163 + $options = array_replace($globals['options'], $annot->getOptions());
  164 +
  165 + $hostnamePattern = $annot->getHostnamePattern();
  166 + if (null === $hostnamePattern) {
  167 + $hostnamePattern = $globals['hostname_pattern'];
  168 + }
159 169
160   - $route = new Route($globals['pattern'].$annot->getPattern(), $defaults, $requirements, $options);
  170 + $route = new Route($globals['pattern'].$annot->getPattern(), $defaults, $requirements, $options, $hostnamePattern);
161 171
162 172 $this->configureRoute($route, $class, $method, $annot);
163 173
25 src/Symfony/Component/Routing/Loader/XmlFileLoader.php
@@ -73,9 +73,10 @@ protected function parseNode(RouteCollection $collection, \DOMElement $node, $pa
73 73 $this->parseRoute($collection, $node, $path);
74 74 break;
75 75 case 'import':
76   - $resource = (string) $node->getAttribute('resource');
77   - $type = (string) $node->getAttribute('type');
78   - $prefix = (string) $node->getAttribute('prefix');
  76 + $resource = $node->getAttribute('resource');
  77 + $type = $node->getAttribute('type');
  78 + $prefix = $node->getAttribute('prefix');
  79 + $hostnamePattern = $node->getAttribute('hostname-pattern');
79 80
80 81 $defaults = array();
81 82 $requirements = array();
@@ -88,13 +89,13 @@ protected function parseNode(RouteCollection $collection, \DOMElement $node, $pa
88 89
89 90 switch ($n->tagName) {
90 91 case 'default':
91   - $defaults[(string) $n->getAttribute('key')] = trim((string) $n->nodeValue);
  92 + $defaults[$n->getAttribute('key')] = trim($n->nodeValue);
92 93 break;
93 94 case 'requirement':
94   - $requirements[(string) $n->getAttribute('key')] = trim((string) $n->nodeValue);
  95 + $requirements[$n->getAttribute('key')] = trim($n->nodeValue);
95 96 break;
96 97 case 'option':
97   - $options[(string) $n->getAttribute('key')] = trim((string) $n->nodeValue);
  98 + $options[$n->getAttribute('key')] = trim($n->nodeValue);
98 99 break;
99 100 default:
100 101 throw new \InvalidArgumentException(sprintf('Unable to parse tag "%s"', $n->tagName));
@@ -102,7 +103,7 @@ protected function parseNode(RouteCollection $collection, \DOMElement $node, $pa
102 103 }
103 104
104 105 $this->setCurrentDir(dirname($path));
105   - $collection->addCollection($this->import($resource, ('' !== $type ? $type : null), false, $file), $prefix, $defaults, $requirements, $options);
  106 + $collection->addCollection($this->import($resource, ('' !== $type ? $type : null), false, $file), $prefix, $defaults, $requirements, $options, $hostnamePattern);
106 107 break;
107 108 default:
108 109 throw new \InvalidArgumentException(sprintf('Unable to parse tag "%s"', $node->tagName));
@@ -141,22 +142,22 @@ protected function parseRoute(RouteCollection $collection, \DOMElement $definiti
141 142
142 143 switch ($node->tagName) {
143 144 case 'default':
144   - $defaults[(string) $node->getAttribute('key')] = trim((string) $node->nodeValue);
  145 + $defaults[$node->getAttribute('key')] = trim((string) $node->nodeValue);
145 146 break;
146 147 case 'option':
147   - $options[(string) $node->getAttribute('key')] = trim((string) $node->nodeValue);
  148 + $options[$node->getAttribute('key')] = trim((string) $node->nodeValue);
148 149 break;
149 150 case 'requirement':
150   - $requirements[(string) $node->getAttribute('key')] = trim((string) $node->nodeValue);
  151 + $requirements[$node->getAttribute('key')] = trim((string) $node->nodeValue);
151 152 break;
152 153 default:
153 154 throw new \InvalidArgumentException(sprintf('Unable to parse tag "%s"', $node->tagName));
154 155 }
155 156 }
156 157
157   - $route = new Route((string) $definition->getAttribute('pattern'), $defaults, $requirements, $options);
  158 + $route = new Route($definition->getAttribute('pattern'), $defaults, $requirements, $options, $definition->getAttribute('hostname-pattern'));
158 159
159   - $collection->add((string) $definition->getAttribute('id'), $route);
  160 + $collection->add($definition->getAttribute('id'), $route);
160 161 }
161 162
162 163 /**
8 src/Symfony/Component/Routing/Loader/YamlFileLoader.php
@@ -27,7 +27,7 @@
27 27 class YamlFileLoader extends FileLoader
28 28 {
29 29 private static $availableKeys = array(
30   - 'type', 'resource', 'prefix', 'pattern', 'options', 'defaults', 'requirements'
  30 + 'type', 'resource', 'prefix', 'pattern', 'options', 'defaults', 'requirements', 'hostname_pattern',
31 31 );
32 32
33 33 /**
@@ -70,9 +70,10 @@ public function load($file, $type = null)
70 70 $defaults = isset($config['defaults']) ? $config['defaults'] : array();
71 71 $requirements = isset($config['requirements']) ? $config['requirements'] : array();
72 72 $options = isset($config['options']) ? $config['options'] : array();
  73 + $hostnamePattern = isset($config['hostname_pattern']) ? $config['hostname_pattern'] : '';
73 74
74 75 $this->setCurrentDir(dirname($path));
75   - $collection->addCollection($this->import($config['resource'], $type, false, $file), $prefix, $defaults, $requirements, $options);
  76 + $collection->addCollection($this->import($config['resource'], $type, false, $file), $prefix, $defaults, $requirements, $options, $hostnamePattern);
76 77 } else {
77 78 $this->parseRoute($collection, $name, $config, $path);
78 79 }
@@ -106,12 +107,13 @@ protected function parseRoute(RouteCollection $collection, $name, $config, $file
106 107 $defaults = isset($config['defaults']) ? $config['defaults'] : array();
107 108 $requirements = isset($config['requirements']) ? $config['requirements'] : array();
108 109 $options = isset($config['options']) ? $config['options'] : array();
  110 + $hostnamePattern = isset($config['hostname_pattern']) ? $config['hostname_pattern'] : null;
109 111
110 112 if (!isset($config['pattern'])) {
111 113 throw new \InvalidArgumentException(sprintf('You must define a "pattern" for the "%s" route.', $name));
112 114 }
113 115
114   - $route = new Route($config['pattern'], $defaults, $requirements, $options);
  116 + $route = new Route($config['pattern'], $defaults, $requirements, $options, $hostnamePattern);
115 117
116 118 $collection->add($name, $route);
117 119 }
2  src/Symfony/Component/Routing/Loader/schema/routing/routing-1.0.xsd
@@ -23,6 +23,7 @@
23 23
24 24 <xsd:attribute name="id" type="xsd:string" />
25 25 <xsd:attribute name="pattern" type="xsd:string" />
  26 + <xsd:attribute name="hostname-pattern" type="xsd:string" />
26 27 </xsd:complexType>
27 28
28 29 <xsd:complexType name="import">
@@ -36,6 +37,7 @@
36 37 <xsd:attribute name="type" type="xsd:string" />
37 38 <xsd:attribute name="prefix" type="xsd:string" />
38 39 <xsd:attribute name="class" type="xsd:string" />
  40 + <xsd:attribute name="hostname-pattern" type="xsd:string" />
39 41 </xsd:complexType>
40 42
41 43 <xsd:complexType name="element" mixed="true">
78 src/Symfony/Component/Routing/Matcher/Dumper/ApacheMatcherDumper.php
@@ -46,17 +46,55 @@ public function dump(array $options = array())
46 46
47 47 $rules = array("# skip \"real\" requests\nRewriteCond %{REQUEST_FILENAME} -f\nRewriteRule .* - [QSA,L]");
48 48 $methodVars = array();
  49 + $hostnameRegexUnique = 0;
  50 + $prevHosnameRegex = '';
49 51
50 52 foreach ($this->getRoutes()->all() as $name => $route) {
51   - $rules[] = $this->dumpRoute($name, $route, $options);
52   - $methodVars = array_merge($methodVars, $this->getRouteMethods($route));
53   - }
54 53
  54 + $compiledRoute = $route->compile();
  55 + $hostnameRegex = $compiledRoute->getHostnameRegex();
  56 +
  57 + if (null !== $hostnameRegex && $prevHosnameRegex !== $hostnameRegex) {
  58 +
  59 + $prevHosnameRegex = $hostnameRegex;
  60 + $hostnameRegexUnique++;
  61 +
  62 + $rule = array();
  63 +
  64 + $regex = $this->regexToApacheRegex($hostnameRegex);
  65 + $regex = self::escape($regex, ' ', '\\');
  66 +
  67 + $rule[] = sprintf('RewriteCond %%{HTTP:Host} %s', $regex);
  68 +
  69 + $variables = array();
  70 + $variables[] = sprintf('E=__ROUTING_hostname_%s:1', $hostnameRegexUnique);
  71 +
  72 + foreach ($compiledRoute->getHostnameVariables() as $i => $variable) {
  73 + $variables[] = sprintf('E=__ROUTING_hostname_%s_%s:%%%d', $hostnameRegexUnique, $variable, $i+1);
  74 + }
  75 +
  76 + $variables = implode(',', $variables);
  77 +
  78 + $rule[] = sprintf('RewriteRule .? - [%s]', $variables);
  79 +
  80 + $rules[] = implode("\n", $rule);
  81 + }
  82 +
  83 + $rules[] = $this->dumpRoute($name, $route, $options, $hostnameRegexUnique);
  84 +
  85 + if ($req = $route->getRequirement('_method')) {
  86 + $methods = explode('|', strtoupper($req));
  87 + $methodVars = array_merge($methodVars, $methods);
  88 + }
  89 + }
55 90 if (0 < count($methodVars)) {
56 91 $rule = array('# 405 Method Not Allowed');
57 92 $methodVars = array_values(array_unique($methodVars));
  93 + if (in_array('GET', $methodVars) && !in_array('HEAD', $methodVars)) {
  94 + $methodVars[] = 'HEAD';
  95 + }
58 96 foreach ($methodVars as $i => $methodVar) {
59   - $rule[] = sprintf('RewriteCond %%{_ROUTING_allow_%s} !-z%s', $methodVar, isset($methodVars[$i + 1]) ? ' [OR]' : '');
  97 + $rule[] = sprintf('RewriteCond %%{ENV:_ROUTING__allow_%s} =1%s', $methodVar, isset($methodVars[$i + 1]) ? ' [OR]' : '');
60 98 }
61 99 $rule[] = sprintf('RewriteRule .* %s [QSA,L]', $options['script_name']);
62 100
@@ -66,7 +104,17 @@ public function dump(array $options = array())
66 104 return implode("\n\n", $rules)."\n";
67 105 }
68 106
69   - private function dumpRoute($name, $route, array $options)
  107 + /**
  108 + * Dumps a single route
  109 + *
  110 + * @param string $name Route name
  111 + * @param Route $route The route
  112 + * @param array $options Options
  113 + * @param bool $hostnameRegexUnique Unique identifier for the hostname regex
  114 + *
  115 + * @return string The compiled route
  116 + */
  117 + private function dumpRoute($name, $route, array $options, $hostnameRegexUnique)
70 118 {
71 119 $compiledRoute = $route->compile();
72 120
@@ -79,7 +127,10 @@ private function dumpRoute($name, $route, array $options)
79 127 $hasTrailingSlash = (!$methods || in_array('HEAD', $methods)) && '/$' === substr($regex, -2) && '^/$' !== $regex;
80 128
81 129 $variables = array('E=_ROUTING_route:'.$name);
82   - foreach ($compiledRoute->getVariables() as $i => $variable) {
  130 + foreach ($compiledRoute->getHostnameVariables() as $variable) {
  131 + $variables[] = sprintf('E=_ROUTING_param_%s:%%{ENV:__ROUTING_hostname_%s_%s}', $variable, $hostnameRegexUnique, $variable);
  132 + }
  133 + foreach ($compiledRoute->getPathVariables() as $i => $variable) {
83 134 $variables[] = 'E=_ROUTING_param_'.$variable.':%'.($i + 1);
84 135 }
85 136 foreach ($route->getDefaults() as $key => $value) {
@@ -98,10 +149,13 @@ private function dumpRoute($name, $route, array $options)
98 149 if (0 < count($methods)) {
99 150 $allow = array();
100 151 foreach ($methods as $method) {
101   - $methodVars[] = $method;
102 152 $allow[] = 'E=_ROUTING_allow_'.$method.':1';
103 153 }
104 154
  155 + if ($hostnameRegex = $compiledRoute->getHostnameRegex()) {
  156 + $rule[] = sprintf("RewriteCond %%{ENV:__ROUTING_hostname_%s} =1", $hostnameRegexUnique);
  157 + }
  158 +
105 159 $rule[] = "RewriteCond %{REQUEST_URI} $regex";
106 160 $rule[] = sprintf("RewriteCond %%{REQUEST_METHOD} !^(%s)$ [NC]", implode('|', $methods));
107 161 $rule[] = sprintf('RewriteRule .* - [S=%d,%s]', $hasTrailingSlash ? 2 : 1, implode(',', $allow));
@@ -109,11 +163,21 @@ private function dumpRoute($name, $route, array $options)
109 163
110 164 // redirect with trailing slash appended
111 165 if ($hasTrailingSlash) {
  166 +
  167 + if ($hostnameRegex = $compiledRoute->getHostnameRegex()) {
  168 + $rule[] = sprintf("RewriteCond %%{ENV:__ROUTING_hostname_%s} =1", $hostnameRegexUnique);
  169 + }
  170 +
112 171 $rule[] = 'RewriteCond %{REQUEST_URI} '.substr($regex, 0, -2).'$';
113 172 $rule[] = 'RewriteRule .* $0/ [QSA,L,R=301]';
114 173 }
115 174
116 175 // the main rule
  176 +
  177 + if ($hostnameRegex = $compiledRoute->getHostnameRegex()) {
  178 + $rule[] = sprintf("RewriteCond %%{ENV:__ROUTING_hostname_%s} =1", $hostnameRegexUnique);
  179 + }
  180 +
117 181 $rule[] = "RewriteCond %{REQUEST_URI} $regex";
118 182 $rule[] = "RewriteRule .* {$options['script_name']} [QSA,L,$variables]";
119 183
49 src/Symfony/Component/Routing/Matcher/Dumper/DumperCollection.php
@@ -20,6 +20,7 @@ class DumperCollection implements \IteratorAggregate
20 20 {
21 21 private $parent;
22 22 private $children = array();
  23 + private $attributes = array();
23 24
24 25 /**
25 26 * Returns the children routes and collections.
@@ -45,7 +46,7 @@ public function add($child)
45 46 }
46 47
47 48 /**
48   - * Sets children
  49 + * Sets children.
49 50 *
50 51 * @param array $children The children
51 52 */
@@ -98,4 +99,50 @@ protected function setParent(DumperCollection $parent)
98 99 {
99 100 $this->parent = $parent;
100 101 }
  102 +
  103 + /**
  104 + * Returns true if the attribute is defined.
  105 + *
  106 + * @param string $name The attribute name
  107 + *
  108 + * @return Boolean true if the attribute is defined, false otherwise
  109 + */
  110 + public function hasAttribute($name)
  111 + {
  112 + return array_key_exists($name, $this->attributes);
  113 + }
  114 +
  115 + /**
  116 + * Returns an attribute by name.
  117 + *
  118 + * @param string $name The attribute name
  119 + * @param mixed $default Default value is the attribute doesn't exist
  120 + *
  121 + * @return mixed The attribute value
  122 + */
  123 + public function getAttribute($name, $default = null)
  124 + {
  125 + return $this->hasAttribute($name) ? $this->attributes[$name] : $default;
  126 + }
  127 +
  128 + /**
  129 + * Sets an attribute by name.
  130 + *
  131 + * @param string $name The attribute name
  132 + * @param mixed $value The attribute value
  133 + */
  134 + public function setAttribute($name, $value)
  135 + {
  136 + $this->attributes[$name] = $value;
  137 + }
  138 +
  139 + /**
  140 + * Sets multiple attributes.
  141 + *
  142 + * @param array $attributes The attributes
  143 + */
  144 + public function setAttributes($attributes)
  145 + {
  146 + $this->attributes = $attributes;
  147 + }
101 148 }
105 src/Symfony/Component/Routing/Matcher/Dumper/PhpMatcherDumper.php
@@ -37,7 +37,7 @@ class PhpMatcherDumper extends MatcherDumper
37 37 */
38 38 public function dump(array $options = array())
39 39 {
40   - $options = array_merge(array(
  40 + $options = array_replace(array(
41 41 'class' => 'ProjectUrlMatcher',
42 42 'base_class' => 'Symfony\\Component\\Routing\\Matcher\\UrlMatcher',
43 43 ), $options);
@@ -109,18 +109,44 @@ public function match(\$pathinfo)
109 109 */
110 110 private function compileRoutes(RouteCollection $routes, $supportsRedirections)
111 111 {
112   - $collection = $this->flattenRouteCollection($routes);
113   - $tree = $this->buildPrefixTree($collection);
  112 + $fetchedHostname = false;
114 113
115   - return $this->compilePrefixRoutes($tree, $supportsRedirections);
  114 + $routes = $this->flattenRouteCollection($routes);
  115 + $groups = $this->groupRoutesByHostnameRegex($routes);
  116 + $code = '';
  117 +
  118 + foreach ($groups as $collection) {
  119 + if (null !== $regex = $collection->getAttribute('hostname_regex')) {
  120 + if (!$fetchedHostname) {
  121 + $code .= " \$hostname = \$this->context->getHost();\n\n";
  122 + $fetchedHostname = true;
  123 + }
  124 +
  125 + $code .= sprintf(" if (preg_match(%s, \$hostname, \$hostnameMatches)) {\n", var_export($regex, true));
  126 + }
  127 +
  128 + $tree = $this->buildPrefixTree($collection);
  129 + $groupCode = $this->compilePrefixRoutes($tree, $supportsRedirections);
  130 +
  131 + if (null !== $regex) {
  132 + // apply extra indention at each line (except empty ones)
  133 + $groupCode = preg_replace('/^.{2,}$/m', ' $0', $groupCode);
  134 + $code .= $groupCode;
  135 + $code .= " }\n\n";
  136 + } else {
  137 + $code .= $groupCode;
  138 + }
  139 + }
  140 +
  141 + return $code;
116 142 }
117 143
118 144 /**
119 145 * Generates PHP code recursively to match a tree of routes
120 146 *
121   - * @param DumperPrefixCollection $routes A DumperPrefixCollection instance
  147 + * @param DumperPrefixCollection $routes A DumperPrefixCollection instance
122 148 * @param Boolean $supportsRedirections Whether redirections are supported by the base class
123   - * @parma string $prefix Prefix of the parent collection
  149 + * @parma string $prefix Prefix of the parent collection
124 150 *
125 151 * @return string PHP code
126 152 */
@@ -171,6 +197,7 @@ private function compileRoute(Route $route, $name, $supportsRedirections, $paren
171 197 $conditions = array();
172 198 $hasTrailingSlash = false;
173 199 $matches = false;
  200 + $hostnameMatches = false;
174 201 $methods = array();
175 202
176 203 if ($req = $route->getRequirement('_method')) {
@@ -183,7 +210,7 @@ private function compileRoute(Route $route, $name, $supportsRedirections, $paren
183 210
184 211 $supportsTrailingSlash = $supportsRedirections && (!$methods || in_array('HEAD', $methods));
185 212
186   - if (!count($compiledRoute->getVariables()) && false !== preg_match('#^(.)\^(?<url>.*?)\$\1#', $compiledRoute->getRegex(), $m)) {
  213 + if (!count($compiledRoute->getPathVariables()) && false !== preg_match('#^(.)\^(?<url>.*?)\$\1#', $compiledRoute->getRegex(), $m)) {
187 214 if ($supportsTrailingSlash && substr($m['url'], -1) === '/') {
188 215 $conditions[] = sprintf("rtrim(\$pathinfo, '/') === %s", var_export(rtrim(str_replace('\\', '', $m['url']), '/'), true));
189 216 $hasTrailingSlash = true;
@@ -205,6 +232,10 @@ private function compileRoute(Route $route, $name, $supportsRedirections, $paren
205 232 $matches = true;
206 233 }
207 234
  235 + if ($compiledRoute->getHostnameVariables()) {
  236 + $hostnameMatches = true;
  237 + }
  238 +
208 239 $conditions = implode(' && ', $conditions);
209 240
210 241 $code .= <<<EOF
@@ -263,14 +294,21 @@ private function compileRoute(Route $route, $name, $supportsRedirections, $paren
263 294 }
264 295
265 296 // optimize parameters array
266   - if (true === $matches && $route->getDefaults()) {
267   - $code .= sprintf(" return array_merge(\$this->mergeDefaults(\$matches, %s), array('_route' => '%s'));\n"
268   - , str_replace("\n", '', var_export($route->getDefaults(), true)), $name);
269   - } elseif (true === $matches) {
270   - $code .= sprintf(" \$matches['_route'] = '%s';\n\n", $name);
271   - $code .= " return \$matches;\n";
  297 + if ($matches || $hostnameMatches) {
  298 + $vars = array();
  299 + if ($hostnameMatches) {
  300 + $vars[] = '$hostnameMatches';
  301 + }
  302 + if ($matches) {
  303 + $vars[] = '$matches';
  304 + }
  305 + $vars[] = "array('_route' => '$name')";
  306 +
  307 + $code .= sprintf(" return \$this->mergeDefaults(array_replace(%s), %s);\n"
  308 + , implode(', ', $vars), str_replace("\n", '', var_export($route->getDefaults(), true)));
  309 +
272 310 } elseif ($route->getDefaults()) {
273   - $code .= sprintf(" return %s;\n", str_replace("\n", '', var_export(array_merge($route->getDefaults(), array('_route' => $name)), true)));
  311 + $code .= sprintf(" return %s;\n", str_replace("\n", '', var_export(array_replace($route->getDefaults(), array('_route' => $name)), true)));
274 312 } else {
275 313 $code .= sprintf(" return array('_route' => '%s');\n", $name);
276 314 }
@@ -284,10 +322,10 @@ private function compileRoute(Route $route, $name, $supportsRedirections, $paren
284 322 }
285 323
286 324 /**
287   - * Flattens a tree of routes to a single collection
  325 + * Flattens a tree of routes to a single collection.
288 326 *
289   - * @param RouteCollection $routes Collection of routes
290   - * @param DumperCollection $to A DumperCollection to add routes to
  327 + * @param RouteCollection $routes Collection of routes
  328 + * @param DumperCollection $to A DumperCollection to add routes to
291 329 *
292 330 * @return DumperCollection
293 331 */
@@ -309,12 +347,42 @@ private function flattenRouteCollection(RouteCollection $routes, DumperCollectio
309 347 }
310 348
311 349 /**
  350 + * Groups consecutive routes having the same hostname regex.
  351 + *
  352 + * The results is a collection of collections of routes having the same hostname regex.
  353 + *
  354 + * @param DumperCollection $routes Flat collection of DumperRoutes
  355 + *
  356 + * @return DumperCollection A collection with routes grouped by hostname regex in sub-collections
  357 + */
  358 + private function groupRoutesByHostnameRegex(DumperCollection $routes)
  359 + {
  360 + $groups = new DumperCollection();
  361 +
  362 + $currentGroup = new DumperCollection();
  363 + $currentGroup->setAttribute('hostname_regex', null);
  364 + $groups->add($currentGroup);
  365 +
  366 + foreach ($routes as $route) {
  367 + $hostnameRegex = $route->getRoute()->compile()->getHostnameRegex();
  368 + if ($currentGroup->getAttribute('hostname_regex') !== $hostnameRegex) {
  369 + $currentGroup = new DumperCollection();
  370 + $currentGroup->setAttribute('hostname_regex', $hostnameRegex);
  371 + $groups->add($currentGroup);
  372 + }
  373 + $currentGroup->add($route);
  374 + }
  375 +
  376 + return $groups;
  377 + }
  378 +
  379 + /**
312 380 * Organizes the routes into a prefix tree.
313 381 *
314 382 * Routes order is preserved such that traversing the tree will traverse the
315 383 * routes in the origin order
316 384 *
317   - * @param DumperCollection $collection A collection of routes
  385 + * @param DumperCollection $collection A collection of routes
318 386 *
319 387 * @return DumperPrefixCollection
320 388 */
@@ -331,5 +399,4 @@ private function buildPrefixTree(DumperCollection $collection)
331 399
332 400 return $tree;
333 401 }
334   -
335 402 }
7 src/Symfony/Component/Routing/Matcher/UrlMatcher.php
@@ -118,6 +118,11 @@ protected function matchCollection($pathinfo, RouteCollection $routes)
118 118 continue;
119 119 }
120 120
  121 + $hostnameMatches = array();
  122 + if ($compiledRoute->getHostnameRegex() && !preg_match($compiledRoute->getHostnameRegex(), $this->context->getHost(), $hostnameMatches)) {
  123 + continue;
  124 + }
  125 +
121 126 // check HTTP method requirement
122 127 if ($req = $route->getRequirement('_method')) {
123 128 // HEAD and GET are equivalent as per RFC
@@ -142,7 +147,7 @@ protected function matchCollection($pathinfo, RouteCollection $routes)
142 147 continue;
143 148 }
144 149
145   - return array_merge($this->mergeDefaults($matches, $route->getDefaults()), array('_route' => $name));
  150 + return $this->mergeDefaults(array_replace($matches, $hostnameMatches, array('_route' => $name)), $route->getDefaults());
146 151 }
147 152 }
148 153
27 src/Symfony/Component/Routing/Route.php
@@ -25,6 +25,7 @@ class Route implements \Serializable
25 25 private $requirements;
26 26 private $options;
27 27 private $compiled;
  28 + private $hostnamePattern;
28 29
29 30 private static $compilers = array();
30 31
@@ -39,15 +40,17 @@ class Route implements \Serializable
39 40 * @param array $defaults An array of default parameter values
40 41 * @param array $requirements An array of requirements for parameters (regexes)
41 42 * @param array $options An array of options
  43 + * @param string $hostname The hostname pattern to match
42 44 *
43 45 * @api
44 46 */
45   - public function __construct($pattern, array $defaults = array(), array $requirements = array(), array $options = array())
  47 + public function __construct($pattern, array $defaults = array(), array $requirements = array(), array $options = array(), $hostnamePattern = '')
46 48 {
47 49 $this->setPattern($pattern);
48 50 $this->setDefaults($defaults);
49 51 $this->setRequirements($requirements);
50 52 $this->setOptions($options);
  53 + $this->setHostnamePattern($hostnamePattern);
51 54 }
52 55
53 56 public function __clone()
@@ -104,6 +107,28 @@ public function setPattern($pattern)
104 107 }
105 108
106 109 /**
  110 + * Returns the hostname pattern.
  111 + *
  112 + * @return string The pattern
  113 + */
  114 + public function getHostnamePattern()
  115 + {
  116 + return $this->hostnamePattern;
  117 + }
  118 +
  119 + /**
  120 + * Sets the hostname pattern.
  121 + *
  122 + * @param string $pattern The pattern
  123 + */
  124 + public function setHostnamePattern($pattern)
  125 + {
  126 + $this->hostnamePattern = (string) $pattern;
  127 +
  128 + return $this;
  129 + }
  130 +
  131 + /**
107 132 * Returns the options.
108 133 *
109 134 * @return array The options
44 src/Symfony/Component/Routing/RouteCollection.php
@@ -30,6 +30,7 @@ class RouteCollection implements \IteratorAggregate, \Countable
30 30 private $resources;
31 31 private $prefix;
32 32 private $parent;
  33 + private $hostnamePattern;
33 34
34 35 /**
35 36 * Constructor.
@@ -41,6 +42,7 @@ public function __construct()
41 42 $this->routes = array();
42 43 $this->resources = array();
43 44 $this->prefix = '';
  45 + $this->hostnamePattern = '';
44 46 }
45 47
46 48 public function __clone()
@@ -183,17 +185,18 @@ public function remove($name)
183 185 /**
184 186 * Adds a route collection to the current set of routes (at the end of the current set).
185 187 *
186   - * @param RouteCollection $collection A RouteCollection instance
187   - * @param string $prefix An optional prefix to add before each pattern of the route collection
188   - * @param array $defaults An array of default values
189   - * @param array $requirements An array of requirements
190   - * @param array $options An array of options
  188 + * @param RouteCollection $collection A RouteCollection instance
  189 + * @param string $prefix An optional prefix to add before each pattern of the route collection
  190 + * @param array $defaults An array of default values
  191 + * @param array $requirements An array of requirements
  192 + * @param array $options An array of options
  193 + * @param string $hostnamePattern Hostname pattern
191 194 *
192 195 * @throws \InvalidArgumentException When the RouteCollection already exists in the tree
193 196 *
194 197 * @api
195 198 */
196   - public function addCollection(RouteCollection $collection, $prefix = '', $defaults = array(), $requirements = array(), $options = array())
  199 + public function addCollection(RouteCollection $collection, $prefix = '', $defaults = array(), $requirements = array(), $options = array(), $hostnamePattern = '')
197 200 {
198 201 // prevent infinite loops by recursive referencing
199 202 $root = $this->getRoot();
@@ -208,6 +211,11 @@ public function addCollection(RouteCollection $collection, $prefix = '', $defaul
208 211 // the sub-collection must have the prefix of the parent (current instance) prepended because it does not
209 212 // necessarily already have it applied (depending on the order RouteCollections are added to each other)
210 213 $collection->addPrefix($this->getPrefix() . $prefix, $defaults, $requirements, $options);
  214 +
  215 + if ('' !== $hostnamePattern) {
  216 + $collection->setHostnamePattern($hostnamePattern);
  217 + }
  218 +
211 219 $this->routes[] = $collection;
212 220 }
213 221
@@ -260,6 +268,30 @@ public function getPrefix()
260 268 }
261 269
262 270 /**
  271 + * Returns the hostname pattern.
  272 + *
  273 + * @return string The pattern
  274 + */
  275 + public function getHostnamePattern()
  276 + {
  277 + return $this->hostnamePattern;
  278 + }
  279 +