Welcome to Abdul Malik Ikhsan's Blog

Zend Framework 2 : Programmatically handle 404 page

Posted in Tutorial PHP, Zend Framework 2 by samsonasik on January 1, 2014

zf2-zendframework2Sometime, in real life project, we need to handle 404 page more than only show the 404 page, but logging, redirecting, forwarding, change template/content, etc. We can handle it by programmatically at Module.php. In Zend Framework 2, there is 4 types of 404 ( page not found ), they are : ERROR_CONTROLLER_CANNOT_DISPATCH, ERROR_CONTROLLER_NOT_FOUND, ERROR_CONTROLLER_INVALID, and ERROR_ROUTER_NO_MATCH.  Let’s investigate one by one.

1. ERROR_CONTROLLER_CANNOT_DISPATCH
It means the controller is matched, but the action that passed to the url can’t be dispatched. We can handle it via onBootstrap like the following :

namespace YourModule;

use Zend\Mvc\MvcEvent;

class Module
{
    public function onBootstrap(MvcEvent $e)
    {
        $eventManager  = $e->getApplication()->getEventManager();
        $sharedManager = $eventManager->getSharedManager();
        //controller can't dispatch request action that passed to the url
        $sharedManager->attach('Zend\Mvc\Controller\AbstractActionController',
                  'dispatch', 
                  array($this, 'handleControllerCannotDispatchRequest' ), 101);
    }

    public function handleControllerCannotDispatchRequest(MvcEvent $e)
    {
        $action = $e->getRouteMatch()->getParam('action');
        $controller = get_class($e->getTarget());
        
        // error-controller-cannot-dispatch
        if (! method_exists($e->getTarget(), $action.'Action')) {
            $logText = 'The requested controller '.
                        $controller.' was unable to dispatch the request : '.$action.'Action';
            //you can do logging, redirect, etc here..
             echo $logText;
        }
    }   

