« Zend_View helpers in include path | New Wishlist site »

Managing 404 errors in the Zend Framework

16th February 2007

This applies to version 0.7 of the Zend Framework. Changes for later versions of the framework are at the end of the article.

Early versions of the Zend Framework had a noRoute action that was called when the correct action couldn’t be found. This was a way to deal with some page not found errors. At some point it was dropped - I don’t know when or why because I only started using the Zend Framework recently. It’s still possible to handle non-existent actions using the __call() method of the controller class. But there’s no obvious way to deal with all page not found errors in one place, including instances where the controller doesn’t exist.

The Zend Framework is based around controllers and actions, using URL’s of the form http://www.example.com/controller/action. If no action is specified the index action is used, and if no controller is specified the index controller is used. You can modify the mapping of URLs to controllers and actions by setting up different rewrite routers in the front controller.

A 404 error should occur when the controller or action specified in the URL aren’t defined. But instead of a 404 error, the Zend Framework throws an exception because the controller class or the action method can’t be found. There’s nothing worng with this - it leaves it for each developer to decide what to do. So what should we do?

To generate an appropriate 404 error with a custom page we need either to intercept the request before the error occurs or to catch the exception after the error occurs, and in either case to redirect to an appropriate controller and action.

I created a controller plug-in to do just that. In the pre-despatch method it checks the controller class can be loaded and that it contains the required action. If either is not the case it redirects the request to the index action of the noroute controller. Then one just creates an appropriate noroute controller to display the page not found error.

Here’s the plug-in code.

require_once 'Zend/Controller/Plugin/Abstract.php';

class Bigroom_Controller_Plugin_Noroute
                           extends Zend_Controller_Plugin_Abstract
{
    public function preDispatch(
                       Zend_Controller_Request_Abstract $request )
    {
        $dispatcher = Zend_Controller_Front::getInstance()
                                                ->getDispatcher();
        
        $controllerName = $request->getControllerName();
        if (empty($controllerName)) {
            $controllerName = $dispatcher->getDefaultController();
        }
        $className = $dispatcher
                          ->formatControllerName($controllerName);
        if ($className)
        {
            try
            {
                // if this fails, an exception will be thrown and
                // caught below, indicating that the class can't
                // be loaded.
                Zend::loadClass($className,
                           $dispatcher->getControllerDirectory());
                $actionName = $request->getActionName();
                if (empty($actionName)) {
                    $actionName = $dispatcher->getDefaultAction();
                }
                $methodName = $dispatcher
                                  ->formatActionName($actionName);
                
                $class = new ReflectionClass( $className );
                if( $class->hasMethod( $methodName ) )
                {
                    // all is well - exit now
                    return;
                }
            }
            catch (Zend_Exception $e)
            {
                // Couldn't load the class. No need to act yet,
                // just catch the exception and fall out of the
                // if
            }
        }
    
        // we only arrive here if can't find controller or action
        $request->setControllerName( 'noroute' );
        $request->setActionName( 'index' );
        $request->setDispatched( false );
    }
}

Simply register this plug-in with the front controller and set-up the NorouteController class to display the error page. And remember to send a 404 error header from the noroute controller.

The plug-in doesn’t deal with modules - mainly because I don’t use modules and couldn’t decide whether the NorouteController class should be global or per-module. It shouldn’t be too hard to add module handling to the code.

Zend Framework 0.9

For version 0.9 of the framework you need to change Zend::loadClass to Zend_Loader::loadClass. With this change the plug-in works again as intended.

Read more articles about PHP, Zend Framework 

