Extending Zend Framework Route and Router for custom routing
Wednesday, September 19, 2007 1:25:59 PM
The default routing scheme is like this: site.com/module/controller/action. While this works just fine for "normal" sites, it doesn't work very well for a CMS. This is because you would need to create copies of controllers if you wanted to have many similar pages or having longer URLs with the page name as a parameter in the end.
I wanted a better URL scheme for my CMS: site.com/module/page/action where module and action are optional. As you can see, there is no variable for the controller, so how do we tell the framework which controller the request needs to be routed to?
The basic idea behind this scheme is that it allows me to have a single controller which parses a single page type. The page parameter in the URL would be used to determine which page's content is in question and also which controller is used. This is achieved by saving each page in a database along with a page type. The page type is also saved in the database with the name of the controller which is used to parse this type of page.
To be able to do this, we need to create a custom Zend_Controller_Router_Route to represent our custom URL scheme and a custom Zend_Controller_Router to query the database for the page type and things like that.
Creating the custom route
I decided to extend the built-in class Zend_Controller_Router_Route_Module for this because it has everything we need except for one minor change.
This change is very simple: Because we want to use a parameter called page in place of controller, we just extend the Module route and change the controllerKey:
<?php
require_once 'Zend/Controller/Router/Route/Module.php';
class Route_Page extends Zend_Controller_Router_Route_Module
{
public function __construct(Zend_Controller_Dispatcher_Interface $dispatcher,Zend_Controller_Request_Abstract $request)
{
parent::__construct(array('module'=>'','action'=>'index','page'=>''),$dispatcher,$request);
$this->_controllerKey = 'page';
}
}
?>
Simple huh? Now our request will contain a parameter called "page" instead of controller. All we need to do now is to make a custom router which understands this url scheme.
Creating a custom router
So the router needs to understand the page parameter in the request.
It needs to query the database for the controller for the page type of the page parameter. I also made the default value for page be '', so when the page is '', the router should also be able to fetch the default/index page.
For this, we can again extend a builtin class: Zend_Controller_Router_Rewrite.
The routing magic is done in the route method in the router class, so that's what we need to override and change a bit.
<?php
require_once 'Zend/Controller/Router/Rewrite.php';
class Router_Page extends Zend_Controller_Router_Rewrite
{
public function route(Zend_Controller_Request_Abstract $request)
{
//Let the Rewrite router route the request first
$request = parent::route($request);
if($request->getParam('page') == '')
{
//If the page param isn't set, route to default page and controller
$defaultPage = PageManager::getInstance()->getDefaultPage();
$request->setControllerName($defaultPage->pageType->controller);
$request->setParam('page',$defaultPage->page);
}
else
{
//Route to current page's controller
$page = PageManager::getInstance()->getPage($request->getParam('page'));
$request->setControllerName($page->pageType->controller);
}
return $request;
}
}
?>
This isn't very complicated either. First, we call the parent route method which will parse the parameters for the URL, then we simply check the page parameter from the request and determine the name of the controller.
The PageManager class used in this is just an example, loosely based on the PageManager and the classes it returns in my CMS. It's up to you to create your own implementation of a class which is used to get the pages from the database, however here's a suggestion for the database tables:
Table pages, columns id, page, page_type, perhaps contents and things like that.
Table page_types, columns id, controller
The pages table should contain rows for each page. the ID column is the ID of the page, the page column is the page parameter name and page_type column should contain a reference to the page_types table.
The page_types table should simply contain each page type and the controller which parses them.
Putting our new classes to use
To have Zend Framework use our shiny new classes instead of the defaults, we need to assign them to the front controller in our bootstrap.
$fc = Zend_Controller_Front::getInstance();
//Create the new router
$router = new Router_Page();
//Route_page needs dispatcher and request
$dispatcher = $fc->getDispatcher();
$request = $fc->getRequest();
//Create new route
$route = new Route_Page($dispatcher,$request);
//Replace default route in our router with our own route
$router->addRoute('default',$route);
//Replace the default router with our own
$fc->setRouter($router);
So...
Now that you have a custom router and a custom route, you can just write controllers that check the page parameter and use it to fetch the page-specific content. This way you can easily re-use controllers for multiple pages while still keeping the URL's short.
Despite seeming like a big change to routing, getting the pages and figuring out the controllers from a database is actually very simple as you can see.
I hope this is of assistance to anyone who wants to change the way the Zend Framework performs routing.







