Completed
Push — master ( 91618d...0ad795 )
by Peter
47:17 queued 22:32
created

Event   B

Complexity

Total Complexity 39

Size/Duplication

Total Lines 324
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 3

Test Coverage

Coverage 94.95%

Importance

Changes 6
Bugs 2 Features 0
Metric Value
wmc 39
c 6
b 2
f 0
lcom 1
cbo 3
dl 0
loc 324
ccs 94
cts 99
cp 0.9495
rs 8.2857

8 Methods

Rating   Name   Duplication   Size   Complexity  
A on() 0 12 3
B off() 0 30 6
C trigger() 0 66 12
A valid() 0 11 2
A handled() 0 8 2
A hasHandler() 0 14 3
A _getName() 0 15 3
C _propagate() 0 32 8
1
<?php
2
3
/**
4
 * This software package is licensed under AGPL or Commercial license.
5
 *
6
 * @package maslosoft/mangan
7
 * @licence AGPL or Commercial
8
 * @copyright Copyright (c) Piotr Masełkowski <[email protected]>
9
 * @copyright Copyright (c) Maslosoft
10
 * @copyright Copyright (c) Others as mentioned in code
11
 * @link http://maslosoft.com/mangan/
12
 */
13
14
namespace Maslosoft\Mangan\Events;
15
16
use Maslosoft\Addendum\Interfaces\AnnotatedInterface;
17
use Maslosoft\Addendum\Utilities\ClassChecker;
18
use Maslosoft\Mangan\Interfaces\Events\EventInterface;
19
use Maslosoft\Mangan\Meta\ManganMeta;
20
use ReflectionClass;
21
use UnexpectedValueException;
22
23
/**
24
 * This is based on Yii 2 Events
25
 */
26
/**
27
 * @link http://www.yiiframework.com/
28
 * @copyright Copyright (c) 2008 Yii Software LLC
29
 * @license http://www.yiiframework.com/license/
30
 */
31
32
/**
33
 * Event is the base class for all event classes.
34
 *
35
 * It encapsulates the parameters associated with an event.
36
 * The [[sender]] property describes who raises the event.
37
 * And the [[handled]] property indicates if the event is handled.
38
 * If an event handler sets [[handled]] to be true, the rest of the
39
 * uninvoked handlers will no longer be called to handle the event.
40
 *
41
 * Additionally, when attaching an event handler, extra data may be passed
42
 * and be available via the [[data]] property when the event handler is invoked.
43
 *
44
 * @author Qiang Xue <[email protected]>
45
 * @since 2.0
46
 */
47
class Event implements EventInterface
48
{
49
50
	/**
51
	 * @var string the event name. This property is set by [[Component::trigger()]] and [[trigger()]].
52
	 * Event handlers may use this property to check what event it is handling.
53
	 */
54
	public $name;
55
56
	/**
57
	 * @var object the sender of this event. If not set, this property will be
58
	 * set as the object whose "trigger()" method is called.
59
	 * This property may also be a `null` when this event is a
60
	 * class-level event which is triggered in a static context.
61
	 */
62
	public $sender;
63
64
	/**
65
	 * @var boolean whether the event is handled. Defaults to false.
66
	 * When a handler sets this to be true, the event processing will stop and
67
	 * ignore the rest of the uninvoked event handlers.
68
	 */
69
	public $handled = false;
70
71
	/**
72
	 * @var mixed the data that is passed to [[Component::on()]] when attaching an event handler.
73
	 * Note that this varies according to which event handler is currently executing.
74
	 */
75
	public $data;
76
77
	/**
78
	 * Array of events
79
	 * @var EventInterface[]
80
	 */
81
	private static $_events = [];
82
83
	/**
84
	 * Attaches an event handler to a class-level event.
85
	 *
86
	 * When a class-level event is triggered, event handlers attached
87
	 * to that class and all parent classes will be invoked.
88
	 *
89
	 * For example, the following code attaches an event handler to document's
90
	 * `afterInsert` event:
91
	 *
92
	 * ~~~
93
	 * Event::on($model, EntityManager::EventAfterInsert, function ($event) {
94
	 * 		var_dump(get_class($event->sender) . ' is inserted.');
95
	 * });
96
	 * ~~~
97
	 *
98
	 * The handler will be invoked for every successful document insertion.
99
	 *
100
	 * **NOTE:** Each call will attach new event handler. When placing event
101
	 * initialization in class constructors etc. ensure that it is evaluated once,
102
	 * or it might trigger same event handler multiple times.
103
	 *
104
	 * @param AnnotatedInterface|object|string $model the object specifying the class-level event.
105
	 * @param string $name the event name.
106
	 * @param callable $handler the event handler.
107 20
	 * @param mixed $data the data to be passed to the event handler when the event is triggered.
108
	 * When the event handler is invoked, this data can be accessed via [[Event::data]].
109 20
	 * @param boolean $append whether to append new event handler to the end of the existing
110 20
	 * handler list. If false, the new handler will be inserted at the beginning of the existing
111 20
	 * handler list.
112 20
	 * @see off()
113 20
	 */
114
	public static function on($model, $name, $handler, $data = null, $append = true)
115
	{
116
		$class = self::_getName($model);
117
		if ($append || empty(self::$_events[$name][$class]))
118 20
		{
119
			self::$_events[$name][$class][] = [$handler, $data];
120
		}
121
		else
122
		{
123
			array_unshift(self::$_events[$name][$class], [$handler, $data]);
124
		}
125
	}
126
127
	/**
128
	 * Detaches an event handler from a class-level event.
129
	 *
130
	 * This method is the opposite of [[on()]].
131
	 *
132 10
	 * @param AnnotatedInterface|object|string $model the object specifying the class-level event.
133
	 * @param string $name the event name.
134 10
	 * @param callable $handler the event handler to be removed.
135 10
	 * If it is null, all handlers attached to the named event will be removed.
136 10
	 * @return boolean whether a handler is found and detached.
137
	 * @see on()
138
	 */
139 10
	public static function off($model, $name, $handler = null)
140 10
	{
141
		$class = self::_getName($model);
142
		if (empty(self::$_events[$name][$class]))
143
		{
144
			return false;
145
		}
146 10
		if ($handler === null)
147 10
		{
148
			unset(self::$_events[$name][$class]);
149 10
			return true;
150 10
		}
151 10
		else
152 10
		{
153 10
			$removed = false;
154 10
			foreach (self::$_events[$name][$class] as $i => $event)
155
			{
156 10
				if ($event[0] === $handler)
157 10
				{
158 10
					unset(self::$_events[$name][$class][$i]);
159 10
					$removed = true;
160
				}
161
			}
162
			if ($removed)
163
			{
164
				self::$_events[$name][$class] = array_values(self::$_events[$name][$class]);
165
			}
166
			return $removed;
167
		}
168
	}
169
170
	/**
171
	 * Triggers a class-level event.
172 107
	 * This method will cause invocation of event handlers that are attached to the named event
173
	 * for the specified class and all its parent classes.
174 107
	 * @param AnnotatedInterface $model the object specifying the class-level event.
175 107
	 * @param string $name the event name.
176 107
	 * @param ModelEvent $event the event parameter. If not set, a default [[Event]] object will be created.
177 107
	 * @return bool True if event was triggered.
178
	 */
179 17
	public static function trigger(AnnotatedInterface $model, $name, &$event = null)
180 17
	{
181 13
		$wasTriggered = false;
182 13
		if (empty(self::$_events[$name]))
183 17
		{
184 17
			return self::_propagate($model, $name, $event);
185
		}
186 17
		if ($event === null)
187 17
		{
188 13
			$event = new ModelEvent();
189 13
		}
190 17
		$event->handled = false;
191
		$event->name = $name;
192
193 17
		if ($event->sender === null)
194 17
		{
195 16
			$event->sender = $model;
196
		}
197 17
		$className = self::_getName($model);
198
199 17
		// Partials holds parts of class, this include interfaces and traits
200 17
		$partials = [];
201 17
		// Iterate over traits
202 17
		foreach ((new ReflectionClass($className))->getTraitNames() as $trait)
203 17
		{
204 1
			$partials[] = $trait;
205
		}
206 16
207
		// Iterate over interfaces to get partials
208 16
		foreach ((new ReflectionClass($className))->getInterfaceNames() as $interface)
209 16
		{
210
			$partials[] = $interface;
211
		}
212
213
		// Iterate over parent classes
214
		do
215
		{
216
			$partials[] = $className;
217
		}
218
		while (($className = get_parent_class($className)) !== false);
219
220
		// Trigger all partial events if applicable
221
		foreach ($partials as $className)
222 106
		{
223
			if (empty(self::$_events[$name][$className]))
224 106
			{
225 106
				continue;
226 8
			}
227
228
			foreach (self::$_events[$name][$className] as $handler)
229
			{
230 106
				$event->data = $handler[1];
231
				call_user_func($handler[0], $event);
232
				$wasTriggered = true;
233
234
				// Event was handled, return true
235
				if ($event->handled)
236
				{
237
					return true;
238
				}
239
			}
240
		}
241
242
		// Propagate events to sub objects
243
		return self::_propagate($model, $name, $event) || $wasTriggered;
244 1
	}
245
246 1
	/**
247 1
	 * Triggers a class-level event and checks if it's valid.
248 1
	 * If don't have event handler returns true. If event handler is set, return true if `Event::isValid`.
249
	 * This method will cause invocation of event handlers that are attached to the named event
250
	 * for the specified class and all its parent classes.
251
	 * @param AnnotatedInterface $model the object specifying the class-level event.
252
	 * @param string $name the event name.
253
	 * @param ModelEvent $event the event parameter. If not set, a default [[ModelEvent]] object will be created.
254
	 * @return bool True if event was triggered and is valid.
255
	 */
256
	public static function valid(AnnotatedInterface $model, $name, $event = null)
257
	{
258
		if (Event::trigger($model, $name, $event))
259
		{
260 56
			return $event->isValid;
261
		}
262 56
		else
263
		{
264
			return true;
265
		}
266 56
	}
267 56
268 1
	/**
269
	 * Triggers a class-level event and checks if it's handled.
270
	 * If don't have event handler returns true. If event handler is set, return true if `Event::handled`.
271 56
	 * This method will cause invocation of event handlers that are attached to the named event
272 56
	 * for the specified class and all its parent classes.
273
	 * @param AnnotatedInterface $model the object specifying the class-level event.
274
	 * @param string $name the event name.
275
	 * @param ModelEvent $event the event parameter. If not set, a default [[Event]] object will be created.
276
	 * @return bool|null True if handled, false otherway, null if not triggered
277
	 */
278
	public static function handled(AnnotatedInterface $model, $name, $event = null)
279
	{
280 69
		if (Event::trigger($model, $name, $event))
281
		{
282 69
			return $event->handled;
283
		}
284
		return true;
285
	}
286
287
	/**
288
	 * Check if model has event handler.
289
	 * **IMPORTANT**: It does not check for propagated events
290
	 * 
291 107
	 * @param AnnotatedInterface|string $class the object specifying the class-level event
292
	 * @param string $name the event name.
293 107
	 * @return bool True if has handler
294 107
	 */
295 107
	public static function hasHandler($class, $name)
296 1
	{
297
		$className = self::_getName($class);
298 107
299 107
		do
300
		{
301 107
			if (!empty(self::$_events[$name][$className]))
302 107
			{
303 107
				return true;
304
			}
305 32
		}
306 32
		while (($className = get_parent_class($className)) !== false);
307 18
		return false;
308
	}
309
310 26
	/**
311 26
	 * Get class name
312 12
	 * @param AnnotatedInterface|object|string $class
313
	 * @return string
314 12
	 */
315 12
	private static function _getName($class)
316 12
	{
317
		if (is_object($class))
318
		{
319 17
			$class = get_class($class);
320 107
		}
321 107
		else
322
		{
323
			if (!ClassChecker::exists($class))
324
			{
325
				throw new UnexpectedValueException(sprintf("Class `%s` not found", $class));
326
			}
327
		}
328
		return ltrim($class, '\\');
329
	}
330
331
	/**
332
	 * Propagate event
333
	 * @param AnnotatedInterface $model
334
	 * @param string $name
335
	 * @param ModelEvent|null $event
336
	 */
337
	private static function _propagate(AnnotatedInterface $model, $name, &$event = null)
338
	{
339
		$wasTriggered = false;
340
		if ($event && !$event->propagate())
341
		{
342
			return false;
343
		}
344
		$meta = ManganMeta::create($model);
345
		foreach ($meta->properties('propagateEvents') as $property => $propagate)
346
		{
347
			if (!$propagate)
348
			{
349
				continue;
350
			}
351
			if (!$model->$property)
352
			{
353
				continue;
354
			}
355
			// Trigger for arrays
356
			if (is_array($model->$property))
357
			{
358
				foreach ($model->$property as $object)
359
				{
360
					$wasTriggered = self::trigger($object, $name, $event);
361
				}
362
				continue;
363
			}
364
			// Trigger for single value
365
			$wasTriggered = self::trigger($model->$property, $name, $event);
366
		}
367
		return $wasTriggered;
368
	}
369
370
}
371