One of the reasons why people like vanity url’s is because they are easy to remember. For example take the following url:
http://www.somesocialsite.com/?user_id=123456789&page=somepage
If this was the url to my page in some social network site, there’s no way I could remember it nor would I be able to easily share the url with others unless I sent them the link. Now, if I could instead create a vanity url that looked like the following:
http://www.somesocialsite.com/joeyrivera
it would be much easier to remember and to share with others. Not only that but now I have a much more search engine friendly url with keywords that I would like to be found under – but ignore the search engine benefits for now.
Why
I’m currently working on an application that can benefit from vanity urls for the reasons mentioned above so I decided to spend some time thinking of ways to implement this. The first way that came to my mind was using mod_rewrite. Mod rewrite lets you manipulate urls. For example, you can write rules in your .htaccess file so when a user goes to http://www.somesocialsite.com/joeyrivera it really calls http://www.somesocialsite.com/search.php?user=joeyrivera or in zend the request would be more like http://www.somesocialsite.com/search/user/name/joeyrivera
Not mod_rewrite
We could make mod_rewrite work but zend already routes all traffic to the index.php file and to me, having this external-to-the-app rules just seems a bit messy. Plus, it’ll be tricky to tell mod_rewrite what urls are valid and which should be handled be zend directly. In the example above, what if joeyrivera was a legitimate controller name? In that case, zend should handle the request.
Zend… yes!
So then I decided I wanted zend framework to handle the vanity urls. I looked at adding rules to the router to take care of this but again I didn’t feel like this would be the best approach plus I would still have the same issue of which is a valid vanity name versus a valid controller name. What I do know is that zend at some point knows if the requested url leads to a valid controller since it throws an exception otherwise. So the search began to find that piece of code.
I reread the basics on zend controller to see where the code I’m looking for should be. Based on this image:
You start with a request which is sent to the router and is then dispatched. Based on the framework documentation, the Zend_Controller_Dispatcher_Interface:
is used to define dispatchers. Dispatching is the process of pulling the controller and action from the request object and mapping them to a controller file (or class) and action method in the controller class. If the controller or action do not exist, it handles determining default controllers and actions to dispatch
Working with the controller dispatcher
So Zend_Controller_Dispatcher_Interface is what handles loading the controller class and takes action if the controller is not valid. This seems like the best place to add my vanity code. My thoughts are to add code to Zend_Controller_Dispatch_Interface so if a controller is not valid, before throwing an exception, it’ll check if the vanity url is valid. If so, it’ll redirect the user to the correct page instead of displaying the contents of the new page. I’m choosing to redirect since I don’t want to have problems with duplicate content in Google or other search engines. There are ways around it, but for now this works well for me.
After reading the docs on the zend controller dispatcher, this part caught my eyes:
in most cases, however, you should simply extend the abstract class Zend_Controller_Dispatcher_Abstract, in which each of these have already been defined, or Zend_Controller_Dispatcher_Standard to modify functionality of the standard dispatcher.
Zend_Controller_Dispatcher_Standard already extends Abstract and has all the current functionality. So what I want to do is create a new class that will extend Zend_Controller_Dispatcher_Standard and rewrite the method that loads the controller file. The method is dispatch:
/**
* Dispatch a request to a (module/)controller/action.
*
* @param Zend_Controller_Request_Abstract $request
* @param Zend_Controller_Response_Abstract $response
* @return Zend_Controller_Request_Abstract|boolean
*/
public function dispatch(
Zend_Controller_Request_Abstract $request,
Zend_Controller_Response_Abstract $response
);
Extending the current dispatcher
I created a new file in my library under my/controller/dispatcher/standard.php that extends Zend_Controller_Dispatcher_Standard. Then I copied the dispatch method from Zend_Controller_Dispatcher_Standard and pasted it into my newly created class. Now I have my new class with one method – dispatch. You can open the file in your zend library folder to see everything the dispatch method does but I’m only going to be modifying the first part:
if(!$this->isDispatchable($request))
{
$controller = $request->getControllerName();
if(!$this->getParam('useDefaultControllerAlways') && !empty($controller))
{
require_once 'Zend/Controller/Dispatcher/Exception.php';
throw new Zend_Controller_Dispatcher_Exception
('Invalid controller specified (' . $request->getControllerName() . ')');
}
$className = $this->getDefaultControllerClass($request);
}
else
{
$className = $this->getControllerClass($request);
if(!$className)
{
$className = $this->getDefaultControllerClass($request);
}
}
isDispatchable checks to see if the controller from the request is valid. For example, if we call: http://www.joeyrivera.com/car, isDispatchable checks to see if car is a valid controller. If it’s valid, the class is loaded and eventually gets called. If it’s not valid, there is a check to see if the dispatch method should load the default controller. If so, it loads it else it throws an exception.
Implementing the vanity code
There are two ways to handle the vanity code at this point: 1 – set useDefaultControllerAlways to true so if an invalid controller is found, the default controller is called anyways and you can then let that controller decide what to do. I can think of pros and cons on doing it that way, cleaner code maybe but more repetitive code also. It seems as if I would be rewriting what this dispatch method already does. I choose the second way which is to just check if my vanity url is valid here. If so, redirect else continue with the exception.
Here’s my code:
if(!$this->isDispatchable($request))
{
$controller = $request->getControllerName();
if(!$this->getParam('useDefaultControllerAlways') && !empty($controller))
{
// before we assume it's bad, check for a vanity name
$user = new My_Model_User();
$user->find($controller);
// check if valid user and redirect
if(!is_null($user->user_id))
{
header("Location:" . parent::getFrontController()
->getBaseUrl() .
"/controller/action/user_id/{$user->user_id}/
first_name/{$user->first_name}/
last_name/{$user->last_name}");
exit();
}
require_once 'Zend/Controller/Dispatcher/Exception.php';
throw new Zend_Controller_Dispatcher_Exception
('Invalid controller specified (' . $request->getControllerName() . ')');
}
$className = $this->getDefaultControllerClass($request);
}
else
{
$className = $this->getControllerClass($request);
if(!$className)
{
$className = $this->getDefaultControllerClass($request);
}
}
If the controller is not valid, and useDefaultControllerAlways is not true, then I create a new instance of my user model and try to find an user with the vanity name passed using the variable $controller which is already set to $request->getControllerName(). The vanity name is a field stored in the database with the rest of the user information. If the user is found, I redirect to the users profile page else I just let the method call the exception as usual. I call parent::getFrontController() so I can use the getBaseUrl() method when rewriting the redirect url.
Updating the bootstrap
This is all for the dispatch method but we aren’t done yet. Now that we have our new class we need to tell the framework to use it. To do this we need to modify the bootstrap class. Here is the code I added to the top of my bootstrap.php file:
protected function _initVanityName()
{
$this->bootstrap('FrontController');
// Retrieve the front controller from the bootstrap registry
$front = $this->getResource('FrontController');
$dispatcher = new My_Controller_Dispatcher_Standard();
$dispatcher->setControllerDirectory($front->getDispatcher()->getControllerDirectory());
$front->setDispatcher($dispatcher);
return $front;
}
The first line makes sure the front controller resource gets initialized. Then we retrieve it via the getResource method. Next we instantiate the new dispatcher My_Controller_Dispatcher_Standard we created and set the controller directory since this won’t work without it. Finally we set the new dispatcher in the front controller instance and we are done.
End Result
Now, our application can handle vanity urls. Instead of having to remember long, complex urls such as
http://www.somesocialsite.com/?user_id=1231231&page=somepage
we can use smaller, more readable urls like
http://www.somesocialsite.com/joeyrivera
Thoughts
I’m still not completely satisfied with this implementation so I’m sure I’ll be tweaking it some but it’s doing what I need it to do. I’m trying to think of ways, probably using cache, so I don’t have to hit the database each time a vanity url or an invalid controller name is passed. My other concern of having my code in the dispatch method is what happens when that method gets updated in future zend framework releases? Then I’ll have to remember to go back and update the method while if this code was in a controller that wouldn’t be an issue.
What do you guys think? Where do you guys implement this code? Let me hear your thoughts and thanks for reading.
You make an excellent point, extending the standard dispatcher is generally not the best thing to do, but after all, ZF is meant to be extended when need be 🙂
Some thoughts-
You could add some semantics to the vanity urls, for example /u/:username would reduce the possibility of collisions between usernames and module/controller names, after all that is the biggest problem with using top level names for vanity’s sake.
You could also go the facebook-like route: force people to opt into vanity urls, what this would allow you to do is check that the name requested is not dispatchable (like a user named “user”). The problem with this is you wont be able to ensure that your future code (modules and controllers) collide with an existing users vanity url. This also allows you to do whatever caching of the list at registration time. (You might consider invalidating the cache when a new url is registered and letting a plugin rebuild the cache only when it needs to, again lots of options here.)
One option might be to use a different 3rd level domain, like member.mysite.com/username, that would redirect to http://www.mysite.com/user/username, etc.
Either way, its interesting code you’ve got there, as for improvements, I’d try to localize everything to inside a route (even if you have to pull in the standard dispatcher). And like you said, a cache is the best way to go since the collection of possible routes only changes when a new user comes into the system.
I did not know that things like that are called vanity URLs! haha.. but i agree with you, they are easier to remember and they really look better on the address bar than those with long series of numbers!