Well, I’m oficially a CakePHP convert – so much so that I’m in the middle of writing a general purpose CMS based on the excellent framework (I’ll probably convert this blog to Cake if I ever get the time!). One of the most convenient aspects of Cake is its plugin feature, which allows for easy code drop-in from project to project (a feature that my CMS is based on). One shortcoming, however, is that – in an Interwebs world full of ‘pretty’ URLs – plugin URLs are unncessarily long, since by default they include the plugin name as well as the controller and action. I would much rather get rid of the plugin name in the URL and just have the Router figure things out, you know, automagically.
For example, if we have a “calendar” plugin with a “CalendarEvents” controller that has a “view” method, the URL will look something like this:
http://mydomain.com/calendar/CalendarEvents/view
That’s not terrible (certainly better than ?plugin=calendar&page=events&action=view blahblah), but a little lengthy for my liking. We could just rename CalendarEventsController to EventsController, but Events is a pretty general controller name so we could have a future conflict. It’s always best to prefix any plugin controller names with the name of the plugin itself.
So what to do? I’d love to have this:
http://mydomain.com/calendar/events/view
More intuitive and concise, I think. And, since I couldn’t find anything in the Cookbook, Bakery, Forge or Google group that implemented this to my liking, I gave it a go myself. The process is actually quite simple, and only involves editing a couple of files.
The first thing we need to do is find a way to gather up all of our plugin controllers. Tucked away in one of the Bakery’s many tutorials, I found a method called, aptly enough, “_get_plugin_controller_names()”. The tutorial on the Bakery puts this method inside of the AppController, but I’m going to put it in app/config/bootstrap. I’ve also modified it slightly so that instead of returning a flat list, it returns a multi-level array organized by plugin. Here’s the code:
/**
* Get the names of plugin controllers
* Adapted from: http://book.cakephp.org/view/647/An-Automated-tool-for-creating-ACOs
* /app/config/boostrap.php
*
* This function will get an array of the plugin controller names, and
* also makes sure the controllers are available for us to get the
* method names by doing an App::import for each plugin controller.
*
* @return array of plugin names.
*
*/
function _get_plugin_controller_names(){
App::import('Core', 'File', 'Folder');
$paths = Configure::getInstance();
$folder =& new Folder();
// Change directory to the plugins
$folder->cd(APP.'plugins');
// Get a list of the files that have a file name that ends
// with controller.php
$files = $folder->findRecursive('.*_controller.php');
// Get the list of plugins
$Plugins = Configure::listObjects('plugin');
// Loop through the controllers we found in the plugins directory
$names = array();
foreach($files as $f => $fileName)
{
// Get the base file name
$file = basename($fileName);
// Get the controller name
$file = Inflector::camelize(substr($file, 0, strlen($file)-strlen('_controller.php')));
// Loop through the plugins
foreach($Plugins as $pluginName){
if (preg_match('/^'.$pluginName.'/', $file)){
// First get rid of the App controller for the plugin
// We do this because the app controller is never called
// directly ...
if (preg_match('/^'.$pluginName.'App/', $file)){
unset($files[$f]);
} else {
if (!App::import('Controller', $pluginName.'.'.$file))
{
debug('Error importing '.$file.' for plugin '.$pluginName);
}
/// Now prepend the Plugin name ...
// This is required to allow us to fetch the method names.
$names[$pluginName][] = $file;
}
break;
}
}
}
return $names;
}
When called, it’ll look something like this:
$plugins = _get_plugin_controller_names();
debug($plugins);
Array
(
[Calendar] => Array
(
[0] => CalendarEvents
)
[Faq] => Array
(
[0] => FaqFaqs
)
)
[/sourcecode]
Great! Now we just need to put it in our router and add some code to interpret the findings. So, somewhere in /app/config/routes.php (before the last catch-all route of course), add something like this:
foreach ($pluginControllers as $plugin => $controllers) {
foreach ($controllers as $controller) {
if (substr($controller, 0, strlen($plugin)) == $plugin) {
$controller = substr($controller, strlen($plugin));
Router::connect('/(?i)'.$plugin.'/'.$controller.'/:action/*', array('plugin' => $plugin, 'controller' => $plugin.'_'.$controller));
Router::connect('/(?i)admin/'.$plugin.'/'.$controller.'/:action/*', array('plugin' => $plugin, 'controller' => $plugin.'_'.$controller, 'admin' => true));
}
}
}
Basically, what I'm doing is looping through the array of plugins, and, within each plugin, looping through the array of controllers. I do a check to make sure the controller name is prefixed with the plugin name (so "CalendarEvents" is in the "Calendar" plugin), and, if so, I remove the plugin name from the name of the controller and pass along all of the information to the Router. I've also thrown in an extra line to ensure that my admin actions are properly routed.
And that's it! I'm not sure if it's the ideal solution, or if it's 'Cakey' enough for the established users out there, but it works for me and is simple enough. I'd love to hear comments, improvements, etc.
2 Responses to CakePHP: Prettier URLs for Plugins
deizel.
May 29th, 2009 at 6:30 am
While not always a solution, it is possible to have Cake automagically hide the plugin’s name in the URL by having the your plugin and plugin’s controller both share the same name.
So using your example above:
http://mydomain.com/calendar/calendar_events/view
.. if we rename the plugin to `calendar_events`, the url becomes:
http://mydomain.com/calendar_events/view
.. or, if we rename the controller to `calendar`, the url becomes:
http://mydomain.com/calendar/view
See the second bullet point here: http://book.cakephp.org/view/119/Plugin-Tips
Jamie
June 4th, 2009 at 10:36 am
Great tip! Thanks for pointing that out, I missed that second bullet point in the book. I would’ve approved your comment sooner, but for whatever reason WP didn’t send me email notification and I haven’t logged in for a few days.
Your suggestion works well for simple plugins that only have one controller, since you can just give the controller the same name as the plugin. But if you have more than one controller, things get a little sticky.