Intercepting not found errors in multi-module applications

Hi all! I have multi-module app like in 'mvc\multiple-shared-layouts' and have two modules, frontend and backend. I'm trying to intercept not found events by setting up dispatcher service inside every module's registerServices() method, but it seems application doesn't even run this method. The idea is to have different not found pages for frontend and backend. How can I achieve it? Here is example of my Frontend module, backend is the same except 'module' definitions.

public function registerServices($di)
    {
        $di->set('dispatcher', function() {
                $eventsManager = new \Phalcon\Events\Manager();
                $eventsManager->attach('dispatch', function($event, $dispatcher, $exception){
                        //The controller exists but the action not
                        if($event->getType() == 'beforeNotFoundAction') {
                            $dispatcher->forward(array(
                                    'module'=>'frontend',
                                    'controller'=>'index',
                                    'action'=>'show404'
                                ));
                            return false;
                        }
                        if($event->getType() == 'beforeException') {
                            switch ($exception->getCode()) {
                                case \Phalcon\Dispatcher::EXCEPTION_HANDLER_NOT_FOUND:
                                case \Phalcon\Dispatcher::EXCEPTION_ACTION_NOT_FOUND:
                                    $dispatcher->forward(array(
                                            'module'=>'frontend',
                                            'controller'=>'index',
                                            'action'=>'show404'
                                        ));
                                    return false;
                            }
                        }
                    });
                $dispatcher = new \Phalcon\Mvc\Dispatcher();
                $dispatcher->setEventsManager($eventsManager);
                $dispatcher->setDefaultNamespace('Modules\Frontend\Controllers');

                return $dispatcher;

            });


        $di['view'] = function() {
            $view = new \Phalcon\Mvc\View();

            $view->setViewsDir(__DIR__ . '/views/');
            $view->setLayoutsDir('../../common/layouts/');
            $view->setTemplateAfter('main');

            return $view;
        };
    }

Thanks.



83.4k
edited Feb '14

There are 3 not-found events you need to be aware of:

  1. The current URI is not matched by any route set in the router
  2. The current controller cannot be loaded by the dispatcher
  3. The current action is not part of the active controller

