1<?php 2/** 3 * Zend Framework (http://framework.zend.com/) 4 * 5 * @link http://github.com/zendframework/zf2 for the canonical source repository 6 * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) 7 * @license http://framework.zend.com/license/new-bsd New BSD License 8 */ 9 10namespace Zend\Mvc\Controller\Plugin; 11 12use Zend\EventManager\SharedEventManagerInterface as SharedEvents; 13use Zend\Mvc\Controller\ControllerManager; 14use Zend\Mvc\Exception; 15use Zend\Mvc\InjectApplicationEventInterface; 16use Zend\Mvc\MvcEvent; 17use Zend\Mvc\Router\RouteMatch; 18 19class Forward extends AbstractPlugin 20{ 21 /** 22 * @var ControllerManager 23 */ 24 protected $controllers; 25 26 /** 27 * @var MvcEvent 28 */ 29 protected $event; 30 31 /** 32 * @var int 33 */ 34 protected $maxNestedForwards = 10; 35 36 /** 37 * @var int 38 */ 39 protected $numNestedForwards = 0; 40 41 /** 42 * @var array[]|null 43 */ 44 protected $listenersToDetach = null; 45 46 /** 47 * @param ControllerManager $controllers 48 */ 49 public function __construct(ControllerManager $controllers) 50 { 51 $this->controllers = $controllers; 52 } 53 54 /** 55 * Set maximum number of nested forwards allowed 56 * 57 * @param int $maxNestedForwards 58 * @return self 59 */ 60 public function setMaxNestedForwards($maxNestedForwards) 61 { 62 $this->maxNestedForwards = (int) $maxNestedForwards; 63 64 return $this; 65 } 66 67 /** 68 * Get information on listeners that need to be detached before dispatching. 69 * 70 * Each entry in the array contains three keys: 71 * 72 * id (identifier for event-emitting component), 73 * event (the hooked event) 74 * and class (the class of listener that should be detached). 75 * 76 * @return array 77 */ 78 public function getListenersToDetach() 79 { 80 // If a blacklist has not been explicitly set, return the default: 81 if (null === $this->listenersToDetach) { 82 // We need to detach the InjectViewModelListener to prevent templates 83 // from getting attached to the ViewModel twice when a calling action 84 // returns the output generated by a forwarded action. 85 $this->listenersToDetach = array(array( 86 'id' => 'Zend\Stdlib\DispatchableInterface', 87 'event' => MvcEvent::EVENT_DISPATCH, 88 'class' => 'Zend\Mvc\View\Http\InjectViewModelListener', 89 )); 90 } 91 return $this->listenersToDetach; 92 } 93 94 /** 95 * Set information on listeners that need to be detached before dispatching. 96 * 97 * @param array $listeners Listener information; see getListenersToDetach() for details on format. 98 * 99 * @return self 100 */ 101 public function setListenersToDetach($listeners) 102 { 103 $this->listenersToDetach = $listeners; 104 105 return $this; 106 } 107 108 /** 109 * Dispatch another controller 110 * 111 * @param string $name Controller name; either a class name or an alias used in the controller manager 112 * @param null|array $params Parameters with which to seed a custom RouteMatch object for the new controller 113 * @return mixed 114 * @throws Exception\DomainException if composed controller does not define InjectApplicationEventInterface 115 * or Locator aware; or if the discovered controller is not dispatchable 116 */ 117 public function dispatch($name, array $params = null) 118 { 119 $event = clone($this->getEvent()); 120 121 $controller = $this->controllers->get($name); 122 if ($controller instanceof InjectApplicationEventInterface) { 123 $controller->setEvent($event); 124 } 125 126 // Allow passing parameters to seed the RouteMatch with & copy matched route name 127 if ($params !== null) { 128 $routeMatch = new RouteMatch($params); 129 $routeMatch->setMatchedRouteName($event->getRouteMatch()->getMatchedRouteName()); 130 $event->setRouteMatch($routeMatch); 131 } 132 133 if ($this->numNestedForwards > $this->maxNestedForwards) { 134 throw new Exception\DomainException("Circular forwarding detected: greater than $this->maxNestedForwards nested forwards"); 135 } 136 $this->numNestedForwards++; 137 138 // Detach listeners that may cause problems during dispatch: 139 $sharedEvents = $event->getApplication()->getEventManager()->getSharedManager(); 140 $listeners = $this->detachProblemListeners($sharedEvents); 141 142 $return = $controller->dispatch($event->getRequest(), $event->getResponse()); 143 144 // If we detached any listeners, reattach them now: 145 $this->reattachProblemListeners($sharedEvents, $listeners); 146 147 $this->numNestedForwards--; 148 149 return $return; 150 } 151 152 /** 153 * Detach problem listeners specified by getListenersToDetach() and return an array of information that will 154 * allow them to be reattached. 155 * 156 * @param SharedEvents $sharedEvents Shared event manager 157 * @return array 158 */ 159 protected function detachProblemListeners(SharedEvents $sharedEvents) 160 { 161 // Convert the problem list from two-dimensional array to more convenient id => event => class format: 162 $formattedProblems = array(); 163 foreach ($this->getListenersToDetach() as $current) { 164 if (!isset($formattedProblems[$current['id']])) { 165 $formattedProblems[$current['id']] = array(); 166 } 167 if (!isset($formattedProblems[$current['id']][$current['event']])) { 168 $formattedProblems[$current['id']][$current['event']] = array(); 169 } 170 $formattedProblems[$current['id']][$current['event']][] = $current['class']; 171 } 172 173 // Loop through the class blacklist, detaching problem events and remembering their CallbackHandlers 174 // for future reference: 175 $results = array(); 176 foreach ($formattedProblems as $id => $eventArray) { 177 $results[$id] = array(); 178 foreach ($eventArray as $eventName => $classArray) { 179 $results[$id][$eventName] = array(); 180 $events = $sharedEvents->getListeners($id, $eventName); 181 foreach ($events as $currentEvent) { 182 $currentCallback = $currentEvent->getCallback(); 183 184 // If we have an array, grab the object 185 if (is_array($currentCallback)) { 186 $currentCallback = array_shift($currentCallback); 187 } 188 189 // This routine is only valid for object callbacks 190 if (!is_object($currentCallback)) { 191 continue; 192 } 193 194 foreach ($classArray as $class) { 195 if ($currentCallback instanceof $class) { 196 $sharedEvents->detach($id, $currentEvent); 197 $results[$id][$eventName][] = $currentEvent; 198 } 199 } 200 } 201 } 202 } 203 204 return $results; 205 } 206 207 /** 208 * Reattach all problem listeners detached by detachProblemListeners(), if any. 209 * 210 * @param SharedEvents $sharedEvents Shared event manager 211 * @param array $listeners Output of detachProblemListeners() 212 * @return void 213 */ 214 protected function reattachProblemListeners(SharedEvents $sharedEvents, array $listeners) 215 { 216 foreach ($listeners as $id => $eventArray) { 217 foreach ($eventArray as $eventName => $callbacks) { 218 foreach ($callbacks as $current) { 219 $sharedEvents->attach($id, $eventName, $current->getCallback(), $current->getMetadatum('priority')); 220 } 221 } 222 } 223 } 224 225 /** 226 * Get the event 227 * 228 * @return MvcEvent 229 * @throws Exception\DomainException if unable to find event 230 */ 231 protected function getEvent() 232 { 233 if ($this->event) { 234 return $this->event; 235 } 236 237 $controller = $this->getController(); 238 if (!$controller instanceof InjectApplicationEventInterface) { 239 throw new Exception\DomainException('Forward plugin requires a controller that implements InjectApplicationEventInterface'); 240 } 241 242 $event = $controller->getEvent(); 243 if (!$event instanceof MvcEvent) { 244 $params = array(); 245 if ($event) { 246 $params = $event->getParams(); 247 } 248 $event = new MvcEvent(); 249 $event->setParams($params); 250 } 251 $this->event = $event; 252 253 return $this->event; 254 } 255} 256