Issues

ZF-2993: Zend_Layout renders twice on ViewRenderer exception

Description

Given a TestController with actions "succesful" and "missingviewscript", configured to use the following layout:


[DEFAULT LAYOUT HEADER]
<?php 
    echo $this->layout()->content;
?>
[DEFAULT LAYOUT FOOTER]

And an ErrorController, configured to use the following layout:


[ERROR LAYOUT HEADER]
<?php 
    echo $this->layout()->content;
?>
[ERROR LAYOUT FOOTER]

With the following defaults unchanged: Zend_Controller_Front::_throwExceptions is false, Zend_Layout::_mvcSuccessfulActionOnly is true, and Zend_Controller_Action_Helper_ViewRenderer and Zend_Controller_Plugin_ErrorHandler are enabled.

/test/succesful expected and seen:


[DEFAULT LAYOUT HEADER]
(TestController::succesfulAction output)
[DEFAULT LAYOUT FOOTER]

/test/missingaction expected and seen:


ERROR LAYOUT HEADER]
(ErrorController::errorAction output)
[ERROR LAYOUT FOOTER]

/test/missingviewscript expected:


[ERROR LAYOUT HEADER]
(ErrorController::errorAction output)
[ERROR LAYOUT FOOTER]

/test/missingviewscript seen:


[ERROR LAYOUT HEADER]
[DEFAULT LAYOUT HEADER]

[DEFAULT LAYOUT FOOTER]
(ErrorController::errorAction output)
[ERROR LAYOUT FOOTER]

This appears to occur because when _throwExceptions is false, the exception thrown by Zend_Controller_Action_Helper_ViewRenderer is caught and Zend_Layout_Controller_Action_Helper_Layout::postDispatch() is allowed to run, setting _isActionControllerSuccessful to true. Therefore the default layout is rendered around the empty content of missingviewscriptAction, then the error layout is rendered around this content and the errorAction content.

The preferred bahavior would be to skip rendering of the layout around the missingviewscriptAction, either by default or through configuration.

Comments

Assigning to Ralph for triage.

Also, if you add another action to TestController called "viewscriptexception" and a corresponding viewscriptexception.pthml that throws an exception, you get the same double-render issue.

The way I understand it, here's what's happening. The following assumes that the error handler, view renderer, and layout plugins are all active.

Zend_Controller_Front::dispatch() calls the Zend_Controller_Action::dispatch()

Zend_Controller_Action::dispatch() calls $this->$action(), which returns normally (no exception is thrown yet)

Zend_Controller_Action::dispatch() calls $this->_helper->notifyPostDispatch()

The layout helper gets notified first. In Zend_Layout_Controller_Action_Helper_Layout::postDispatch(), $this->_isActionControllerSuccessful is set to true.

The viewrenderer helper gets notified next. In Zend_Controller_Action_Helper_ViewRenderer::postDispatch(), $this->render() is called. This is where the trouble begins - in Philip's example, the view script is not found (thus generating an exception); in my example, the view script is found but it throws an exception.

Control now returns to the front controller (Zend_Controller_Front::dispatch()). The exception is caught, and $this->_response->setExecption() is called. Next, Zend_Controller_Front::dispatch() calls $this->_plugins->postDispatch().

The layout plugin is again notified first. Zend_Layout_Controller_Plugin_Layout::postDispatch() runs, and since isActionControllerSuccessful was set to true earlier, it doesn't return early. An empty layout is rendered.

The errorhandler is notified next. Zend_Controller_Plugin_ErrorHandler::postDispatch() runs, sets a bunch of stuff, forwards to the error controllre, which renders again, causing the second layout and error content to be rendered.

The way I've found to band-aid over the issue is to apply the following patch:


--- library/Zend/Layout/Controller/Plugin/Layout.php  Wed Mar 19 22:28:37 2008 -0700
+++ library/Zend/Layout/Controller/Plugin/Layout.php  Wed Mar 19 23:11:40 2008 -0700
@@ -123,8 +123,14 @@
             return;
         }
 
-        $response   = $this->getResponse();
-        $content    = $response->getBody(true);
+        $response = $this->getResponse();
+
+        // Return early if an exception was thrown, unless this is the error handler
+        if ($response->isException() && !$request->getParam('error_handler')) {
+            return;
+        }
+
+        $content = $response->getBody(true);
         $contentKey = $layout->getContentKey();
 
         if (isset($content['default'])) {

This causes the first run through Zend_Layout_Controller_Plugin_Layout::postDispatch() to return early, because the response already has its exception set, but no error handler has been set yet, since the error handler plugin has not yet run. The second run through Zend_Layout_Controller_Plugin_Layout::postDispatch() renders the layout and the error content, as expected.

There are two problems with this patch that are immediately obvious. First, it doesn't take mvcSuccessfulActionOnly into account. If mvcSuccessfulActionOnly is set to false, this patch will still cause the layout to not render the first time around. Another problem is that the fix is hard-coded to detect behavior of the error handler plugin, which links the two classes together in a way that they probably shouldn't be linked. In particular, if the error handler plugin was disabled, exceptions would never be rendered with a layout. So this patch is not probably not ready to be applied as-is. But I hope it illustrates the problem and makes getting to a real fix easier.

Finally, I'd like to mention that the documentation for mvcSuccessfulActionOnly on http://framework.zend.com/manual/en/… has a typo in it - "flag" is incorrectly spelled as "flat".

Scheduling for next mini release

There is actually a simple fix for this you can put into your bootstrap file. Before the call to use Zend_Layout::startMvc(/* ... */); try explicitly added the ViewRenderer to the Broker.

The call would basically look like this:


Zend_Controller_Action_HelperBroker::addHelper(new Zend_Controller_Action_Helper_ViewRenderer());
Zend_Layout::startMvc(array('layout' => 'default', 'layoutPath' => APPLICATION_PATH . '/layouts/'));

What this allows to happen is that the viewRenderer will run before the Layout action helper and will be able to detect if a view script exception had occured, thus affecting whether or not Zend_Layout can detect if there was a SuccessfulAction.

The real solution which I have been working on is implementing a better stack for the Zend_Controller_Action_HelperBroker. This is turning out to be more than a few line fix, so I think it would be best to use the above workaround in 1.5.x and then I will introduce the HelperBroker_Stack in 1.6 as a new feature.

Having a proper stack will allow Zend_Controller to register the ViewRenderer at a specific predetermined location in the stack, and will also allow Zend_Layout to register its action helper at a predetermined location in the stack. If both can go into known locations, we can ensure that the viewRenderer (And all other future action helpers - system or user), can be assured that they can run before the layout action helper thus - fixing the problem of "unkown state regarding successful action controllers".

Moving to next minor release at it includes a new feature.

Fixed at r10734 in trunk. Fixed at r10746 in release 1.6 branch.

Updating for the 1.6.0 release.