ZF-5282: Calling _forward() from init() make the dispatcher dispatch the wrong controller

Description

When calling _forward(), the method just injects the arguments into the request, aka module, controller, action, and set the dispatch token to false.

The problem is when we call _forward() from an init() method of an action controller. In that case, the dispatcher tries to load the action, but keeps the actual controller as to be dispatched instead of taking the one which could have been specified in _forward().

That special use case should be patched in the dispatch() method of the ZC_Action class.



class MyController extends Zend_Controller_Action
{
    public function init()
    {
        $this->_forward('someaction', 'somecontroller');
    }
    // ...
}

That code will ask for the dispatching of MyController / someaction ; instead of someController / someaction

Comments

This needs to be addressed. We just ran into it and spent a lot of time trying to figure out why our _forward() wasn't being respected correctly.

I have come across this same issue and believe I have found the problem.

Calling _forward() sets the dispatched flag on the request to false. This is done at the time of controller instantiation in the dispatch method of he dispatcher. But before after that and before the controller action is dispatched the dispatcher sets the dispatched flag back to true. This is why foward works in init() in the same controller but not across controllers or modules. Here is the offending section of Zend/Controller/Dispatcher/Standard.php with my comments added.


    //controller instantiated here..._forward() will be called in init now.
    $controller = new $className($request, $this->getResponse(), $this->getParams());

        if (!$controller instanceof Zend_Controller_Action) {
            require_once 'Zend/Controller/Dispatcher/Exception.php';
            throw new Zend_Controller_Dispatcher_Exception("Controller '$className' is not an instance of Zend_Controller_Action");
        }

        /**
         * Retrieve the action name
         */
        $action = $this->getActionMethod($request);

        /**
         * Dispatch the method call
         */
        //dispatch flag reset to true here after having been set to false by _forward() call
        $request->setDispatched(true);

        // by default, buffer output
        $disableOb = $this->getParam('disableOutputBuffering');
        $obLevel   = ob_get_level();
        if (empty($disableOb)) {
            ob_start();
        }

        try {
            $controller->dispatch($action);
        } catch (Exception $e) { ...

I commented out the setDispatched() call for a quick trial and it solved the problem but of course this is not the solution. Could the call to setDispatched() be moved so that it happens before controller instantiation? I don't see any problems that it could cause but I'm not expert on the dispatch loop. I'm going to move it for the time being in my projects and report back if anything fails.

In addition it seems to me that a call to _forward in init() should stop the processing of the current action and move to the new action. Again this works in the same controller because the action is determined after the request has been modified by _forward(). But a forward to a different controller fails because we are already in the controller called in the original request. The rest of the current dispatcher dispatch() call should be ignored so that the front controller dispatch loop can run again and pick up the new controller and module. Were forwards not meant to happen from init()?

init() was originally intended for extending the constructor, since the constructor has a verbose signature. This would include instantiating appropriate models, setting object state from front controller/bootstrap parameters and/or a registry, etc. The idea is that you would initialize resources that the controller would consume here -- but not do any actual decisioning or application logic.

preDispatch() and postDispatch() are to be used for altering or enhancing behavior of the dispatched action. The idea was that if you determined the action could not be executed in preDispatch(), then you could _forward() to another action.

I see no real benefit to adding a dispatched check between init() and preDispatch() as this basically duplicates logic, and violates the original intentions of the two methods.

I think this is primarily an educational issue, and I'll add a note to the manual detailing correct usage of _forward() in these circumstances.

Changed issue type to "docs improvement"

Section added to action controller chapter of manual detailing usage of init() vs. preDispatch(), specifically mentioning difference in how _forward() will act.