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\ModuleManager;
11
12use Traversable;
13use Zend\EventManager\EventManager;
14use Zend\EventManager\EventManagerInterface;
15
16/**
17 * Module manager
18 */
19class ModuleManager implements ModuleManagerInterface
20{
21    /**#@+
22     * Reference to Zend\Mvc\MvcEvent::EVENT_BOOTSTRAP
23     */
24    const EVENT_BOOTSTRAP = 'bootstrap';
25    /**#@-*/
26
27    /**
28     * @var array An array of Module classes of loaded modules
29     */
30    protected $loadedModules = array();
31
32    /**
33     * @var EventManagerInterface
34     */
35    protected $events;
36
37    /**
38     * @var ModuleEvent
39     */
40    protected $event;
41
42    /**
43     * @var bool
44     */
45    protected $loadFinished;
46
47    /**
48     * modules
49     *
50     * @var array|Traversable
51     */
52    protected $modules = array();
53
54    /**
55     * True if modules have already been loaded
56     *
57     * @var bool
58     */
59    protected $modulesAreLoaded = false;
60
61    /**
62     * Constructor
63     *
64     * @param  array|Traversable $modules
65     * @param  EventManagerInterface $eventManager
66     */
67    public function __construct($modules, EventManagerInterface $eventManager = null)
68    {
69        $this->setModules($modules);
70        if ($eventManager instanceof EventManagerInterface) {
71            $this->setEventManager($eventManager);
72        }
73    }
74
75    /**
76     * Handle the loadModules event
77     *
78     * @return void
79     */
80    public function onLoadModules()
81    {
82        if (true === $this->modulesAreLoaded) {
83            return $this;
84        }
85
86        foreach ($this->getModules() as $moduleName => $module) {
87            if (is_object($module)) {
88                if (!is_string($moduleName)) {
89                    throw new Exception\RuntimeException(sprintf(
90                        'Module (%s) must have a key identifier.',
91                        get_class($module)
92                    ));
93                }
94                $module = array($moduleName => $module);
95            }
96            $this->loadModule($module);
97        }
98
99        $this->modulesAreLoaded = true;
100    }
101
102    /**
103     * Load the provided modules.
104     *
105     * @triggers loadModules
106     * @triggers loadModules.post
107     * @return   ModuleManager
108     */
109    public function loadModules()
110    {
111        if (true === $this->modulesAreLoaded) {
112            return $this;
113        }
114
115        $this->getEventManager()->trigger(ModuleEvent::EVENT_LOAD_MODULES, $this, $this->getEvent());
116
117        /**
118         * Having a dedicated .post event abstracts the complexity of priorities from the user.
119         * Users can attach to the .post event and be sure that important
120         * things like config merging are complete without having to worry if
121         * they set a low enough priority.
122         */
123        $this->getEventManager()->trigger(ModuleEvent::EVENT_LOAD_MODULES_POST, $this, $this->getEvent());
124
125        return $this;
126    }
127
128    /**
129     * Load a specific module by name.
130     *
131     * @param  string|array               $module
132     * @throws Exception\RuntimeException
133     * @triggers loadModule.resolve
134     * @triggers loadModule
135     * @return mixed Module's Module class
136     */
137    public function loadModule($module)
138    {
139        $moduleName = $module;
140        if (is_array($module)) {
141            $moduleName = key($module);
142            $module = current($module);
143        }
144
145        if (isset($this->loadedModules[$moduleName])) {
146            return $this->loadedModules[$moduleName];
147        }
148
149        /*
150         * Keep track of nested module loading using the $loadFinished
151         * property.
152         *
153         * Increment the value for each loadModule() call and then decrement
154         * once the loading process is complete.
155         *
156         * To load a module, we clone the event if we are inside a nested
157         * loadModule() call, and use the original event otherwise.
158         */
159        if (!isset($this->loadFinished)) {
160            $this->loadFinished = 0;
161        }
162
163        $event = ($this->loadFinished > 0) ? clone $this->getEvent() : $this->getEvent();
164        $event->setModuleName($moduleName);
165
166        $this->loadFinished++;
167
168        if (!is_object($module)) {
169            $module = $this->loadModuleByName($event);
170        }
171        $event->setModule($module);
172
173        $this->loadedModules[$moduleName] = $module;
174        $this->getEventManager()->trigger(ModuleEvent::EVENT_LOAD_MODULE, $this, $event);
175
176        $this->loadFinished--;
177
178        return $module;
179    }
180
181    /**
182     * Load a module with the name
183     * @param  \Zend\EventManager\EventInterface $event
184     * @return mixed                            module instance
185     * @throws Exception\RuntimeException
186     */
187    protected function loadModuleByName($event)
188    {
189        $result = $this->getEventManager()->trigger(ModuleEvent::EVENT_LOAD_MODULE_RESOLVE, $this, $event, function ($r) {
190            return (is_object($r));
191        });
192
193        $module = $result->last();
194        if (!is_object($module)) {
195            throw new Exception\RuntimeException(sprintf(
196                'Module (%s) could not be initialized.',
197                $event->getModuleName()
198            ));
199        }
200
201        return $module;
202    }
203
204    /**
205     * Get an array of the loaded modules.
206     *
207     * @param  bool  $loadModules If true, load modules if they're not already
208     * @return array An array of Module objects, keyed by module name
209     */
210    public function getLoadedModules($loadModules = false)
211    {
212        if (true === $loadModules) {
213            $this->loadModules();
214        }
215
216        return $this->loadedModules;
217    }
218
219    /**
220     * Get an instance of a module class by the module name
221     *
222     * @param  string $moduleName
223     * @return mixed
224     */
225    public function getModule($moduleName)
226    {
227        if (!isset($this->loadedModules[$moduleName])) {
228            return;
229        }
230        return $this->loadedModules[$moduleName];
231    }
232
233    /**
234     * Get the array of module names that this manager should load.
235     *
236     * @return array
237     */
238    public function getModules()
239    {
240        return $this->modules;
241    }
242
243    /**
244     * Set an array or Traversable of module names that this module manager should load.
245     *
246     * @param  mixed $modules array or Traversable of module names
247     * @throws Exception\InvalidArgumentException
248     * @return ModuleManager
249     */
250    public function setModules($modules)
251    {
252        if (is_array($modules) || $modules instanceof Traversable) {
253            $this->modules = $modules;
254        } else {
255            throw new Exception\InvalidArgumentException(
256                sprintf(
257                    'Parameter to %s\'s %s method must be an array or implement the Traversable interface',
258                    __CLASS__,
259                    __METHOD__
260                )
261            );
262        }
263        return $this;
264    }
265
266    /**
267     * Get the module event
268     *
269     * @return ModuleEvent
270     */
271    public function getEvent()
272    {
273        if (!$this->event instanceof ModuleEvent) {
274            $this->setEvent(new ModuleEvent);
275        }
276        return $this->event;
277    }
278
279    /**
280     * Set the module event
281     *
282     * @param  ModuleEvent $event
283     * @return ModuleManager
284     */
285    public function setEvent(ModuleEvent $event)
286    {
287        $this->event = $event;
288        return $this;
289    }
290
291    /**
292     * Set the event manager instance used by this module manager.
293     *
294     * @param  EventManagerInterface $events
295     * @return ModuleManager
296     */
297    public function setEventManager(EventManagerInterface $events)
298    {
299        $events->setIdentifiers(array(
300            __CLASS__,
301            get_class($this),
302            'module_manager',
303        ));
304        $this->events = $events;
305        $this->attachDefaultListeners();
306        return $this;
307    }
308
309    /**
310     * Retrieve the event manager
311     *
312     * Lazy-loads an EventManager instance if none registered.
313     *
314     * @return EventManagerInterface
315     */
316    public function getEventManager()
317    {
318        if (!$this->events instanceof EventManagerInterface) {
319            $this->setEventManager(new EventManager());
320        }
321        return $this->events;
322    }
323
324    /**
325     * Register the default event listeners
326     *
327     * @return ModuleManager
328     */
329    protected function attachDefaultListeners()
330    {
331        $events = $this->getEventManager();
332        $events->attach(ModuleEvent::EVENT_LOAD_MODULES, array($this, 'onLoadModules'));
333    }
334}
335