Hello World

Practical programming... and stuff...

Extending Zend Framework Route and Router for custom routing

,

Continuing on posts related to making a CMS with the Zend Framework, todays topic is Zend_Controller_Router_Route, Zend_Controller_Router and customizing the routing in Zend Framework.

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.

Singleton pattern vs Static classesWeb bugs

Comments

Anonymous Saturday, September 22, 2007 11:27:51 PM

rooticzek writes: 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 ;)

Janizomg Saturday, September 22, 2007 11:35:11 PM

Yeah you're right - it doesn't return much.

I think it might work even without the request, but the Module route needs the Dispatcher for creating the URLs.


Bad english? Where? smile

Anonymous Sunday, September 23, 2007 1:31:19 PM

rooticzek writes: Do you have some working part of your CMS? I am working on the CMS too, so we can maybe cooperate ? :)

Anonymous Monday, September 24, 2007 4:03:56 PM

mysterio writes: Do you have some ideas how to make router for this? : site.com/:language/:module/:page(controller)/:action thanks

Janizomg Monday, September 24, 2007 8:04:18 PM

You should be able to do the route you mentioned with the rewrite router, you just need to make a custom route. Perhaps something like this:

$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

mysterio writes: thanks

Igor Davydenkoplaypauseandstop Wednesday, October 10, 2007 3:02:17 AM

Hi,

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 wink

Best regards,
Igor Davydenko

Anonymous Monday, December 10, 2007 2:04:32 PM

Anonymous writes: 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.

Janizomg Monday, December 10, 2007 7:53:36 PM

I'm not sure how to make them load automagically, but you can just use require_once or such.

Anonymous Tuesday, December 11, 2007 9:52:08 AM

Anonymous writes: 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.

Anonymous Monday, December 17, 2007 6:02:33 AM

Amit Hasija writes: 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

Anonymous Saturday, April 12, 2008 11:51:29 AM

SimonSharks writes: 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

Anonymous Sunday, August 24, 2008 6:47:10 AM

Anonymous writes: or in a __call function - for actions at least

Anonymous Wednesday, January 28, 2009 4:58:52 AM

Laxmikanth writes: Hi I trying to develop CMS with zend framework. At last i got this Article after long search i did in google. i am trying to make one sample with above article. But iam getting error when i am sending request . i have created "Route_Page extends Zend_Controller_Router_Route_Module" and "Router_Page extends Zend_Controller_Router_Rewrite" http://localhost/zendsite/module/page/index Its throwing fatal error: Fatal error: Uncaught exception 'Zend_Controller_Dispatcher_Exception' with message 'Invalid controller specified (module)' in /Users/xxxxxxxxxxx/Sites/zendsite/library/Zend/Controller/Dispatcher/Standard.php:249 Stack trace: #0 /Users/xxxxxxxxxxx/Sites/zendsite/library/Zend/Controller/Front.php(946): Zend_Controller_Dispatcher_Standard->dispatch(Object(Zend_Controller_Request_Http), Object(Zend_Controller_Response_Http)) #1 /Users/xxxxxxxxxxx/Sites/zendsite/index.php(69): Zend_Controller_Front->dispatch() #2 {main} thrown in /Users/xxxxxxxxxxx/Sites/zendsite/library/Zend/Controller/Dispatcher/Standard.php on line 249. Please help me to fix this problem.

Anonymous Thursday, April 16, 2009 3:27:48 PM

Robsta writes: Hi Guys, I had to run a '$router->removeDefaultRoutes();' in my bootstrap to get the custom router to work. I'm using ZF 1.7.8. Cheers, Rob http://robsta.id.au

Anonymous Saturday, April 18, 2009 6:34:11 AM

Robsta writes: Hello again, I forgot to mention that I also had to add: '$request->setControllerKey('page');' in Route_Page to get this working. Cheers, Rob http://robsta.id.au

Anonymous Friday, June 12, 2009 12:56:48 PM

Anonymous writes: While implementing above code Im getting below error. Catchable fatal error: Argument 1 passed to Zend_Controller_Router_Abstract::__construct() must be an array, object given, called in index.php on line 153 and defined in \library\Zend\Controller\Router\Abstract.php on line 57 153 line is $route = new Router_Page($dispatcher,$request); Can anyone tell me the solution?

Anonymous Wednesday, July 15, 2009 9:37:57 PM

Anonim writes: Could someone update this article for zf 1.8??

Anonymous Monday, May 10, 2010 2:53:52 AM

vinapoint writes: thẻ tích và tiêu điểm đầu tiên tại việt nam, dành cho các bạn thích mua sắm, ăn uống, giải trí,... với nhiều ưu đãi đặt biệt chỉ có ở http://vinapoint.vn

Anonymous Thursday, October 28, 2010 2:08:46 PM

Anonymous writes: Seriously all this for a simple routing rule ? 0_o

How to use Quote function:

  1. Select some text
  2. Click on the Quote link

Write a comment

Comment
(BBcode and HTML is turned off for anonymous user comments.)

If you can't read the words, press the small reload icon.


Smilies