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!

Published in #Call the expert