    public function getConfig() { //common code here }
    public function getAutoloaderConfig() { //common code here }
}

2. ERROR_CONTROLLER_NOT_FOUND
It means controller class not found with requested [/:controller] route that defined already at module.config.php
3. ERROR_CONTROLLER_INVALID
It means the controller is not dispatchable, it usually because the controller is not extends Zend\Mvc\Controller\AbstractActionController
4. ERROR_ROUTER_NO_MATCH
It means The requested URL could not be matched by routing, for example, there is no route with prefix /foo that passed at the url.
For point 2, 3, and 4, we can handle by :

namespace YourModule;

use Zend\Mvc\MvcEvent;
use Zend\Mvc\Application;

class Module
{
    public function onBootstrap(MvcEvent $e)
    {
        $eventManager = $e->getApplication()->getEventManager();
        $eventManager->attach('dispatch.error',
             array($this,
             'handleControllerNotFoundAndControllerInvalidAndRouteNotFound'), 100);
    }
     
    public function handleControllerNotFoundAndControllerInvalidAndRouteNotFound(MvcEvent $e)
    {
        $error  = $e->getError();
        if ($error == Application::ERROR_CONTROLLER_NOT_FOUND) {
            //there is no controller named $e->getRouteMatch()->getParam('controller')
            $logText =  'The requested controller '
                        .$e->getRouteMatch()->getParam('controller'). '  could not be mapped to an existing controller class.';
            
            //you can do logging, redirect, etc here..
            echo $logText;
        }
        
        if ($error == Application::ERROR_CONTROLLER_INVALID) {
            //the controller doesn't extends AbstractActionController
            $logText =  'The requested controller '
                        .$e->getRouteMatch()->getParam('controller'). ' is not dispatchable';
            
            //you can do logging, redirect, etc here..
            echo $logText;
        }
        
        if ($error == Application::ERROR_ROUTER_NO_MATCH) {
            // the url doesn't match route, for example, there is no /foo literal of route
            $logText =  'The requested URL could not be matched by routing.';
            //you can do logging, redirect, etc here...
            echo $logText;
        }
    } 
    public function getConfig() { //common code here }
    public function getAutoloaderConfig() { //common code here }
}

Want to handle All ? Let’s do this :

namespace YourModule;
use Zend\Mvc\MvcEvent;
use Zend\Mvc\Application;

class Module
{
    public function onBootstrap(MvcEvent $e)
    {
        $eventManager = $e->getApplication()->getEventManager();
        $sharedManager = $eventManager->getSharedManager();
        //controller can't dispatch request action that passed to the url
        $sharedManager->attach('Zend\Mvc\Controller\AbstractActionController',
               'dispatch',
               array($this, 'handleControllerCannotDispatchRequest' ), 101);
        //controller not found, invalid, or route is not matched anymore
        $eventManager->attach('dispatch.error', 
               array($this,
              'handleControllerNotFoundAndControllerInvalidAndRouteNotFound' ), 100);
    }
    
    public function handleControllerCannotDispatchRequest(MvcEvent $e)
    {
        $action = $e->getRouteMatch()->getParam('action');
        $controller = get_class($e->getTarget());
        
        // error-controller-cannot-dispatch
        if (! method_exists($e->getTarget(), $action.'Action')) {
            $logText = 'The requested controller '.
                        $controller.' was unable to dispatch the request : '.$action.'Action';
            //you can do logging, redirect, etc here..
             echo $logText;
        }
    }
    
    public function handleControllerNotFoundAndControllerInvalidAndRouteNotFound(MvcEvent $e)
    {
        $error  = $e->getError();
        if ($error == Application::ERROR_CONTROLLER_NOT_FOUND) {
            //there is no controller named $e->getRouteMatch()->getParam('controller')
            $logText =  'The requested controller '
                        .$e->getRouteMatch()->getParam('controller'). '  could not be mapped to an existing controller class.';
            
            //you can do logging, redirect, etc here..
            echo $logText;
        }
        
        if ($error == Application::ERROR_CONTROLLER_INVALID) {
            //the controller doesn't extends AbstractActionController
            $logText =  'The requested controller '
                        .$e->getRouteMatch()->getParam('controller'). ' is not dispatchable';
            
            //you can do logging, redirect, etc here..
            echo $logText;
        }
        
        if ($error == Application::ERROR_ROUTER_NO_MATCH) {
            // the url doesn't match route, for example, there is no /foo literal of route
            $logText =  'The requested URL could not be matched by routing.';
            //you can do logging, redirect, etc here...
            echo $logText;
        }
    } 
    
    public function getConfig(){ // common code }
    public function getAutoloaderConfig(){ //common code }
}

Done 😉

27 Responses

Subscribe to comments with RSS.

  1. pooria said, on July 31, 2014 at 3:04 am

    hi ! tnx for your tutorials ! they are great 🙂
    i have a question :
    how can i pass e variable to layout in this situation ( 404 page ) ?

    • samsonasik said, on July 31, 2014 at 2:07 pm

      you can set by :

      $e->getViewModel()->setVariable('varname', 'varvalue');
      

      and call in the layout/view :

      echo $this->layout()->varname;
      
      • Serge said, on July 24, 2016 at 1:07 pm

        Hallo Samsonasik,
        I get it how to pass variable to the template. But how do I get the original variable that come with the standard template. Actually all I need is change the layout, but I need all the variables. The only thing I can have is the error.

        Thanks!

      • samsonasik said, on July 24, 2016 at 6:19 pm

        $viewModel->getVariables()

  2. Pankaj said, on September 4, 2014 at 11:50 am

    awesome tutorial dear . I am beginner learn lot from your precious tutorial

  3. Manoel Filho said, on March 19, 2015 at 1:56 am

    Very nice! How to make a toRoute redirect if a ERROR_ROUTER_NO_MATCH?

    • samsonasik said, on March 19, 2015 at 5:38 am

      you can do something like this :

      $response = $e->getResponse();
      $response->setStatusCode(302);
                  $response->getHeaders()->addHeaderLine('Location', '/');
      $response->send();
      
      $e->stopPropagation();
      
      • Manoel Filho said, on March 22, 2015 at 10:20 pm

        very grateful! Your blog is a true source of good information. Thank!

  4. sagruob said, on May 15, 2015 at 4:57 am

    HI,
    Thanks for the great tutorial!

    I have one question in relation to my current work. Why error-controller-cannot-dispatch is not caugth by MvcEvent::EVENT_DISPATCH_ERROR handler ?

    • samsonasik said, on May 15, 2015 at 5:05 am

      because controller is mathed, but action is not

      • sagruob said, on May 15, 2015 at 1:30 pm

        So as far as I see when action is not matched it is not dispatch.error and there is no other error that we can attach handler to catch it directly ?

  5. Srini Vasu said, on June 16, 2015 at 2:15 pm

    I have implemented the above code & its not working, could you plz help in that.. In the Module.php file I have pasted the code on error block :
    $sharedManager->attach(‘Zend\Mvc\Controller\AbstractActionController’,’dispatch’,
    array($this, ‘handleControllerCannotDispatchRequest’ ), 404); & added the function :
    public function handleControllerCannotDispatchRequest(MvcEvent $e)
    {
    $action = $e->getRouteMatch()->getParam(‘action’);
    $controller = get_class($e->getTarget());
    $error = $e->getError();
    if ($error == Application::ERROR_ROUTER_NO_MATCH) {
    // the url doesn’t match route, for example, there is no /foo literal of route
    $logText = ‘The requested URL could not be matched by routing.’;
    //you can do logging, redirect, etc here…
    echo $logText; die;
    }
    }

    but when i give wrong URL it’s not working, wrong URL mean subdomain not exists, example valid url is : text.mydomain.com, invalid one is test1.mydomain.com..
    when I type test1.mydomain.com its not redirecting to anywhere just loading the home page content, which I to be redirected to 404 page.. Please help

  6. WitteStier said, on September 29, 2015 at 4:17 am

    How can i test the Application::ERROR_CONTROLLER_CANNOT_DISPATCH error?
    I mean i want to end up with a viewModel that has the Application::ERROR_CONTROLLER_CANNOT_DISPATCH reason. Do you know what to do, to get this error?

    • samsonasik said, on September 29, 2015 at 7:07 am

      I said it happen when controller found, but action not found, so, you may have route: `/[:controller]/[:action]` and when you access `/controllername/actionname` but you call un-registered action. if you use ZendSkeletonApplication, you already got 404 page 😉

      • WitteStier said, on September 30, 2015 at 2:31 am

        The AbstractActionController::notFoundAction is called when there is no action found. and this renders the 404 page. but i want to respond with JSON. so i want to find a viewModel with the Application::ERROR_CONTROLLER_CANNOT_DISPATCH reason.

      • samsonasik said, on September 30, 2015 at 5:01 am

        create response object. you may need something like this:

                    $response = $e->getResponse();
        
                    $headers = $response->getHeaders();
                    $headers->addHeaderLine('Content-Type', 'application/json');
        
                    $response->setStatusCode(404);
                    $response->setContent('{"page":"notfound"}');
        
                    $response->send();
                    exit(0);
        
  7. WitteStier said, on September 29, 2015 at 5:14 am

    I you want to support both ActionController and RestfulController than you should use `Zend\Mvc\Controller\AbstractController` instead of `Zend\Mvc\Controller\AbstractActionController`.

  8. Ilya said, on October 21, 2015 at 12:36 am

    Hi!
    I have several modules which have own error templates like error/404.phtml and error/index.html (for other errors except 404).
    I need to change path to template for each module. For EVENT_DISPATCH_ERROR & EVENT_RENDER_ERROR my code looks like

    $stack = $event->getApplication()->getServiceManager()->get('ViewTemplateMapResolver');
    if ($response->getStatusCode() == 404) {
    $stack->add('error/404', $path.'404.phtml');
    } else{
    $stack->add('error/index', $path.'index.phtml');
    }

    But in ERROR_CONTROLLER_CANNOT_DISPATCH i cant do like this and error template displayed from one module every time.
    Can you help me?

  9. aniket said, on January 3, 2017 at 4:43 pm

    What if i want to set a controller if ERROR_ROUTER_NO_MATCH .. how would i achieve this

    • samsonasik said, on January 3, 2017 at 11:46 pm

      you can do something like this:

      $router = $e->getRouter();
      $url    = $router->assemble([], ['name' => 'home']);
      
      $response = $e->getResponse();
      $response->setStatusCode(302);
      $response->getHeaders()->addHeaderLine('Location', $url);
      
      $e->stopPropagation();
      
      • aniket said, on January 4, 2017 at 6:23 pm

        Thank you for the reply.. But what i want is without changing the url set the controller and layout to the current url

      • samsonasik said, on January 4, 2017 at 10:58 pm

        that’s impossible without very hacky (pull global service manager , call controller plugin manager, call forward plugin, set controller, call dispatch, etc etc) , as the route already not match, there is no way to pull it and setParam(‘controller’) or setParam(‘action’), my suggestion is to create default render view for that, or just define 404 page properly for your site.

  10. aniket said, on January 4, 2017 at 11:44 pm

    dynamic urls are getting created.. right now what i have hacked is i have written the module.config.php file everytime a new url is added.. But i find that not the correct way to do it.. and i don’t want my urls to be domain.com/page/page1 i want them domain.com/page1 .. Thanks again

    • samsonasik said, on January 5, 2017 at 5:26 pm

      you should not doing that, forwarding also should not be applied for 404 page. Just define a custom 404 page if you want.

      • aniket said, on January 7, 2017 at 2:24 pm

        This is not about 404.. its a cms that i have built on zend.. and looking for a solution where i can create dynamic urls which point to the same controller and action… My urls can be domain.com/page , domain.com/category/page domain.com/category/subcategory/page and i need all to point to the same action of the same controller.. so i truncate the url.. and check the seo tags , page layout , page data etc and if not found manually trigger 404.. Thanks again for the reply.. 🙂


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: