Passed
Pull Request — development (#3621)
by Emanuele
07:11
created

EventManager::pushModule()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 2
c 0
b 0
f 0
nc 1
nop 2
dl 0
loc 4
ccs 0
cts 2
cp 0
crap 2
rs 10
1
<?php
2
3
/**
4
 * Handle events in controller and classes
5
 *
6
 * @package   ElkArte Forum
7
 * @copyright ElkArte Forum contributors
8
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause (see accompanying LICENSE.txt file)
9
 *
10
 * @version 2.0 dev
11
 */
12
13
namespace ElkArte;
14
15
/**
16
 * Handle events in controller and classes
17
 *
18
 * High level overview:
19
 *
20
 * - Register your modules against a class in which it will be triggered with
21
 *  enableModules($moduleName, $class) such as enableModules('Mymodule', ['display','post'])
22
 * - You can also create a full core feature with ADMINDIR/ManageMymoduleModule.php file containing a
23
 * a static class of addCoreFeature.  The file and class will be auto discovered and called.
24
 * - Place your file in ElkArte/Modules/Mymodule/Display.php ElkArte/Modules/Mymodule/Post.php
25
 * - In Mymodules Display.php and Post.php create a `public static function hooks` which returns
26
 * what to do when triggered, example
27
 *     - ['prepare_context', ['\\ElkArte\\Modules\\Mymodule\\Display', 'do_something'], ['attachments', 'start']
28
 * - This will call the do_something() method in Display.php of the Mymodule directory and pass params
29
 * $attachments $start values from Display.php class (where the event was triggered).
30
 * - Some events have defined values passed, but you can always request other values as long as they
31
 * are class properties
32
 *
33
 */
34
class EventManager
35
{
36
	/** @var object[] Event An array of events, each entry is a different position. */
37
	protected $_registered_events = array();
38
39
	/** @var object[] Instances of addons already loaded. */
40
	protected $_instances = array();
41
42
	/** @var object Instances of the controller. */
43
	protected $_source = null;
44
45
	/** @var object Instances of the modules. */
46
	protected $_modules = [];
47
48
	/** @var string[] List of classes already registered. */
49
	protected $_classes = array();
50
51
	/** @var int Remembers the depth of nested modules triggers. */
52
	protected $depth = 0;
53
54
	/** @var null|string[] List of classes declared, kept here just to
55
	    avoid call get_declared_classes at each trigger */
56
	protected $_declared_classes = null;
57
58
	/**
59 101
	 * Just a dummy for the time being.
60
	 */
61 101
	public function __construct()
62
	{
63
	}
64
65
	/**
66
	 * Allows to set the object that instantiated the \ElkArte\EventManager.
67
	 *
68
	 * - Necessary in order to be able to provide the dependencies later on, allows
69
	 * one to access the calling class properties in the registered event
70
	 *
71
	 * @param object $source The controller that instantiated the \ElkArte\EventManager
72
	 */
73
	public function setSource($source)
74
	{
75
		$this->_source = $source;
76
		$this->_modules[0] = ['i' => $source, 'c' => ''];
77
	}
78
79
	/**
80
	 * This is the function use to... trigger an event.
81
	 *
82
	 * - Called from many areas in the code where events can be raised
83
	 * $this->_events->trigger('area', args)
84
	 *
85
	 * @param string $position The "identifier" of the event, such as prepare_post
86 16
	 * @param mixed[] $args The arguments passed to the methods registered
87
	 *
88
	 * @return bool
89 16
	 */
90
	public function trigger($partial_position, $args = array())
91 16
	{
92
		if ($this->depth == 0)
93
		{
94
			$position = $partial_position;
95
		}
96
		else
97
		{
98
			$position = $this->_modules[$this->depth]['c'] . '.' . $partial_position;
99
		}
100
101
		// Nothing registered against this event, just return
102
		if (!array_key_exists($position, $this->_registered_events) || !$this->_registered_events[$position]->hasEvents())
103
		{
104
			return false;
105
		}
106
107
		// For all areas that that registered against this event, let them know its been triggered
108
		foreach ($this->_registered_events[$position]->getEvents() as $event)
109
		{
110
			$class = $event[1];
111
			$class_name = $class[0];
112
			$method_name = $class[1];
113
			$deps = $event[2] ?? array();
114
			$dependencies = null;
115
116
			if (!class_exists($class_name))
117
			{
118
				return false;
119
			}
120
121
			// Any dependency you want? In any order you want!
122
			if (!empty($deps))
123
			{
124
				foreach ($deps as $dep)
125
				{
126
					if (array_key_exists($dep, $args))
127
					{
128
						$dependencies[$dep] = &$args[$dep];
129
					}
130
					else
131
					{
132
						// Access to the calling classes properties
133
						$this->_modules[$this->depth]['i']->provideDependencies($dep, $dependencies);
134
					}
135
				}
136
			}
137
			else
138
			{
139
				foreach ($args as $key => $val)
140
				{
141
					$dependencies[$key] = &$args[$key];
142
				}
143
			}
144
145
			$instance = $this->_getInstance($class_name);
146
147
			// Do what we know we should do... if we find it.
148
			if (is_callable(array($instance, $method_name)))
149
			{
150
				$instance->setEventManager($this);
151
				$this->pushModule($instance, $class_name);
152
				// Don't send $dependencies if there are none / the method can't use them
153
				if (empty($dependencies))
154
				{
155
					call_user_func(array($instance, $method_name));
156
				}
157
				else
158
				{
159
					$this->_checkParameters($class_name, $method_name, $dependencies);
160
					call_user_func_array(array($instance, $method_name), $dependencies);
161
				}
162
				$this->unsetModule($this->depth);
163
			}
164
		}
165
	}
166
167
	protected function pushModule($instance, $class_name)
168
	{
169
		$this->depth += 1;
170
		$this->_modules[$this->depth] = ['i' => $instance, 'c' => $class_name];
171
	}
172
173
	protected function unsetModule($depth)
174
	{
175
		unset($this->_modules[$this->depth]);
176
		$this->depth = $depth - 1;
177
	}
178
179
	/**
180
	 * Retrieves or creates the instance of an object.
181
	 *
182
	 * What it does:
183
	 *
184
	 * - Objects are stored in order to be shared between different triggers
185
	 * in the same \ElkArte\EventManager.
186
	 * - If the object doesn't exist yet, it is created
187
	 *
188
	 * @param string $class_name The name of the class.
189
	 * @return object
190
	 */
191
	protected function _getInstance($class_name)
192
	{
193
		if (isset($this->_instances[$class_name]))
194
		{
195
			return $this->_instances[$class_name];
196
		}
197
		else
198
		{
199
			$instance = new $class_name(HttpReq::instance(), User::$info);
200
			$this->_setInstance($class_name, $instance);
201
202
			return $instance;
203
		}
204
	}
205
206
	/**
207
	 * Stores the instance of a class created by _getInstance.
208
	 *
209
	 * @param string $class_name The name of the class.
210
	 * @param object $instance The object.
211
	 */
212
	protected function _setInstance($class_name, $instance)
213
	{
214
		if (!isset($this->_instances[$class_name]))
215
		{
216
			$this->_instances[$class_name] = $instance;
217
		}
218
	}
219
220
	/**
221
	 * Loads addons and modules based on a pattern.
222
	 *
223
	 * - The pattern defines the names of the classes that will be registered
224
	 * to this \ElkArte\EventManager.
225
	 *
226
	 * @param string[] $classes A set of class names that should be attached
227
	 */
228
	public function registerClasses($classes)
229
	{
230
		$this->_register_events($classes);
231
	}
232
233
	/**
234
	 * Takes care of registering the classes/methods to the different positions
235
	 * of the \ElkArte\EventManager.
236
	 *
237
	 * What it does:
238
	 *
239
	 * - Each class must have a static Method ::hooks
240
	 * - Method ::hooks must return an array defining where and how the class
241
	 * will interact with the object that started the \ElkArte\EventManager.
242
	 *
243
	 * @param string[] $classes A list of class names.
244
	 */
245
	protected function _register_events($classes)
246
	{
247
		foreach ($classes as $class)
248
		{
249
			// Load the events for this area/class combination
250
			$events = $class::hooks($this);
251
			if (!is_array($events))
252
			{
253
				continue;
254
			}
255
256
			foreach ($events as $event)
257
			{
258
				// Check if a priority (ordering) was specified
259
				$priority = $event[1][2] ?? 0;
260
				$position = $event[0];
261
262
				// Register the "action" to take when the event is triggered
263
				$this->register($position, $event, $priority);
264
			}
265
		}
266
	}
267
268
	/**
269
	 * Registers an event at a certain position with a defined priority.
270
	 *
271
	 * @param string $position The position at which the event will be triggered
272
	 * @param mixed[] $event An array describing the event we want to trigger:
273
	 *   0 => string - the position at which the event will be triggered
274
	 *   1 => string[] - the class and method we want to call:
275
	 *      array(
276
	 *        0 => string - name of the class to instantiate
277
	 *        1 => string - name of the method to call
278
	 *      )
279
	 *   2 => null|string[] - an array of dependencies in the form of strings representing the
280
	 *        name of the variables the method requires.
281
	 *        The variables can be from:
282
	 *          - the default list of variables passed to the trigger
283
	 *          - properties (private, protected, or public) of the object that instantiate the \ElkArte\EventManager
284
	 *            (i.e. the controller)
285
	 *          - globals
286
	 * @param int $priority Defines the order the method is called.
287
	 */
288
	public function register($position, $event, $priority = 0)
289
	{
290
		if (!isset($this->_registered_events[$position]))
291
		{
292
			$this->_registered_events[$position] = new Event(new Priority());
293
		}
294
295
		$this->_registered_events[$position]->add($event, $priority);
296
	}
297
298
	/**
299
	 * Gets the names of all the classes already loaded.
300
	 *
301
	 * @return string[]
302
	 */
303
	protected function _declared_classes()
304
	{
305
		if ($this->_declared_classes === null)
306
		{
307
			$this->_declared_classes = get_declared_classes();
308
		}
309
310
		return $this->_declared_classes;
311
	}
312
313
	/**
314
	 * Reflects a specific class method to see what parameters are needed
315
	 *
316
	 * Currently only checks on number required, can be expanded to make use of
317
	 * $params = $r->getParameters() and then $param-> getName isOptional etc
318
	 * to ensure required named are being passed.
319
	 *
320
	 * @param string $class_name
321
	 * @param string $method_name
322
	 * @param array $dependencies the dependencies the event registered
323
	 */
324
	protected function _checkParameters($class_name, $method_name, &$dependencies)
325
	{
326
		// Lets check on the actual methods parameters
327
		try
328
		{
329
			$r = new \ReflectionMethod($class_name, $method_name);
330
			$number_params = $r->getNumberOfParameters();
331
			unset($r);
332
		}
333
		catch (\Exception $e)
334
		{
335
			$number_params = 0;
336
		}
337
338
		// Php8 will not like passing parameters to a method that takes none
339
		if ($number_params == 0 && !empty($dependencies))
340
		{
341
			$dependencies = array();
342
		}
343
	}
344
}
345