4
votes

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?

3

3 Answers

1
votes

I don't think you can, nor should. When you are in the controller, the router has already finished it's job, so going back to a Zend\Mvc\View\Http\RouteNotFoundStrategy will not work.

What you should do is make the parameter required in the route, so the router will do the check for you.

If you have considered this, but still want to use a 404 response for a request processing error, have a look at how the actual DispatchListener handles this it:

$event->setError($application::ERROR_EXCEPTION)
      ->setController($controllerName)
      ->setParam('exception', $exception);

$events  = $application->getEventManager();
$results = $events->trigger(MvcEvent::EVENT_DISPATCH_ERROR, $event);
$return  = $results->last();
if (! $return) {
    $return = $event->getResult();
}
1
votes

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();
1
votes

For Zend Framework 3

Triger 404 manuely in Controller and get it in error listener:

public function articleAction()
{
    // For eg. if we can not found article fetched by url_slug
    if(!$article){
        $this->getResponse()->setStatusCode(404);
        $this->getEvent()->setName(MvcEvent::EVENT_DISPATCH_ERROR);
        $this->getEvent()->getApplication()->getEventManager()->triggerEvent($this->getEvent());

        return false;
    }

    return ['article' => $article];
}