Skip to content
This repository

Workaround for double-template rendering with forward() plug-in. #2206

Merged
merged 1 commit into from over 1 year ago

3 participants

Demian Katz Don't Add Me To Your Organization a.k.a The Travis Bot Matthew Weier O'Phinney
Demian Katz

If a caller returns the value of $this->forward()->dispatch() in a controller action while the InjectViewModelListener is active, the same view model gets attached to the layout twice, and the PHP template will be rendered twice, wasting resources and possibly causing strange side effects. This commit detaches the problem listener before dispatching a forwarded action so that the output of dispatch() can be safely returned by the calling action. It also provides a generic, extensible mechanism for detaching other listeners in different use cases.

Demian Katz If a caller returns the value of $this->forward()->dispatch() in a co…
…ntroller action while the InjectViewModelListener is active, the same view model gets attached to the layout twice, and the PHP template will be rendered twice, wasting resources and possibly causing strange side effects. This commit detaches the problem listener before dispatching a forwarded action. It also provides a generic, extensible mechanism for detaching other listeners in different use cases.
0b8fc7a
Don't Add Me To Your Organization a.k.a The Travis Bot

This pull request passes (merged 0b8fc7a into 5dd58b8).

Matthew Weier O'Phinney weierophinney commented on the diff August 20, 2012
library/Zend/Mvc/Controller/Plugin/Forward.php
@@ -58,6 +64,46 @@ public function setMaxNestedForwards($maxNestedForwards)
58 64
     }
59 65
 
60 66
     /**
  67
+     * Get information on listeners that need to be detached before dispatching.
  68
+     *
  69
+     * Each entry in the array contains three keys:
  70
+     *
  71
+     * id (identifier for event-emitting component),
  72
+     * event (the hooked event)
  73
+     * and class (the class of listener that should be detached).
  74
+     *
  75
+     * @return array
  76
+     */
  77
+    public function getListenersToDetach()
  78
