Call the expert: Adding subdomain requirements to routing.yml

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 familiar sfContext 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!

Comments

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;
}
}

return parent::matchesUrl($url, $context);
}
}

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

Comments are closed.

To ensure that comments stay relevant, they are closed for old posts.