A question came across the mailing list today that provides an excellent opportunity to demonstrate the flexibility of the symfony 1.2 routing system.
Evert Harmeling posed the following:
How can I point sub1.domain.com/test to a different route than sub2.domain.com/test inside the same application?
This is not supported natively by the symfony core, but the routing system can easily be extended to meet Evert's requirement.
Extending sfRequestRoute
Each route in routing.yml
includes a set of requirements that must be met in
order for that route to be successfully connected. Symfony 1.2 introduced
the sfRequestRoute
class, which exposes a sf_method
requirement, allowing you to create RESTful
interfaces based on HTTP request method. We can create a similar mechanism to
expose a sf_host
requirement.
Implementing this new requirement option would look something like this:
# apps/*/config/routing.yml homepage_sub1: url: / param: { module: main, action: homepage1 } class: sfRequestHostRoute requirements: sf_host: sub1.example.com homepage_sub2: url: / param: { module: main, action: homepage2 } class: sfRequestHostRoute requirements: sf_host: sub2.example.com
These routing rules reference a sfRequestHostRoute
class, which we will
write now:
// lib/routing/sfRequestHostRoute.class.php class sfRequestHostRoute extends sfRequestRoute { }
Enforcing the sf_host
requirement
The sf_host
requirement must be enforced when the routing system inspects
an incoming URL. This is done in the
matchesUrl()
method:
class sfRequestHostRoute extends sfRequestRoute { public function matchesUrl($url, $context = array()) { if ( isset($this->requirements['sf_host']) && $this->requirements['sf_host'] != $context['host'] ) { return false; } return parent::matchesUrl($url, $context); } }
This code inspects the current request's host and compares it to the route's
sf_host
requirement. If they don't match, the method returns false
; if
they do match, sfRequestHostRoute
passes the URL to the overloaded method.
Generating URLs that respect sf_host
Enforcing the sf_host
requirement is only part of the solution. In addition
to parsing incoming URLs, the routing system also generates URLs for the view
layer. We add consideration for the generated URL's host to the
generate()
method:
class sfRequestHostRoute extends sfRequestRoute { // ... public function generate($params, $context = array(), $absolute = false) { $url = parent::generate($params, $context, $absolute); if ( isset($this->requirements['sf_host']) && $this->requirements['sf_host'] != $context['host'] ) { // apply the required host $protocol = $context['is_secure'] ? 'https' : 'http'; $url = $protocol.'://'.$this->requirements['sf_host'].$url; } return $url; } }
This code compares the current request's host with the sf_host
requirement
of the route and forces generation of an absolute URL if they don't match.
The
$context
parameter passed to these route methods is not the familiarsfContext
object, but rather an array describing the current request. For more information take a look at sfWebRequest::getRequestContext().
Using the new sfRequestHostRoute
class
Once you define a routing rule with the new sfRequestHostRoute
class you may
use this route in your templates in the same way you would any other. The
tables below illustrate how the routes we defined above will behave when
called from each host.
Called from sub1.example.com | Result |
---|---|
url_for('@homepage_sub1') |
/ |
url_for('@homepage_sub1', true) |
http://sub1.example.com/ |
url_for('@homepage_sub2') |
http://sub2.example.com/ |
url_for('@homepage_sub2', true) |
http://sub2.example.com/ |
Called from sub2.example.com | Result |
---|---|
url_for('@homepage_sub1') |
http://sub1.example.com/ |
url_for('@homepage_sub1', true) |
http://sub1.example.com/ |
url_for('@homepage_sub2') |
/ |
url_for('@homepage_sub2', true) |
http://sub2.example.com/ |
Vary Subdomain by Environment
One of the more useful features of symfony is its support for multiple
environments. The production environment may be running on the example.com
domain, but the development environment most certainly is not.
We can vary the host requirements by environment by defining them in
app.yml
:
# apps/*/config/app.yml prod: sub1_host: sub1.example.com sub2_host: sub2.example.com dev: sub1_host: sub1.example.local sub2_host: sub2.example.local
These values can then be placed in routing.yml
using PHP:
# apps/*/config/routing.yml homepage_sub1: url: / param: { module: main, action: homepage1 } class: sfRequestHostRoute requirements: sf_host: <?php echo sfConfig::get('app_sub1_host')."\n" ?> homepage_sub2: url: / param: { module: main, action: homepage2 } class: sfRequestHostRoute requirements: sf_host: <?php echo sfConfig::get('app_sub2_host')."\n" ?>
Conclusion
By overloading two methods and editing two configuration files we've extended the routing system to consider subdomains. Happy coding!
Hi Kris, Great example! Especially the environment-dependend-routing is excellent!
Regards, Lambert
Great - I haven't touched the new routing stuff yet - this is very very enlightening.
I discovered a minor bug in the routing system having to do with absolute URLs which has been fixed in r15940 and will be included in symfony 1.2.5.
I still don't have the time to check for the new Routing system in symfony 1.2, but this article will make me search the moment to do it, thx.
It looks awesome!
Great! This mechanism will save me a lot of time in my next task, where i am forced with those requirements. Thanks for this article.
Just a question..
This code: $url = parent::generate($params, $context, $absolute);
If $absolute is true, doesn't sfRequestRoute already add the "http://%%host%%/" section of the url? So it would result in something like "http://%%host%%/http://%%host%%/my_url"
Or maybe I just don't understand really well how it works.. This is how I think it works...
Maybe I am stating the obvious, but isn't setting up virtual hosts for every sub-domain a necessary step to set this tuto working ? [On local with windows, I have moreover to enter a new line for every vh in my hosts file.]
Great tutorial Kris!
But, now we still have a problem with dynamic subdomains as in www.domain.com, members.domain.com and *.domain.com.
We thought to set all requirements to www and members subs, but we want to set the opposite in case these aren't the subdomains. We looked at filters, and using regex in the requirements... but that wasn't the solution...
Could you point me in the right direction?
I am a bit confused. What is the calling code for sfRequestHostRoute? Where is this called? How does the calling code know which method to call?
Hi. Great Tutorial. But I'm looking for the same solution as Evert Harmeling is looking for. *.domain.com Would be great if I could do something like:
@dynamic_vhost: url: :slug.domain.com param{ module: vhost, action: show }
For the moment I have a rewrite in lighttpd to do this things:
^(.*).domain.com => index.php/dynamic/$1
And the route in symfony is:
@dynamic_vhost: url: /dynamic/:slug param{ module: vhost, action: show }
My problem here ist, that I cant use the linkhelpers to link back to the regular domain because the host is allways *.domain.com and not http://domain.com or http://www.domain.com. And there are a lot of other helpers that dount function...
Any further Idears would be great
I'm looking for an solution for the problem of Evert Harmeling and Kai.
I want to do something like http://usernameA.site.com http://usernameB.site.com
any ideas?
Hopefully this is a solution:
What the problem was is that we couldn't give a requirement for *.domain.com, and we had more than 1 subdomain. So we have to look from a different angle, if we know what the static subdomains are, we could check on those...
What we've done is we made another HostRouteClass which extends the one from this article, and there check if it's one of the static subs.
class sfDynamicHostRoute extends sfHostRoute { public function matchesUrl($url, $context = array()) { if (sfConfig::has('app_static_host') && sfConfig::has('app_member_host')) { if ($context['host'] == sfConfig::get('app_static_host') || $context['host'] == sfConfig::get('app_member_host')) { return false; } }
} }
Another thing we discovered is that sfRequestRoute overrules the sf_method property and sets it to (from the core sfRequestRoute.class.php): if (!isset($this->requirements['sf_method'])) { $this->requirements['sf_method'] = array('get', 'head'); }
So you loose the post method in case of posting forms... From that point of view we decided to not extend the HostRoute class in this example to sfRequestRoute but to just to sfRoute, and that works.
In your routing.yml you give the requirements of the static subs the requirement sfHostRoute and the dynamic subs the requirement sfDynamicHostRoute...
Hopefully this is a solution for more people!
off the subject : What is the mailing list Kris is talking about ?
Thanks a lot Kris for this excellent example.
I think a lot of people would be interested to know how you would implement this solution on a sfPropelRouteCollection as the class is already defined in the routing.yml and it doesn't seem possible to integrate this behaviour without doing deep modifications to the classes.
Thanks a lot and keep up the good job.
Antoine
Hi, I'm looking at using Symfony for a large project, got the book and all and chose it over cakePHP due to documentation however, this blog is of concern to me.
An critical requirement is for DYNAMIC on the fly sub domains such as memberA.site.com, memberX.site.com .... The site will look and behave the same as www.site.com though the sub part of the domain is to be used as a variable for linking user relationships. I would have thought this to be a common requirement.
Is Symfony set up for this feature yet without loosing any other functionality? If not, is it being worked on now and is there an estimated release date?
Thanks