Extending Zend Framework Route and Router for custom routing
Wednesday, 19. September 2007, 13:25:59
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.
Hi,
bootstrap:
$request = $fc->getRequest();
//Create new route
$route = new Route_Page($dispatcher,$request);
..
$fc->getRequest don't work, because Request object is instantiate in the
$fc->dispatch();
..
patch Route_Page constructor or use this
$request = $request = new Zend_Controller_Request_Http();
Thx for good article and sorry for my bad english
By anonymous user, # 22. September 2007, 23:27:51
I think it might work even without the request, but the Module route needs the Dispatcher for creating the URLs.
Bad english? Where?
By zomg, # 22. September 2007, 23:35:11
Do you have some working part of your CMS? I am working on the CMS too, so we can maybe cooperate ?
By anonymous user, # 23. September 2007, 13:31:19
Do you have some ideas how to make router for this? :
site.com/:language/:module/
thanks
By anonymous user, # 24. September 2007, 16:03:56
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.
By zomg, # 24. September 2007, 20:04:18
thanks
By anonymous user, # 25. September 2007, 00:23:13
Your ideas are interesting, thanks for its.
And in my ZF project i use next schema:
My "routes.xml" file looks like this:
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
By playpauseandstop, # 10. October 2007, 03:02:17
Sorry, for this. But i'm a first time user of ZF and i wonder where i am supposed to put those classes to make it load automatically in my bootstrap.
The bootstrap code you provided gives me:
Fatal error: Class 'Router_Page' not found in /index.php on line 7
The code is a copy of yours, nothing added.
By anonymous user, # 10. December 2007, 14:04:32
By zomg, # 10. December 2007, 19:53:36
Ok, i just thought yours were autoloaded. I solved it through making Zend_Loader::loadClass the default __autoload. Another question though. ;)
I get an error here:
$route = new Route_Page($dispatcher,$request);
The request variable is null no mather how i access the page:
site.com/foo
site.com/foo/bar
site.com/foo/bar/baz
$dispatcher = $fc->getDispatcher();
$request = $fc->getRequest(); // <-- returns null
$route = new Route_Page($dispatcher,$request);
Since the request being null a fatal error is triggered because of the type hinting:
"Zend_Controller_Request_Abstract $request" in Route_Page
I know this might be out of scope for your blog, but i would be glad if you could point me in the right direction.
By anonymous user, # 11. December 2007, 09:52:08
hey i made router for this using?:
site.com/:module/:controller/:action
$route = new Zend_Controller_Router_Route(
':module/:controller/:action/*'
array(
'module' => 'default',
'controller' => 'index',
'action' => 'index'
)
);
$router->addRoute('default', $route);
#################################################################
but when i move to some controller that is my default module(index)
it gives me url like :
site.com/default/:controller/:action
what should i do,so that this default does not comes in url it should be like this:
site.com/:controller/:action
By anonymous user, # 17. December 2007, 06:02:33
Why not create an error handler plugin to handle all exceptions thrown as a result of a missing controller or action. When such an exception is thrown, you can check your CMS database for an entry and display the content of the page. If you want different page types, that information can be stored in the database rather than specified in the URL. If no page exists in the CMS database, return a 404 error.
This would allow the CMS user to create any URL they like without the need to specify any additional routes.
The code for creating such a plugin can be found at:
http://framework.zend.com/manual/en/zend.controller.plugins.html#zend.controller.plugins.standard.errorhandler
By anonymous user, # 12. April 2008, 11:51:29