Moving VuFind to Zend Framework 2: Part 5 — The Dreaded forward() Bug
IMPORTANT UPDATE: The problem discussed in this post only applies to Zend Framework 2.0 RC4 and earlier. Starting with RC5, a fix was introduced which eliminates the double-rendering issue described below. With the fix in place, you can do the logical thing of returning the output of $this->forward()->dispatch() rather than having to return false. Thanks to Nicholas Calugar, Matthew Weier O’Phinney, and all the others on zf-contributors and #zftalk.2 who talked to me about this problem and helped me refine my pull request to fix it.
It’s a law of software development that if you can’t explain some weird behavior and you choose to ignore it, it will inevitably come back to bite you. I know this very well, but knowing it doesn’t prevent it from happening.
When I first implemented VuFind’s Flash Messenger view helper (designed to display status messages stored in the session), I ran into a strange problem. The behavior of the helper was simple: check the session for messages, display them, then clear the session. The problem was that flash messages never displayed. Eventually I just commented out the “clear the session” line and everything started working. I didn’t have time to figure out what was really going on, so I just put a note in the code to check it later, and moved on to other things.
The Real Problem
I kept running into bizarre flash messenger behavior, so I decided I needed to solve this problem for real rather than just putting on a band-aid. After digging deep into ZF2 and putting logging lines all over the place, I figured out what was causing the problem: many of my view templates were getting rendered twice. Even though the templates displayed on screen just once, the code was executing twice. This was the reason for my flash messenger problem: the first render displayed the messages and cleared them. The second render replaced the first, and by this point, the messages were gone.
The Root Cause
After a lot more digging and some extremely valuable help from mpinkston on the #zftalk.2 IRC channel, I discovered the root cause. Sometimes you want to forward from one controller action to another. In ZF1, you could use “return $this->_forward(…)” to achieve the effect. In ZF2, there is a controller plug-in called forward() that does the same basic thing. I was using it like this:
return $this->forward()->dispatch('controllerName', array('action' => 'actionName'));
Seems harmless enough, but what actually happens here is that the forward() plug-in dispatches an action, which causes ZF2 to attach a view model to the top-level layout object. The forward() plug-in then returns that view model. When the calling controller returns it, ZF2 attaches a second copy to the layout object. And then both copies get rendered!
What I should have been doing was this:
$this->forward()->dispatch('controllerName', array('action' => 'actionName'));
A simple, honest mistake with evil, subtle consequences.
I’ve already found a solution to my immediate problem, as described above. I won’t make this mistake again, and VuFind’s flash messenger now works correctly. I was concerned that the solution would only work when forwarding to actions that return a ViewModel, but it seems to work for all cases, even when a Response is returned instead (i.e. when redirecting to another page).
I remain concerned that this is a big potential pitfall for users of ZF2 in general. It’s an easy mistake to make, and the consequences are extremely non-obvious, especially since in many cases, the only result of the mistake is a slight performance hit caused by duplicate work being done.
Hopefully this will be addressed in the future either by notes in the documentation or by the introduction of a convenience method in the forward() plugin that returns false instead of the actual output of the dispatched action. If either of these things had existed, I probably wouldn’t have spent the better part of a work day chasing this problem. But I don’t mean to criticize the ZF2 team — I like the framework, and these are the perils of working with software that is still under development. I just point this out as something to watch out for that should be addressed, one way or another, before the final 2.0 official release.
2 Comments »
RSS feed for comments on this post. TrackBack URI
One little bit of followup to this. Since using the forward() plugin is quite verbose, and since all of VuFind’s forward actions follow the same basic pattern, I have created a helper method called forwardTo() in the base controller that all of VuFind’s other controllers inherit from. The new method allows this lengthy and unattractive code:
$this->forward()->dispatch(‘controller’, array(‘action’ => ‘action’));
to be simplified down to:
return $this->forwardTo(‘controller’, ‘action’);
If ZF2 eventually incorporates its own convenience method, we might consider switching to that. But for now, this makes the code more readable and gives us a centralized place where we can add extra forward-related processing if we run into a new forwarding problem similar to the one described above.
In case anyone’s interested, here are the relevant diffs:
Another update: this is now a non-issue; the framework has been updated so that the forward() plug-in behaves more logically. See the notes added to the top of the post for more details.