+    {
  79
+        // If a blacklist has not been explicitly set, return the default:
  80
+        if (is_null($this->listenersToDetach)) {
1
Matthew Weier O'Phinney Owner

The convention within the framework is to use "null === $this->listenersToDetach", or, if you despise yoda conditions, "$this->listenersToDetach === null", instead of is_null().

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Matthew Weier O'Phinney weierophinney commented on the diff August 20, 2012
library/Zend/Mvc/Controller/Plugin/Forward.php
((10 lines not shown))
  73
+     * and class (the class of listener that should be detached).
  74
+     *
  75
+     * @return array
  76
+     */
  77
+    public function getListenersToDetach()
  78
+    {
  79
+        // If a blacklist has not been explicitly set, return the default:
  80
+        if (is_null($this->listenersToDetach)) {
  81
+            // We need to detach the InjectViewModelListener to prevent templates
  82
+            // from getting attached to the ViewModel twice when a calling action
  83
+            // returns the output generated by a forwarded action.
  84
+            $this->listenersToDetach = array(
  85
+                array(
  86
+                    'id' => 'Zend\Stdlib\DispatchableInterface',
  87
+                    'event' => MvcEvent::EVENT_DISPATCH,
  88
+                    'class' => 'Zend\Mvc\View\Http\InjectViewModelListener'
1
Matthew Weier O'Phinney Owner

Add a comma after last element in array (allows re-ordering or adding new items with minimal changeset in the VCS).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Matthew Weier O'Phinney weierophinney referenced this pull request from a commit August 20, 2012
Matthew Weier O'Phinney [#2206] consistency fixes
- Per comments on issue.
c6ea13d
Matthew Weier O'Phinney
Owner

I took care of all suggestions above when merging.

Matthew Weier O'Phinney weierophinney merged commit 0b8fc7a into from August 20, 2012
Matthew Weier O'Phinney weierophinney closed this August 20, 2012
Deleted user Unknown referenced this pull request from a commit August 20, 2012
Matthew Weier O'Phinney [#2206] consistency fixes
- Per comments on issue.
a4ebab0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Showing 1 unique commit by 1 author.

Aug 20, 2012
Demian Katz If a caller returns the value of $this->forward()->dispatch() in a co…
…ntroller action while the InjectViewModelListener is active, the same view model gets attached to the layout twice, and the PHP template will be rendered twice, wasting resources and possibly causing strange side effects. This commit detaches the problem listener before dispatching a forwarded action. It also provides a generic, extensible mechanism for detaching other listeners in different use cases.
0b8fc7a
This page is out of date. Refresh to see the latest.
118  library/Zend/Mvc/Controller/Plugin/Forward.php
@@ -10,6 +10,7 @@
10 10
 
11 11
 namespace Zend\Mvc\Controller\Plugin;
12 12
 
  13
+use Zend\EventManager\SharedEventManagerInterface as SharedEvents;
13 14
 use Zend\Mvc\Exception;
14 15
 use Zend\Mvc\InjectApplicationEventInterface;
15 16
 use Zend\Mvc\MvcEvent;
@@ -46,6 +47,11 @@ class Forward extends AbstractPlugin
46 47
     protected $numNestedForwards = 0;
47 48
 
48 49
     /**
  50
+     * @var array
  51
+     */
  52
+    protected $listenersToDetach = null;
  53
+
  54
+    /**
49 55
      * Set maximum number of nested forwards allowed
50 56
      *
51 57
      * @param  int $maxNestedForwards
@@ -58,6 +64,46 @@ public function setMaxNestedForwards($maxNestedForwards)
58 64
     }
59 65
 
60 66
     /**
  67
+     * Get information on listeners that need to be detached before dispatching.
  68
+     *
  69
+     * Each entry in the array contains three keys:
  70
+     *
  71
+     * id (identifier for event-emitting component),
  72
+     * event (the hooked event)
  73
+     * and class (the class of listener that should be detached).
  74
+     *
  75
+     * @return array
  76
+     */
  77
+    public function getListenersToDetach()
  78
+    {
  79
+        // If a blacklist has not been explicitly set, return the default:
  80
+        if (is_null($this->listenersToDetach)) {
  81
+            // We need to detach the InjectViewModelListener to prevent templates
  82
+            // from getting attached to the ViewModel twice when a calling action
  83
+            // returns the output generated by a forwarded action.
  84
+            $this->listenersToDetach = array(
  85
+                array(
  86
+                    'id' => 'Zend\Stdlib\DispatchableInterface',
  87
+                    'event' => MvcEvent::EVENT_DISPATCH,
  88
+                    'class' => 'Zend\Mvc\View\Http\InjectViewModelListener'
  89
+                )
  90
+            );
  91
+        }
  92
+        return $this->listenersToDetach;
  93
+    }
  94
+
  95
+    /**
  96
+     * Set information on listeners that need to be detached before dispatching.
  97
+     *
  98
+     * @param  array $listeners Listener information; see getListenersToDetach() for details on format.
  99
+     * @return void
  100
+     */
  101
+    public function setListenersToDetach($listeners)
  102
+    {
  103
+        $this->listenersToDetach = $listeners;
  104
+    }
  105
+
  106
+    /**
61 107
      * Dispatch another controller
62 108
      *
63 109
      * @param  string $name Controller name; either a class name or an alias used in the DI container or service locator
@@ -101,14 +147,86 @@ public function dispatch($name, array $params = null)
101 147
         }
102 148
         $this->numNestedForwards++;
103 149
 
  150
+        // Detach listeners that may cause problems during dispatch:
  151
+        $sharedEvents = $event->getApplication()->getEventManager()->getSharedManager();
  152
+        $listeners = $this->detachProblemListeners($sharedEvents);
  153
+
104 154
         $return = $controller->dispatch($event->getRequest(), $event->getResponse());
105 155
 
  156
+        // If we detached any listeners, reattach them now:
  157
+        $this->reattachProblemListeners($sharedEvents, $listeners);
  158
+
106 159
         $this->numNestedForwards--;
107 160
 
108 161
         return $return;
109 162
     }
110 163
 
111 164
     /**
  165
+     * Detach problem listeners specified by getListenersToDetach() and return an array of information that will
  166
+     * allow them to be reattached.
  167
+     *
  168
+     * @param  SharedEvents $sharedEvents Shared event manager
  169
+     * @return array
  170
+     */
  171
+    protected function detachProblemListeners(SharedEvents $sharedEvents)
  172
+    {
  173
+        // Convert the problem list from two-dimensional array to more convenient id => event => class format:
  174
+        $formattedProblems = array();
  175
+        foreach ($this->getListenersToDetach() as $current) {
  176
+            if (!isset($formattedProblems[$current['id']])) {
  177
+                $formattedProblems[$current['id']] = array();
  178
+            }
  179
+            if (!isset($formattedProblems[$current['id']][$current['event']])) {
  180
+                $formattedProblems[$current['id']][$current['event']] = array();
  181
+            }
  182
+            $formattedProblems[$current['id']][$current['event']][] = $current['class'];
  183
+        }
  184
+
  185
+        // Loop through the class blacklist, detaching problem events and remembering their CallbackHandlers
  186
+        // for future reference:
  187
+        $results = array();
  188
+        foreach ($formattedProblems as $id => $eventArray) {
  189
+            $results[$id] = array();
  190
+            foreach ($eventArray as $eventName => $classArray) {
  191
+                $results[$id][$eventName] = array();
  192
+                $events = $sharedEvents->getListeners($id, $eventName);
  193
+                foreach ($events as $currentEvent) {
  194
+                    $currentCallback = $currentEvent->getCallback();
  195
+                    if (!isset($currentCallback[0])) {
  196
+                        continue;
  197
+                    }
  198
+                    foreach ($classArray as $class) {
  199
+                        if (is_a($currentCallback[0], $class)) {
  200
+                            $sharedEvents->detach($id, $currentEvent);
  201
+                            $results[$id][$eventName][] = $currentEvent;
  202
+                        }
  203
+                    }
  204
+                }
  205
+            }
  206
+        }
  207
+
  208
+        return $results;
  209
+    }
  210
+
  211
+    /**
  212
+     * Reattach all problem listeners detached by detachProblemListeners(), if any.
  213
+     *
  214
+     * @param  SharedEvents $sharedEvents Shared event manager
  215
+     * @param  array        $listeners    Output of detachProblemListeners()
  216
+     * @return void
  217
+     */
  218
+    protected function reattachProblemListeners(SharedEvents $sharedEvents, array $listeners)
  219
+    {
  220
+        foreach ($listeners as $id => $eventArray) {
  221
+            foreach ($eventArray as $eventName => $callbacks) {
  222
+                foreach ($callbacks as $current) {
  223
+                    $sharedEvents->attach($id, $eventName, $current->getCallback(), $current->getMetadatum('priority'));
  224
+                }
  225
+            }
  226
+        }
  227
+    }
  228
+
  229
+    /**
112 230
      * Get the locator
113 231
      *
114 232
      * @return ServiceLocatorInterface
9  tests/ZendTest/Mvc/Controller/Plugin/ForwardTest.php
@@ -29,7 +29,16 @@ class ForwardTest extends TestCase
29 29
     public function setUp()
30 30
     {
31 31
         StaticEventManager::resetInstance();
  32
+
  33
+        $mockSharedEventManager = $this->getMock('Zend\EventManager\SharedEventManagerInterface');
  34
+        $mockSharedEventManager->expects($this->any())->method('getListeners')->will($this->returnValue(array()));
  35
+        $mockEventManager = $this->getMock('Zend\EventManager\EventManagerInterface');
  36
+        $mockEventManager->expects($this->any())->method('getSharedManager')->will($this->returnValue($mockSharedEventManager));
  37
+        $mockApplication = $this->getMock('Zend\Mvc\ApplicationInterface');
  38
+        $mockApplication->expects($this->any())->method('getEventManager')->will($this->returnValue($mockEventManager));
  39
+
32 40
         $event   = new MvcEvent();
  41
+        $event->setApplication($mockApplication);
33 42
         $event->setRequest(new Request());
34 43
         $event->setResponse(new Response());
35 44
         $event->setRouteMatch(new RouteMatch(array('action' => 'test')));
Commit_comment_tip

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.