15 Comments

  • Thanks for the code, I was thinking about writing something like this soon!

    Setting dispatch = false in preDispatch() causes an endless loop. Unless I’m not completely understanding this.

    Will Prater  |  18th February 2007 at 3:23 am

  • Will

    Setting _dispatched = false on the request object ($request->setDispatched( false );) causes the dispatcher to start again with the new route. That means this and other plug-ins will get called again on the new route.

    You would get an endless loop if the preDispatch method always set _dispatched to false, but this plug-in only does so for invalid routes and after altering the route to a valid one (noroute/index). So it will only loop a maximum of once.

    richard  |  18th February 2007 at 4:19 pm

  • Thanks for the explanation.

    My issue was that I had not yet created the NorouteController! You may want to add a note to the blog entry. :)

    Will Prater  |  18th February 2007 at 10:36 pm

  • hi

    regarding forwarding to a 404 page
    http://blog.ixti.ru/2007/01/17/howto-handle-unexistence-
    controllersactions-with-zend-framework-recovered/#more-17

    shows quite a simple method for this
    simply use
    $this->_forward(’controller’, ‘action’);
    inside of your ‘whatever’Action() function to reroute to a different controller/action

    i’m not sure if it’s completely necessary or not (it was for me), but i had to make sure to put a return statement after that call to get it to redirect to the 404 controller properly

    dave

    dave  |  19th February 2007 at 11:26 pm

  • Dave

    The method you describe can only reroute to another action if the original action exists in the first place, and by definition a 404 error occurs because either the controller or action doesn’t exist.

    The article you mention uses a feature in early versions of the Zend Framework to redirect requests for non-existent controllers to the noRoute action in the index controller. This behaviour has since been removed from the Zend Framework so the technique in the article will no longer work.

    richard  |  20th February 2007 at 8:28 am

  • ahh yes, no route/controller
    i might have to steal your code for that :D

    dave  |  20th February 2007 at 11:23 am

  • Thank you for the code, it does really what I need.

    Sincerely,
    Alexander

    Alexander  |  9th March 2007 at 12:40 am

  • I try to handle 404 on my Website using ZF 0.9 !
    Works well with your noRoute plugin. Unfortunately
    I don’t how can retur correct Status ?

    Google Sitemap check if really site can handle 404 properly… using mod_rewrite seems that Apache won’t return correct status code ?

    I’ve tried this in my noRoute method

    $this->getResponse()->setHeader(’Status’,'404 File not found’);

    But doen’t work as expected …

    Any tips, ideas, solutions … ?

    Regards

    Sébastien Cramatte  |  23rd March 2007 at 2:31 pm

  • I’ve resolve the problem …
    You must add this to your noRoute Method

    $this->getResponse()->setHeader(’HTTP/1.1′,’404 Not Found’);
    $this->getResponse()->setHeader(’Status’,'404 File not found’);

    Sébastien Cramatte  |  23rd March 2007 at 2:52 pm

  • I don’t certainly sure that ZF 0.7 does have such availability, but ZF >= 0.9 does… So this plugin can be simplified like this:

    public function preDispatch(Zend_Controller_Request_Abstract $request)
    {
    $dispatcher = Zend_Controller_Front::getInstance()->getDispatcher();

    try {
    /* @var $dispatcher Zend_Controller_Dispatcher_Standard */
    $controllerClass = $dispatcher->getControllerClass($request);
    $className = $dispatcher->loadClass($controllerClass);
    /* now you can doo all actions with Reflectioning, like you do */
    } catch (Zend_Exception $e) {
    $request->setControllerName($this->getNorouteControllerName());
    $request->setActionName($this->getNorouteActionName());
    $request->setDispatched(false);
    }
    }

    Aleksey Zapparov A.K.A. iXTi  |  16th April 2007 at 8:28 am

  • Sorry for my chain-post, but just to explain…

    $this->getNorouteControllerName()
    and
    $this->getNorouteActionName()

    Are used by me to get string ‘noroute’ and ‘index’ :))

    Aleksey Zapparov A.K.A. iXTi  |  16th April 2007 at 8:30 am

  • Excuse me again. I’ve just done full code :)) So your plugin can be like this:

    public function preDispatch(Zend_Controller_Request_Abstract $request)
    {
    $dispatcher = Zend_Controller_Front::getInstance()->getDispatcher();

    try {
    /* @var $dispatcher Zend_Controller_Dispatcher_Standard */
    $controllerClass = $dispatcher->getControllerClass($request);
    $controllerclassName = $dispatcher->loadClass($controllerClass);
    $actionName = $dispatcher->getActionMethod($request);

    $controllerObject = new ReflectionClass($controllerclassName);
    if ($controllerObject->hasMethod($actionName)) {
    // Controller and action exists, so we can securely continue
    return;
    }
    } catch (Zend_Exception $e) {
    }

    $request->setControllerName( ‘noroute’ );
    $request->setActionName( ‘index’ );
    $request->setDispatched( false );
    }

    And for those who thinking how to make HTTP1.1/Status code, I guess the best solution is to include in NorouteController init() method with setting HTTP Status like this:

    class CorouteController extends Zend_Controller_Action
    {
    public function init()
    {
    $this->getResponse()->setHttpResponseCode(404);
    }

    /* … skipped … */
    }

    Aleksey Zapparov A.K.A. iXTi  |  16th April 2007 at 9:07 am

  • And one more enhancement :)) As you all know abstract Zend_Controller_Action has __call() method which is called if specified action wasn’t found in controller. By default it throws an Exception, but you may redeclare this method in each separate controller. If you don’t understand why you need it, you may read brief explanation on my homepage. So for those who know why you need it, here’s modified version:

    require_once ‘Zend/Controller/Exception.php’;
    require_once ‘Zend/Controller/Plugin/Abstract.php’;

    class NoRoute extends Zend_Controller_Plugin_Abstract
    {
    public function preDispatch(Zend_Controller_Request_Abstract $request)
    {
    $dispatcher = Zend_Controller_Front::getInstance()->getDispatcher();

    try {
    /* @var $dispatcher Zend_Controller_Dispatcher_Standard */
    $controller_class = $dispatcher->getControllerClass($request);

    // If specified controller is not exists, $dispatcher->loadClass
    // will throw a Zend_Controller_Dispatcher_Exception with message
    $controller_name = $dispatcher->loadClass($controller_class);
    $action_name = $dispatcher->getActionMethod($request);
    $controller_obj = new ReflectionClass($controller_name);

    // If specified action exists in this controller, then we can
    // interrupt plugin and return to normal dispatching mode
    if ($controller_obj->hasMethod($action_name)) {
    return;
    }

    // If specified controller has overrided
    $call_overrided_in = $controller_obj->getMethod(’__call’)
    ->getDeclaringClass()
    ->getName();
    if ($controller_name == $call_overrided_in) {
    return;
    }

    throw new Zend_Controller_Exception();
    } catch (Zend_Exception $e) {
    // Setting new controller and acton to noroute handler
    $request->setControllerName( ‘noroute’ );
    $request->setActionName( ‘index’ );
    // This is very-very internal parametr, that will allow unserialize
    // and then use data of exception in noroute handler
    $request->setParam(’__noroute_caught_exception’, serialize($e));
    $request->setDispatched( false );
    }

    return;
    }

    As you can notice, this plugin gives you an availability to get the Exception that was caught while plugin worked :)) So in your NorouteController you can call:

    $e = unserialize($this->getRequest()
    ->getParam(’__noroute_caught_exception’));

    Aleksey Zapparov A.K.A. iXTi  |  18th April 2007 at 6:40 am

  • For those who do not want any plugins and/or want only to redirect all request to default controller/action when something goes wrong, it can be simply done with one simple line:

    $front->setParam(’useDefaultControllerAlways’, true);

    for more info you can refer to manual:

    http://framework.zend.com/manual/en/zend.controller.exceptions.html#zend.controller.exceptions.internal

    Aleksey Zapparov A.K.A. iXTi  |  18th April 2007 at 12:58 pm

  • Tnx for 404 code. I will use it on my web page. :)

    vlakikosta  |  30th June 2007 at 12:58 am

Comments closed.