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
CALLEDZend\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?
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();