3 * Zend Framework (http://framework.zend.com/)
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
10 namespace Zend\Mvc\Controller\Plugin
;
12 use Zend\EventManager\SharedEventManagerInterface
as SharedEvents
;
13 use Zend\Mvc\Controller\ControllerManager
;
14 use Zend\Mvc\Exception
;
15 use Zend\Mvc\InjectApplicationEventInterface
;
16 use Zend\Mvc\MvcEvent
;
17 use Zend\Mvc\Router\RouteMatch
;
18 use Zend\Stdlib\CallbackHandler
;
20 class Forward
extends AbstractPlugin
23 * @var ControllerManager
25 protected $controllers;
35 protected $maxNestedForwards = 10;
40 protected $numNestedForwards = 0;
45 protected $listenersToDetach = null;
48 * @param ControllerManager $controllers
50 public function __construct(ControllerManager
$controllers)
52 $this->controllers
= $controllers;
56 * Set maximum number of nested forwards allowed
58 * @param int $maxNestedForwards
61 public function setMaxNestedForwards($maxNestedForwards)
63 $this->maxNestedForwards
= (int) $maxNestedForwards;
69 * Get information on listeners that need to be detached before dispatching.
71 * Each entry in the array contains three keys:
73 * id (identifier for event-emitting component),
74 * event (the hooked event)
75 * and class (the class of listener that should be detached).
79 public function getListenersToDetach()
81 // If a blacklist has not been explicitly set, return the default:
82 if (null === $this->listenersToDetach
) {
83 // We need to detach the InjectViewModelListener to prevent templates
84 // from getting attached to the ViewModel twice when a calling action
85 // returns the output generated by a forwarded action.
86 $this->listenersToDetach
= [[
87 'id' => 'Zend\Stdlib\DispatchableInterface',
88 'event' => MvcEvent
::EVENT_DISPATCH
,
89 'class' => 'Zend\Mvc\View\Http\InjectViewModelListener',
92 return $this->listenersToDetach
;
96 * Set information on listeners that need to be detached before dispatching.
98 * @param array $listeners Listener information; see getListenersToDetach() for details on format.
102 public function setListenersToDetach($listeners)
104 $this->listenersToDetach
= $listeners;
110 * Dispatch another controller
112 * @param string $name Controller name; either a class name or an alias used in the controller manager
113 * @param null|array $params Parameters with which to seed a custom RouteMatch object for the new controller
115 * @throws Exception\DomainException if composed controller does not define InjectApplicationEventInterface
116 * or Locator aware; or if the discovered controller is not dispatchable
118 public function dispatch($name, array $params = null)
120 $event = clone($this->getEvent());
122 $controller = $this->controllers
->get($name);
123 if ($controller instanceof InjectApplicationEventInterface
) {
124 $controller->setEvent($event);
127 // Allow passing parameters to seed the RouteMatch with & copy matched route name
128 if ($params !== null) {
129 $routeMatch = new RouteMatch($params);
130 $routeMatch->setMatchedRouteName($event->getRouteMatch()->getMatchedRouteName());
131 $event->setRouteMatch($routeMatch);
134 if ($this->numNestedForwards
> $this->maxNestedForwards
) {
135 throw new Exception\
DomainException("Circular forwarding detected: greater than $this->maxNestedForwards nested forwards");
137 $this->numNestedForwards++
;
139 // Detach listeners that may cause problems during dispatch:
140 $sharedEvents = $event->getApplication()->getEventManager()->getSharedManager();
141 $listeners = $this->detachProblemListeners($sharedEvents);
143 $return = $controller->dispatch($event->getRequest(), $event->getResponse());
145 // If we detached any listeners, reattach them now:
146 $this->reattachProblemListeners($sharedEvents, $listeners);
148 $this->numNestedForwards
--;
154 * Detach problem listeners specified by getListenersToDetach() and return an array of information that will
155 * allow them to be reattached.
157 * @param SharedEvents $sharedEvents Shared event manager
160 protected function detachProblemListeners(SharedEvents
$sharedEvents)
162 // Convert the problem list from two-dimensional array to more convenient id => event => class format:
163 $formattedProblems = [];
164 foreach ($this->getListenersToDetach() as $current) {
165 if (!isset($formattedProblems[$current['id']])) {
166 $formattedProblems[$current['id']] = [];
168 if (!isset($formattedProblems[$current['id']][$current['event']])) {
169 $formattedProblems[$current['id']][$current['event']] = [];
171 $formattedProblems[$current['id']][$current['event']][] = $current['class'];
174 // Loop through the class blacklist, detaching problem events and remembering their CallbackHandlers
175 // for future reference:
177 foreach ($formattedProblems as $id => $eventArray) {
179 foreach ($eventArray as $eventName => $classArray) {
180 $results[$id][$eventName] = [];
181 $events = $this->getSharedListenersById($id, $eventName, $sharedEvents);
182 foreach ($events as $priority => $currentPriorityEvents) {
184 if (!is_array($currentPriorityEvents)) {
185 $currentPriorityEvents = [$currentPriorityEvents];
188 foreach ($currentPriorityEvents as $currentEvent) {
189 $currentCallback = $currentEvent;
191 // zend-eventmanager v2 compatibility:
192 if ($currentCallback instanceof CallbackHandler
) {
193 $currentCallback = $currentEvent->getCallback();
194 $priority = $currentEvent->getMetadatum('priority');
197 // If we have an array, grab the object
198 if (is_array($currentCallback)) {
199 $currentCallback = array_shift($currentCallback);
202 // This routine is only valid for object callbacks
203 if (!is_object($currentCallback)) {
207 foreach ($classArray as $class) {
208 if ($currentCallback instanceof $class) {
209 // Pass $currentEvent; when using zend-eventmanager v2,
210 // this is the CallbackHandler, while in v3 it's
211 // the actual listener.
212 $this->detachSharedListener($id, $currentEvent, $sharedEvents);
213 $results[$id][$eventName][$priority] = $currentEvent;
225 * Reattach all problem listeners detached by detachProblemListeners(), if any.
227 * @param SharedEvents $sharedEvents Shared event manager
228 * @param array $listeners Output of detachProblemListeners()
231 protected function reattachProblemListeners(SharedEvents
$sharedEvents, array $listeners)
233 foreach ($listeners as $id => $eventArray) {
234 foreach ($eventArray as $eventName => $callbacks) {
235 foreach ($callbacks as $priority => $current) {
236 $callback = $current;
238 // zend-eventmanager v2 compatibility:
239 if ($current instanceof CallbackHandler
) {
240 $callback = $current->getCallback();
241 $priority = $current->getMetadatum('priority');
244 $sharedEvents->attach($id, $eventName, $callback, $priority);
254 * @throws Exception\DomainException if unable to find event
256 protected function getEvent()
262 $controller = $this->getController();
263 if (!$controller instanceof InjectApplicationEventInterface
) {
264 throw new Exception\
DomainException(sprintf(
265 'Forward plugin requires a controller that implements InjectApplicationEventInterface; received %s',
266 (is_object($controller) ?
get_class($controller) : var_export($controller, 1))
270 $event = $controller->getEvent();
271 if (!$event instanceof MvcEvent
) {
274 $params = $event->getParams();
276 $event = new MvcEvent();
277 $event->setParams($params);
279 $this->event
= $event;
285 * Retrieve shared listeners for an event by identifier.
287 * Varies retrieval based on zend-eventmanager version.
289 * @param string|int $id
290 * @param string $event
291 * @param SharedEvents $sharedEvents
292 * @return array|\Traversable
294 private function getSharedListenersById($id, $event, SharedEvents
$sharedEvents)
296 if (method_exists($sharedEvents, 'attachAggregate')) {
298 return $sharedEvents->getListeners($id, $event) ?
: [];
302 return $sharedEvents->getListeners([$id], $event);
306 * Detach a shared listener by identifier.
308 * Varies detachment based on zend-eventmanager version.
310 * @param string|int $id
311 * @param callable|CallbackHandler $listener
312 * @param SharedEvents $sharedEvents
315 private function detachSharedListener($id, $listener, SharedEvents
$sharedEvents)
317 if (method_exists($sharedEvents, 'attachAggregate')) {
319 $sharedEvents->detach($id, $listener);
324 $sharedEvents->detach($listener, $id);