It never fails. Every time I start a new ZF2 project I end up needing to disable the layout for certain responses. Usually it’s to feed some HTML content to a Dojo Dialog or something else involving XHR. Being a firm believer in RTFM first I look through the ZF2 Reference Guide which provides no help. Then I dive into the ZF2 source code which also provides no answers. After a bit of cursing my frustration leads me to Google which yields Zend Framework 2 : Disable Layout in specific Module. OK that was more difficult than is should have been, but at least it works… except… the obsessive compulsive part of me just can’t let it go without knowing “Why does it work?”, “Why isn’t there a simpler solution?” So to hopefully save myself, and other obsessive compulsive programmers, from future frustration here is the how and why.
Looking at the source code the MVC seems to create the layout out of some kind of magical programming aether. The magic starts way back during the Application’s bootstrap event when the ViewManager object is created. As part of it’s bootstrap listener the ViewManager initializes the MvcEvent’s ViewModel and sets its template to the configured layout. So how does it get the content into the layout? This happens during the dispatch event in the InjectViewModelListener which sets the ViewModel result returned from the dispatched controller as the child of the ViewModel created by the ViewManager. The trick of disabling the layout works because of 4 very inconspicuous lines in the InjectViewModelListener which replace the default ViewModel (which renders the layout) with the ViewModel returned from the controller action, if it’s terminal flag is true, instead of setting it as a child.
OK lets back up and try something simple to illustrate. We can disable the layout in a controller action by simply returning a ViewModel with the terminal flag set to true.
class MyController extends AbstractActionController
{
public function myAction()
{
$viewModel = new \Zend\View\Model\ViewModel();
$viewModel->setTerminal(true);
return $viewModel;
}
}
Pretty strait forward. When the InjectViewModelListener checks the terminal flag of our ViewModel it will set it as the ViewModel for MvcEvent, which replaces the default ViewModel and disable the layout. So how about disabling the layout for the entire controller?
class MyController extends AbstractActionController
{
public function myAction()
{
$myViewModel = new \Zend\View\Model\ViewModel();
return $myViewModel;
}
protected function attachDefaultListeners()
{
parent::attachDefaultListeners();
$eventManager = $this->getEventManager();
$eventManager->attach(
\Zend\Mvc\MvcEvent::EVENT_DISPATCH,
function(\Zend\Mvc\MvcEvent $event) {
$myViewModel = $event->getResult();
if ($myViewModel instanceof \Zend\View\Model\ViewModel) {
$myViewModel->setTerminal(true);
}
},
-99);
}
}
Still pretty strait forward. This time we attach a callback to the dispatch event which sets the terminal flag to true for any ViewModel returned from any action in controller. Notice the priority of -99 which places the listener just before the InjectViewModelListener but after any other listeners which modify the results of the controller’s action. This is especially important if any of the controllers actions return something other than a ViewModel such as an array or null.
So to disable the layout for all XMLHttpRequests for the entire application we should be able to use the same callback with a condition that tests for a XMLHttpRequest in our Module class… right? Well not quite. The problem is that the InjectViewModelListener doesn’t listen for the Application’s dispatch event; it is attached to the Controller’s dispatch event. The fact that the dispatch event triggers another dispatch event makes me want to throw my hands up, call it a day and head to the pub, but I’ll press on a little more. To make it work outside of the controller we need to use the shared event manager and attach it to the dispatch event for any controller.
namespace MyModule;
class Module
{
public function onBootstrap(\Zend\Mvc\MvcEvent $event)
{
$sharedEventManager = $event->getApplication()
->getEventManager()->getSharedManager();
$sharedEventManager->attach(
'Zend\Mvc\Controller\AbstractController',
\Zend\Mvc\MvcEvent::EVENT_DISPATCH,
function(\Zend\Mvc\MvcEvent $event) {
$request = $event->getRequest();
if ($request->isXMLHttpRequest()) {
$dispatchResult = $event->getResult();
if ($dispatchResult instanceof ViewModel) {
$dispatchResult->setTerminal(true);
}
}
},
-99);
}
}
This does the same thing, but uses the shared event manager to attach the listener to all controllers. As confusing as having the dispatch event trigger another dispatch event is it is also what allows us to target specific modules or even controllers. In the code above ‘Zend\Mvc\Controller\AbstractController’ could be replaced with a specific module or controller.
To make things a little more portable you could also add the following class to your library.
<?php
namespace MyLib\Mvc\Listener;
use Zend\EventManager\AbstractListenerAggregate;
use Zend\EventManager\EventManagerInterface;
use Zend\Mvc\MvcEvent;
use Zend\View\Model\ViewModel;
class XMLRequestListener extends AbstractListenerAggregate
{
public function attach(EventManagerInterface $events)
{
$sharedEvents = $events->getSharedManager();
$this->listeners[] = $sharedEvents->attach(
'Zend\Mvc\Controller\AbstractController',
MvcEvent::EVENT_DISPATCH,
array($this, 'handleXMLRequest'),
-99);
}
public function handleXMLRequest(MvcEvent $event)
{
$request = $event->getRequest();
if ($request->isXMLHttpRequest()) {
$dispatchResult = $event->getResult();
if ($dispatchResult instanceof ViewModel) {
$dispatchResult->setTerminal(true);
}
}
}
}
Then all you would need to do is add the following to your module
public function onBootstrap(MvcEvent $event)
{
$eventManager = $event->getApplication()->getEventManager();
$eventManager->attachAggregate(
new MyLib\Mvc\Listener\XMLRequestListener());
}