Vanity URLs in Zend Framework
A question I've seen pop up in a few places is that of vanity URLs, and how to achieve them using frameworks. What I mean by a vanity URL is one where you have a URL structure such as http://example.com/<something>, where something is some kind of user-generated string stored in the database, perhaps to give users a more memorable profile URL. Twitter is the obvious example of this, e.g. my twitter page is http://twitter.com/tfountain.
How would you setup routing for this? Easy, you might think, you just create a standard route containing a 'username' variable:
$route = new Zend_Controller_Router_Route(
':username',
array(
'controller' => 'users',
'action' => 'profile'
)
);
But what if you want to add http://twitter.com/about and route it to a different place? Your router has no way of knowing whether 'about' is a username or not, so you end up having to add a load of static routes for your normal pages, leaving your username route as the default one checked last.
What if you then want a top level vanity URL for another feature? Say you introduced a groups feature, where groups can be setup with URLs like http://twitter.com/<group name>. You now have two identical URL structures which need to route to different places.
This is something that can be quite difficult to do with a lot of frameworks. Most follow the Rails-style approach where you define a load of URL patterns in a routes file along with details of where each should map to. This is easy to use and will cover 90% of routing cases. But for the other 10% you're screwed - you're left with a handful of messy options which either involve bypassing the routing process or compromising on your desired URL structure.
In my opinion the biggest strength of Zend Framework is its flexibility, and the routing is a great example of this. You can just load all your routes in from a file in ZF, but you can also create route objects and assign them to the router directly, or define your own route types and assign these, or extend the router class with some customisations, or define your own router; or some combination of these options. There's a slightly bigger learning curve, but you're able to setup your routing to match your application's needs, rather than having to change your application to work within the limitations of the framework.
In ZF a custom route class is the way to go to for vanity URLs. Remember how I said the router has no way of knowing whether 'about' is a username or not? Well if you create a 'username' route class that checks the database to see whether a particular string is a user or not, then suddenly your router can handle these routes like any other. You are also able to add additional route classes for any other vanity URL structures you need.
A custom route class at its most basic just needs to implement the Zend_Controller_Router_Route_Interface. This defines three methods:
interface Zend_Controller_Router_Route_Interface {
public function match($path);
public function assemble($data = array(), $reset = false, $encode = false);
public static function getInstance(Zend_Config $config);
}
the match function is where the action is - this takes the path from the URL as a parameter, and should return an array of params to be passed to the controller if the route matched, false if it did not. The params array should also contain the module, controller and action name that tell the router which part of your application to route the request to.
I'd recommend extending the Zend_Controller_Router_Route_Abstract class, as this is used by all of the standard ZF route classes and will allow you to follow some of their conventions.
Here's an basic example for a username route:
<?php
class Application_Route_User extends Zend_Controller_Router_Route
{
public static function getInstance(Zend_Config $config)
{
$defs = ($config->defaults instanceof Zend_Config) ? $config->defaults->toArray() : array();
return new self($config->route, $defs);
}
public function __construct($route, $defaults = array())
{
$this->_route = trim($route, $this->_urlDelimiter);
$this->_defaults = (array)$defaults;
}
public function match($path, $partial = false)
{
if ($path instanceof Zend_Controller_Request_Http) {
$path = $path->getPathInfo();
}
$path = trim($path, $this->_urlDelimiter);
$pathBits = explode($this->_urlDelimiter, $path);
if (count($pathBits) != 1) {
return false;
}
// check database for this user
$result = Zend_Registry::get('db')->fetchRow('SELECT userID, username FROM users WHERE username = ?', $pathBits[0]);
if ($result) {
// user found
$values = $this->_defaults + $result;
return $values;
}
return false;
}
public function assemble($data = array(), $reset = false, $encode = false)
{
return $data['username'];
}
}
(This example assumes that there's a Zend_Db instance in the registry under the key 'db'.)
All it's doing is extracting the string from the path and looking this up in the database. If a match is found, it adds the user ID and username to an array that already contains the module, controller and action names and passes this back to the router.
To use this route you assign it to the router during your bootstrap:
protected function _initRoutes()
{
$router = Zend_Controller_Front::getInstance()->getRouter();
// general page route
$router->addRoute('about', new Zend_Controller_Router_Route(
':page',
array(
'module' => 'default',
'controller' => 'pages',
'action' => 'show'
)
));
// username route
$router->addRoute('user', new Application_Route_User(
'user',
array(
'module' => 'default',
'controller' => 'users',
'action' => 'show'
)
));
}
You'll see I've included a page route here as well, and note that I can now use a more conventional :page variable route instead of a static route, as there's no longer any ambiguity. This way you don't have to modify your routes whenever you add a new page.
I've only scratched the surface of what you can do with ZF's routing. I've used a similar approach (custom route class) for hierarchical routes, where you have /category/sub-category/sub-sub-category type structure, but that is a topic for another blog post.
Comments
22:48, on 12/9/2010
20:39, on 28/10/2010
I have tried to use your route but there is something wrong
The default route is working www.something.com/fr/controller/action the route for vanity url is ok too: www.something.com/fr/one-url-in-data-base
But if there are params (www.something.com/fr/one-url-in-data-base/key1/value1/key2/value2) is the default route that was used => I don't understand why.
Can you help me?
The routes:
$router = $this->_front_controller->getRouter(); // general purpose page route $arr_cond_lang['route_str'] =''; $arr_default['module'] ='default'; if(count($this->_arrLangAvailable)>1){ $arr_cond_lang['route_str'] =':language/'; $arr_default['language'] = NULL ; } $arr_default[ 'controller'] ='index'; $arr_default['action'] ='index'; //mvc route $route = new Zend_Controller_Router_Route( $arr_cond_lang['route_str'].':@controller/:@action/*',$arr_default ); // pages route $arr_default_page[ 'controller']='page'; $arr_default_page['action'] ='show'; $arr_default_page['page'] ='pagevar'; $routePage = new Bewaw_Route_Page(':page',$arr_default); //$arr_cond_lang['route_str'].'/:page' $router->removeDefaultRoutes(); $router->addRoute('page', $routePage);The class "Page":class Bewaw_Route_Page extends Zend_Controller_Router_Route { public $db = null; public $lang = null; public static function getInstance(Zend_Config $config) { $defs = ($config->defaults instanceof Zend_Config) ? $config->defaults->toArray() : array(); return new self($config->route, $defs); } public function __construct($route, $defaults = array()) { $this->db = Zend_Db_Table::getDefaultAdapter(); $this->lang = Zend_registry::get('Zend_Translate')->getLocale(); $this->_route = trim($route, $this->_urlDelimiter); $this->_defaults = (array)$defaults; } public function match($path, $partial = false) { if ($path instanceof Zend_Controller_Request_Http) { $path = $path->getPathInfo(); } $path = trim($path, $this->_urlDelimiter); $pathBits = explode($this->_urlDelimiter, $path); if (count($pathBits) != 2) { return false; } // check database for this user $result = $this->db->fetchRow('SELECT id_page, url, langs.id_lang FROM pages LEFT JOIN langs ON pages.id_lang = langs.id_lang WHERE url = ? AND lang_short_text=?', array($pathBits[1],$this->lang)); if ($result) { // user found $values = $this->_defaults + $result; return $values; } return false; } //not used public function assemble($data = array(), $reset = false, $encode = false) { return $data['page']; } }Joseph20:41, on 28/10/2010
20:44, on 28/10/2010
16:22, on 8/2/2011
22:21, on 21/4/2011
It would seem trivial to set the username route as /u/:username, and groups as /g/:group, without requiring too much of the user.
06:58, on 26/4/2011
12:41, on 26/4/2011
21:22, on 3/2/2012
06:29, on 27/3/2012
18:11, on 17/5/2012
14:39, on 18/5/2012
$this->getRequest()->getParam(0)->id_page
Add Comment