Using the code you posted before you can handle the cases 2 and 3. Additionally you need to set some not-found routes (http://docs.phalconphp.com/en/latest/reference/routing.html#not-found-paths) up to tell the application what module needs to be loaded in case 1.

Btw, the code to handle 2 and 3 is:

public function registerServices($di)
{
    $di->['dispatcher'] = function() {

        $eventsManager = new \Phalcon\Events\Manager();

        $eventsManager->attach('dispatch:beforeException', function($event, $dispatcher, $exception) {

            switch ($exception->getCode()) {
                case \Phalcon\Dispatcher::EXCEPTION_HANDLER_NOT_FOUND:
                case \Phalcon\Dispatcher::EXCEPTION_ACTION_NOT_FOUND:
                    $dispatcher->forward(array(
                        'module'=>'frontend',
                        'controller'=>'index',
                        'action'=>'show404'
                    ));
                    return false;
            }

        });

        $dispatcher = new \Phalcon\Mvc\Dispatcher();
        $dispatcher->setEventsManager($eventsManager);
        $dispatcher->setDefaultNamespace('Modules\Frontend\Controllers');

        return $dispatcher;

    };

    $di['view'] = function() {

        $view = new \Phalcon\Mvc\View();

        $view->setViewsDir(__DIR__ . '/views/');
        $view->setLayoutsDir('../../common/layouts/');
        $view->setTemplateAfter('main');

        return $view;
    };
}
edited Feb '14

Thanks for the answer. In this case, I always should set router inside of bootstrap file, where I need to choose module that should be able to handle all future errors\exceptions (index.php)

$di->set('router', function() use ($routes){
            $router = new Phalcon\Mvc\Router(false);
            array_walk($routes->toArray(), function($value, $key) use (&$router){
                    $router->add($key, $value);
                });
            $router->removeExtraSlashes(true);
            $router->notFound(array(
               'module' => 'frontend',
               'controller' => 'index',
                'action' => 'show404'
            ));

            return $router;
        });

Okay, that's clear. But how can I show different 404 pages for different modules? Should I create new module, 'common', where to route all notFound actions and check there what module should run in this case, right?

edited May '14

You don't need new module for that. if You set dispacher on each module.php with custom 404 pages and it's should work. I have similar setup and it's work with different error 404 pages for each module.

edited Nov '14

I have the same problem, I have a project using 2 modules, "www" and "api". I can't find how to have a 404 error page in HTML for my "www" module and a JSON format 404 error for the API module.

index.php

    $di->set('router', function () {
        $router = new Router(false);
        $router->setDefaultModule('www');
        $router->add('/', ['module' => 'www', 'controller' => 'index', 'action' => 'index']);
        $router->add('/api', ['module' => 'api', 'controller' => 'index', 'action' => 'index']);
        $router->notFound(['module' => 'www', 'controller' => 'error', 'action' => 'show404']);
        return $router;
    });

Www - Module.php

    public function registerServices($di)
    {
        $di->set('dispatcher', function() use ($di) {
            $eventManager = $di->getShared('eventsManager');
            $eventManager->attach('dispatch:beforeException', function($event, Dispatcher $dispatcher, $exception) {
                switch ($exception->getCode()) {
                    case Dispatcher::EXCEPTION_HANDLER_NOT_FOUND:
                    case Dispatcher::EXCEPTION_ACTION_NOT_FOUND:
                        $dispatcher->forward(['controller' => 'error', 'action' => 'show404']);
                        return false;
                }
            });

            $dispatcher = new Dispatcher();
            $dispatcher->setDefaultNamespace('MonPartenaire\\Www\\Controllers');
            return $dispatcher;
        });
    }

Api - Module.php is the same

When $router->notFound() is defined, it intercepts everything, including /api/* calls and route them to www module error controller. If I remove $router->notFound(), any call to /api/* is routed to default controller (home page).

Any idea ?

Thank you and keep going on with this great framework.



3.6k

My is not working. I can't make not found error page.

router.php

$router = new Phalcon\Mvc\Router();

$router->removeExtraSlashes(true);

// required for modules.
$router->setDefaultModule('core');

$router->add(
    '/{lang:[a-z]{2}}/index/:action/:params',
    array(
        'module' => 'core',
        'controller' => 'index',
        'action' => 2,
        'params' => 3,
    )
);
$router->add(
    '/{lang:[a-z]{2}}', 
    array(
        'module' => 'core',
        'controller' => 'index',
        'action' => 'index',
    )
);

services.php

// set dispatcher
$di->set('dispatcher', function() use ($di) {
    $evManager = $di->getShared('eventsManager');

    $evManager->attach('dispatch:beforeException', function($event, $dispatcher, $exception) {
        switch ($exception->getCode()) {
            case PhDispatcher::EXCEPTION_HANDLER_NOT_FOUND:
            case PhDispatcher::EXCEPTION_ACTION_NOT_FOUND:
                $dispatcher->forward(
                    array(
                        'module' => 'core',
                        'controller' => 'error',
                        'action' => 'e404',
                    )
                );
                return false;
        }
    });

    $dispatcher = new PhDispatcher();
    $dispatcher->setEventsManager($evManager);
    $dispatcher->setDefaultNamespace('Core\\Controllers');
    return $dispatcher;
});

index.php

$application = new \Phalcon\Mvc\Application($di);
    $application->registerModules(
        array(
            'core' => array(
                'className' => 'Core\\Module',
                'path' => APPFULLPATH . '/Module.php',
            ),
            'contact' => array(
                'className' => 'Modules\\Contact\\Module',
                'path' => ROOTFULLPATH . '/modules/contact/Module.php',
            )
        )
    );

Request the page to /index/action-not-exists action-not-exists is not exists in the index controller. but it is not forward to error page.

"Action 'action-not-exists' was not found on handler 'index'"

This is what i got instead of my 404 page.

Can you help me please?

I had a similar problem. My application was in modules and was having two equal services, one in the global file services and other inside the module. A service was overwriting the other.

edited Mar '17

Please fix that!!! I have a similar problem.

Any workaround for this? I want to intercept the NotFound for my API too. But the notFound is redirecting me to the Web not Found route.

I have encountered this because I separate my UI from API and I have different versions for both. However for the api, end users supply an application vendor accept header which defines an api version. Each version is a module. if a that module is not found it throws this error which is caught in the bootstrap try...catch. I can't be sending HTML to api requests and vice versa.

Module 'api_v3' isn't registered in the application container

My api version/module selection logic is in the router service, and in a nest of if conditions i set the default module based on this information. This gets executed before the routes are defined.

I have worked around this by setting my api module to a the latest version of the api if an invalid version is supplied.

    $frontend_version = $this->getConfig()->application->frontend_version;
    $api_version = $this->getConfig()->application->api_version;

    // do some logic on base api route
    if (preg_match('/^\/api/', $uri)) {
        // if the version string is supplied, process it. 
        if (preg_match($vendor_regex, $accept, $vendor_version)) {
            // check if the version is less than or equal to the latest default api version
            if ($vendor_version <= $api_version && $vender_version > 0) {
                $api_version = $vendor_version[1];
            }
        }

        // no version matched, or was not supplied
        $router->setDefaultModule('api_v' .$api_version);
        $router->setDefaultNamespace('App\Api\V' . $api_version . '\Controllers');
    } else {
        // when not an api request, automatically set frontend to version from config
        $router->setDefaultModule('frontend_v' .$frontend_version);
        $router->setDefaultNamespace('App\Frontend\V' . $frontend_version . '\Controllers');
    }

I push a draft on here https://github.com/phalcon/cphalcon/pull/12746 but I do not have the time to finish it. What it does is "if module is not found then fire an application:beforeException event". Just in case that someone want to take up the torch.