Using Absolute URL's In The View
We recently had a project at work that involved replacing all the relative URL's from the application with absolute URL's. In the past, developers had just hard-coded an absolute URL only when they need to force the browser over to https. Now we are using multiple subdomains, so this approach is no longer sufficient. We also wanted a way to easy rotate assets through multiple CDN URL's to speed up the time it takes a user's web browser to load all the content.
There are two requirements:
- Prefix a relative url path with a host.
- Rotate a set of relative url's through a given number of cdn hosts.
Another problem is that we need absolute url's in some of our business logic. We have webservices that serve up XML or JSON that contain locations to such as images or a catalog. We want these services to take advantage of the absolute url logic too. If we implement a view helper, then the service layer becomes coupled to the view in order to reuse the logic.
Enter PHP 5.3, functional programming and an inspirational post from Eli White. We can use the factory method pattern and closures to meet all the requirements and isolate the parts that change. Read these posts if you are not familiar with functional programming in PHP.
class Crimson_Url
{
public static function absolute($host)
{
return function($path) use ($host) {
return "{$host}{$path}";
};
}
public static function rotate($subdomain, $host, $rotations, $protocol='http')
{
$current = 0;
return function($path) use (&$current, $subdomain, $host, $rotations, $protocol) {
if ($current == $rotations) {
$current = 1;
} else {
$current++;
}
return "{$protocol}://{$subdomain}{$current}.{$host}{$path}";
};
}
}
The absolute function demonstrates how simple, yet powerful, closures can be. The absolute function is creating a function that concatenates to strings together: a host name and a relative url. Here is how we would use this:
$www = Crimson_Url::absolute('http://www.example.com');
$ssl = Crimson_Url::absolute('https://www.example.com');
echo $www('/foo.jpg'), PHP_EOL;
echo $ssl('/secure.php'), PHP_EOL;
Output:
http://www.example.com/items.php https://www.example.com/secure.php
Take this one step further and think about how you can use the HTTP_HOST or HTTP_REFERER values from $_SERVER to make absolute URL generation almost completely automatic.
The rotate function is slightly more complex. The $current variable is being declared in the factory method and passed by reference. We must explicitly pass this by reference otherwise PHP will pass it by value and the value of $current will be 1 everytime. Here is how we would use the rotater:
$cdn = Crimson_Url::rotate('cdn', 'hautelook.com', 3);
$sslCdn = Crimson_Url::rotate('cdn', 'hautelook.com', 3, 'https');
echo $cdn('/foo.jpg'), PHP_EOL;
echo $cdn('/bar.jpg'), PHP_EOL;
echo $cdn('/baz.jpg'), PHP_EOL;
echo $cdn('/bob.jpg'), PHP_EOL;
echo $sslCdn('/foo.jpg'), PHP_EOL;
echo $sslCdn('/bar.jpg'), PHP_EOL;
Output:
http://cdn1.hautelook.com/foo.jpg http://cdn2.hautelook.com/bar.jpg http://cdn3.hautelook.com/baz.jpg http://cdn1.hautelook.com/bob.jpg https://cdn1.hautelook.com/foo.jpg https://cdn2.hautelook.com/bar.jpg
Notice how there are no problems with static variable conflicts. Each function is independent and can be used for completely different tasks.
The complete source code, with tests and documentation, can be found here: http://github.com/hradtke/crimson/tree/master/Crimson_Url