Search code examples
zend-framework2

Manually trigger 404 error


I've been struggling to manually trigger dispatch.error from within another controller listener.

The goal is to raise a 404 error where a route parameter is invalid and have it 'caught' by the standard Zend\Mvc\View\Http\RouteNotFoundStrategy so the 404 page is rendered.

public function FooController extends AbstractActionController
{
    protected $foo;

    public function getEventManager()
    {  
        $em = parent::getEventManager();

        // Attach with higher priority than dispatched indexAction
        $em->attach('dispatch', array($this, 'getFooFromRouteParam'), 10);
    }

    public function indexAction()
    {
        $foo = $this->getFoo(); // Error 500 (not 400)
    }

    public function getFooFromRouteParam(EventInterface $event)
    {
        $id = $this->params('id', false);
    
        if (! empty($id)) {

            $foo = $this->aDatabaseService->loadFromTheDatabase($id);

            if ($foo instanceof Foo) {

                $this->setFoo($foo);
                return;
            }
        }

        $response = $event->getResponse();

        $response->setStatusCode(404);
        $event->setError(\Zend\Mvc\Application::ERROR_ROUTER_NO_MATCH);

        $event->getApplication()
              ->getEventManager()
              ->trigger('dispatch.error', $event);

        //return $response;
    }

    public function getObjectFoo()
    {
        if (null == $this->foo) {
            throw new \RuntimeException('foo not set');
        }
        return $this->foo;
    }

    public fucntion setObjectFoo(Foo $object)
    {
        $this->foo = $object;
    }
}

The events are triggered correctly, debugging them gives me :

CALLED Zend\Mvc\View\Http\RouteNotFoundStrategy::detectNotFoundError (The error is : error-controller-not-found)

CALLED Zend\Mvc\View\Http\RouteNotFoundStrategy::prepareNotFoundViewModel CALLED Zend\Mvc\View\Http\RouteNotFoundStrategy::injectNotFoundReason

However, returning the response after gives me a 404 with no body.

return $response; // ends execution with 404 but no body, just white screen.

If I do not return the response the dispatch continues and I get a 500 error

RuntimeException 'foo not set'

How can I manually trigger a 404 error and correctly render the 404 template?


Solution

  • Finally managed to figure out the issue.

    The Zend\Mvc\Controller\AbstractController method is expecting a Response to be returned before it will stop execution of the controller's events.

    This is the relevant section of the dispatch method.

    $result = $this->getEventManager()->trigger(MvcEvent::EVENT_DISPATCH, $e, 
        function ($test) {
            return ($test instanceof Response);
        }
    );
    
    if ($result->stopped()) {
        return $result->last();
    }
    
    return $e->getResult();
    

    What I needed to do was return my result (which was the error view model) and manually stop event propagation (to ensure that the remaining controller actions were not executed)

    // Set the event's 'error' vars
    $event->setError(Application::ERROR_EXCEPTION);
    $event->setParam('exception', new \Exception('A custom error message here'));
    
    // Trigger the dispatch.error event
    $event->getApplication()
          ->getEventManager()
          ->trigger(MvcEvent::EVENT_DISPATCH_ERROR, $event);
    
    // The missing piece! Required to ensure the remaining dispatch 
    // listeners are not triggered
    $event->stopPropagation(true);
    
    // Contains the view model that was set via the ExceptionStrategy
    return $event->getResult();