Anonymous # Saturday, September 22, 2007 11:27:51 PM
Janizomg # Saturday, September 22, 2007 11:35:11 PM
I think it might work even without the request, but the Module route needs the Dispatcher for creating the URLs.
Bad english? Where?
Anonymous # Sunday, September 23, 2007 1:31:19 PM
Anonymous # Monday, September 24, 2007 4:03:56 PM
Janizomg # Monday, September 24, 2007 8:04:18 PM
$route = new Zend_Controller_Router_Route( ':language/:module/:controller/:action/*' array( 'language' => 'english', 'module' => 'default', 'controller' => 'index', 'action' => 'index' ) ); $router->addRoute('default', $route);It will go to the default module's IndexController's indexAction by default and the default language parameter will be "english"
There's a lot of info on routing in the Zend Framework manual, Zend Controller Router section, so remember to check it out.
Anonymous # Tuesday, September 25, 2007 12:23:13 AM
Igor Davydenkoplaypauseandstop # Wednesday, October 10, 2007 3:02:17 AM
Your ideas are interesting, thanks for its.
And in my ZF project i use next schema:
<?php // Inits instance of Zend_Controller_Framework $ctrl = Zend_Controller_Front::getInstance(); // Gets default router $router = $ctrl->getRouter(); // Gets all valid routes, auto-created by control panel // This routes are not standart, not dispatched by default // rule :module/:controller/:action/* $validRoutes = new Zend_Config_Xml('routes.xml', null); // Adds this routes to router foreach ($validRoutes as $validRoute) { $routeName = $validRoute->defaults->module[0] . '_'; $routeName .= str_replace('/', '', rtrim($validRoute->route, '/')); $router->addRoute( $routeName, new Zend_Controller_Router_Route( $validRoute->route . '*', $validRoute->defaults->toArray() ) ); } // Updates front controller $ctrl->returnResponse(true) ->setRouter($router) ->throwExceptions(true); // Now we can to dispatch current uri, gets response $response = $ctrl->dispatch(); ?>My "routes.xml" file looks like this:
<routes> <routeFirst> <route>news/</route> <defaults> <module>content</module> <controller>category</controller> <action>category</action> <id>1</id> </defaults> </routeFirst> <routeSecond> <route>news/today-is-very-very-hot</route> <defaults> <module>content</module> <controller>entry</controller> <action>entry</action> <id>1</id> </defaults> </routeSecond> </routes>So, I separate some Content-like modules from another used by project. Content-like modules get their friendly URLs and other works with default ZF router.
ps. Code can contain some errors
Best regards,
Igor Davydenko
Anonymous # Monday, December 10, 2007 2:04:32 PM
Janizomg # Monday, December 10, 2007 7:53:36 PM
Anonymous # Tuesday, December 11, 2007 9:52:08 AM
Anonymous # Monday, December 17, 2007 6:02:33 AM
Anonymous # Saturday, April 12, 2008 11:51:29 AM
Anonymous # Sunday, August 24, 2008 6:47:10 AM
Anonymous # Wednesday, January 28, 2009 4:58:52 AM
Anonymous # Thursday, April 16, 2009 3:27:48 PM
Anonymous # Saturday, April 18, 2009 6:34:11 AM
Anonymous # Friday, June 12, 2009 12:56:48 PM
Anonymous # Wednesday, July 15, 2009 9:37:57 PM
Anonymous # Monday, May 10, 2010 2:53:52 AM
Anonymous # Thursday, October 28, 2010 2:08:46 PM