Completed
Push — master ( 3bf75b...4f1df0 )
by Peter
04:51
created

Event::getPartials()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 28
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 5.0144

Importance

Changes 1
Bugs 1 Features 0
Metric Value
c 1
b 1
f 0
dl 0
loc 28
ccs 11
cts 12
cp 0.9167
rs 8.439
cc 5
eloc 13
nc 5
nop 1
crap 5.0144
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 https://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\Hi5Edit\Models\Href;
19
use Maslosoft\Mangan\Interfaces\EntityManagerInterface;
20
use Maslosoft\Mangan\Interfaces\Events\EventInterface;
21
use Maslosoft\Mangan\Meta\ManganMeta;
22
use ReflectionClass;
23
use UnexpectedValueException;
24
25
/**
26
 * This is based on Yii 2 Events
27
 */
28
/**
29
 * @link http://www.yiiframework.com/
30
 * @copyright Copyright (c) 2008 Yii Software LLC
31
 * @license http://www.yiiframework.com/license/
32
 */
33
34
/**
35
 * Event is the base class for all event classes.
36
 *
37
 * It encapsulates the parameters associated with an event.
38
 * The [[sender]] property describes who raises the event.
39
 * And the [[handled]] property indicates if the event is handled.
40
 * If an event handler sets [[handled]] to be true, the rest of the
41
 * uninvoked handlers will no longer be called to handle the event.
42
 *
43
 * Additionally, when attaching an event handler, extra data may be passed
44
 * and be available via the [[data]] property when the event handler is invoked.
45
 *
46
 * @author Qiang Xue <[email protected]>
47
 * @since 2.0
48
 */
49
class Event implements EventInterface
50
{
51
52
	/**
53
	 * @var string the event name. This property is set by [[Component::trigger()]] and [[trigger()]].
54
	 * Event handlers may use this property to check what event it is handling.
55
	 */
56
	public $name;
57
58
	/**
59
	 * @var object the sender of this event. If not set, this property will be
60
	 * set as the object whose "trigger()" method is called.
61
	 * This property may also be a `null` when this event is a
62
	 * class-level event which is triggered in a static context.
63
	 */
64
	public $sender;
65
66
	/**
67
	 * @var boolean whether the event is handled. Defaults to false.
68
	 * When a handler sets this to be true, the event processing will stop and
69
	 * ignore the rest of the uninvoked event handlers.
70
	 */
71
	public $handled = false;
72
73
	/**
74
	 * @var mixed the data that is passed to [[Component::on()]] when attaching an event handler.
75
	 * Note that this varies according to which event handler is currently executing.
76
	 */
77
	public $data;
78
79
	/**
80
	 * Array of events. This contains all event registered for application.
81
	 * @var EventInterface[]
82
	 */
83
	private static $events = [];
84
85
	/**
86
	 * Array containing partial classes for class. This holds traits, interfaces,
87
	 * parent classes names for class.
88
	 *
89
	 * Structure is like below:
90
	 * ```
91
	 * $partials = [
92
	 * 		'\Vendor\Package\ClassOne' => [
93
	 * 			'\Vendor\Package\TraitOne',
94
	 * 			'\Vendor\Package\TraitTwo',
95
	 * 			'\Vendor\Package\InterfaceX',
96
	 * 			'\Vendor\Package\AbstractSix'
97
	 * 		],
98
	 * 		'\Vendor\Package\ClassTwo' => [
99
	 * 			'\Vendor\Package\TraitBig',
100
	 * 			'\Vendor\Package\TraitSmall',
101
	 * 			'\Vendor\Package\InterfaceY',
102
	 * 			'\Vendor\Package\AbstractGuy'
103
	 * 		],
104
	 * ];
105
	 * ```
106
	 *
107
	 * @var string[]
108
	 */
109
	private static $partials = [];
110
111
	/**
112
	 * Propagated properties cache. It contains only properties which should
113
	 * propagate, others are skipped, thus value is always true.
114
	 *
115
	 * Structure is like follow:
116
	 * ```
117
	 * $propagated = [
118
	 * 		'\Vendor\Package\ClassOne' => [
119
	 * 			'fieldOne' => true,
120
	 * 			'fieldTwo' => true
121
	 * 		],
122
	 * 		'\Vendor\Package\ClassTwo' => [
123
	 * 			'otherFieldOne' => true,
124
	 * 			'otherFieldTwo' => true
125
	 * 		],
126
	 *
127
	 * ];
128
	 * ```
129
	 * @var bool[]
130
	 */
131
	private static $propagated = [];
132
133
	/**
134
	 * Attaches an event handler to a class-level event.
135
	 *
136
	 * When a class-level event is triggered, event handlers attached
137
	 * to that class and all parent classes will be invoked.
138
	 *
139
	 * For example, the following code attaches an event handler to document's
140
	 * `afterInsert` event:
141
	 *
142
	 * ~~~
143
	 * Event::on($model, EntityManager::EventAfterInsert, function ($event) {
144
	 * 		var_dump(get_class($event->sender) . ' is inserted.');
145
	 * });
146
	 * ~~~
147
	 *
148
	 * The handler will be invoked for every successful document insertion.
149
	 *
150
	 * **NOTE:** Each call will attach new event handler. When placing event
151
	 * initialization in class constructors etc. ensure that it is evaluated once,
152
	 * or it might trigger same event handler multiple times.
153
	 *
154
	 * @param AnnotatedInterface|object|string $model the object specifying the class-level event.
155
	 * @param string $name the event name.
156
	 * @param callable $handler the event handler.
157
	 * @param mixed $data the data to be passed to the event handler when the event is triggered.
158
	 * When the event handler is invoked, this data can be accessed via [[Event::data]].
159
	 * @param boolean $append whether to append new event handler to the end of the existing
160
	 * handler list. If false, the new handler will be inserted at the beginning of the existing
161
	 * handler list.
162
	 * @see off()
163
	 */
164 26
	public static function on($model, $name, $handler, $data = null, $append = true)
165
	{
166 26
		$class = self::getName($model);
167 26
		if ($append || empty(self::$events[$name][$class]))
168
		{
169 26
			self::$events[$name][$class][] = [$handler, $data];
170
		}
171
		else
172
		{
173
			array_unshift(self::$events[$name][$class], [$handler, $data]);
174
		}
175 26
	}
176
177
	/**
178
	 * Detaches an event handler from a class-level event.
179
	 *
180
	 * This method is the opposite of [[on()]].
181
	 *
182
	 * @param AnnotatedInterface|object|string $model the object specifying the class-level event.
183
	 * @param string $name the event name.
184
	 * @param callable $handler the event handler to be removed.
185
	 * If it is null, all handlers attached to the named event will be removed.
186
	 * @return boolean whether a handler is found and detached.
187
	 * @see on()
188
	 */
189 10
	public static function off($model, $name, $handler = null)
190
	{
191 10
		$class = self::getName($model);
192 10
		if (empty(self::$events[$name][$class]))
193
		{
194
			return false;
195
		}
196 10
		if ($handler === null)
197
		{
198
			unset(self::$events[$name][$class]);
199
			return true;
200
		}
201
		else
202
		{
203 10
			$removed = false;
204 10
			foreach (self::$events[$name][$class] as $i => $event)
205
			{
206 10
				if ($event[0] === $handler)
207
				{
208 10
					unset(self::$events[$name][$class][$i]);
209 10
					$removed = true;
210
				}
211
			}
212 10
			if ($removed)
213
			{
214 10
				self::$events[$name][$class] = array_values(self::$events[$name][$class]);
215
			}
216 10
			return $removed;
217
		}
218
	}
219
220
	/**
221
	 * Triggers a class-level event.
222
	 * This method will cause invocation of event handlers that are attached to the named event
223
	 * for the specified class and all its parent classes.
224
	 * @param AnnotatedInterface $model the object specifying the class-level event.
225
	 * @param string $name the event name.
226
	 * @param ModelEvent $event the event parameter. If not set, a default `ModelEvent` object will be created.
227
	 * @return bool True if event was triggered.
228
	 */
229 121
	public static function trigger(AnnotatedInterface $model, $name, &$event = null)
230
	{
231 121
		$wasTriggered = false;
232 121
		if (empty(self::$events[$name]))
233
		{
234 118
			return self::propagate($model, $name, $event);
235
		}
236 107
		if ($event === null)
237
		{
238 103
			$event = new ModelEvent();
239
		}
240 107
		$event->handled = false;
241 107
		$event->name = $name;
242
243 107
		if ($event->sender === null)
244
		{
245 107
			$event->sender = $model;
246
		}
247 107
		$event->currentTarget = $model;
248 107
		$className = self::getName($model);
249
250
		// Partials holds parts of class, this include interfaces and traits
251 107
		$allPartials = self::getPartials($className);
252
253
		// Filter out empty partials
254 107
		$partials = [];
255 107
		foreach ($allPartials as $className)
256
		{
257 107
			if (empty(self::$events[$name][$className]))
258
			{
259 107
				continue;
260
			}
261 107
			$partials[] = $className;
262
		}
263
264
		// Trigger all partial events if applicable
265 107
		foreach ($partials as $className)
266
		{
267 107
			foreach (self::$events[$name][$className] as $handler)
268
			{
269
				// Assign source for easier debugging or other uses
270 107
				$event->source = $className;
271
272 107
				$event->data = $handler[1];
273 107
				call_user_func($handler[0], $event);
274 107
				$wasTriggered = true;
275
276
277
				// Event was handled, return true
278 107
				if ($event->handled)
279
				{
280 107
					return true;
281
				}
282
			}
283
		}
284
285
		// Propagate events to sub objects
286 107
		return self::propagate($model, $name, $event) || $wasTriggered;
287
	}
288
289
	/**
290
	 * Triggers a class-level event and checks if it's valid.
291
	 * If don't have event handler returns true. If event handler is set, return true if `Event::isValid`.
292
	 * This method will cause invocation of event handlers that are attached to the named event
293
	 * for the specified class and all its parent classes.
294
	 * @param AnnotatedInterface $model the object specifying the class-level event.
295
	 * @param string $name the event name.
296
	 * @param ModelEvent $event the event parameter. If not set, a default [[ModelEvent]] object will be created.
297
	 * @return bool True if event was triggered and is valid.
298
	 */
299 120
	public static function valid(AnnotatedInterface $model, $name, $event = null)
300
	{
301 120
		if (Event::trigger($model, $name, $event))
302
		{
303 103
			return $event->isValid;
304
		}
305
		else
306
		{
307 61
			return true;
308
		}
309
	}
310
311
	/**
312
	 * Triggers a class-level event and checks if it's handled.
313
	 * If don't have event handler returns true. If event handler is set, return true if `Event::handled`.
314
	 * This method will cause invocation of event handlers that are attached to the named event
315
	 * for the specified class and all its parent classes.
316
	 * @param AnnotatedInterface $model the object specifying the class-level event.
317
	 * @param string $name the event name.
318
	 * @param ModelEvent $event the event parameter. If not set, a default [[Event]] object will be created.
319
	 * @return bool|null True if handled, false otherways, null if not triggered
320
	 */
321 1
	public static function handled(AnnotatedInterface $model, $name, $event = null)
322
	{
323 1
		if (Event::trigger($model, $name, $event))
324
		{
325 1
			return $event->handled;
326
		}
327
		return true;
328
	}
329
330
	/**
331
	 * Check if model has event handler.
332
	 * **IMPORTANT**: It does not check for propagated events
333
	 *
334
	 * @param AnnotatedInterface|string $class the object specifying the class-level event
335
	 * @param string $name the event name.
336
	 * @return bool True if has handler
337
	 */
338 84
	public static function hasHandler($class, $name)
339
	{
340
		// Partials holds parts of class, this include interfaces and traits
341 84
		$partials = self::getPartials(self::getName($class));
342 84
		foreach ($partials as $className)
343
		{
344 84
			if (!empty(self::$events[$name][$className]))
345
			{
346 84
				return true;
347
			}
348
		}
349 5
		return false;
350
	}
351
352
	/**
353
	 * Get class name
354
	 * @param AnnotatedInterface|object|string $class
355
	 * @return string
356
	 */
357 107
	private static function getName($class)
358
	{
359 107
		if (is_object($class))
360
		{
361 107
			$class = get_class($class);
362
		}
363
		else
364
		{
365 2
			if (!ClassChecker::exists($class))
366
			{
367
				throw new UnexpectedValueException(sprintf("Class `%s` not found", $class));
368
			}
369
		}
370 107
		return ltrim($class, '\\');
371
	}
372
373
	/**
374
	 * Propagate event
375
	 * @param AnnotatedInterface $model
376
	 * @param string $name
377
	 * @param ModelEvent|null $event
378
	 */
379 121
	private static function propagate(AnnotatedInterface $model, $name, &$event = null)
380
	{
381 121
		$wasTriggered = false;
382 121
		if ($event && !$event->propagate())
383
		{
384 1
			return false;
385
		}
386
387 120
		foreach (self::getPropagatedProperties($model) as $property => $propagate)
388
		{
389 33
			if (empty($model->$property))
390
			{
391
				// Property is empty, skip
392 11
				continue;
393
			}
394
			// Trigger for arrays
395 32
			if (is_array($model->$property))
396
			{
397 12
				foreach ($model->$property as $object)
398
				{
399 12
					$wasTriggered = self::trigger($object, $name, $event) || $wasTriggered;
400
				}
401 12
				continue;
402
			}
403 23
			$wasTriggered = self::trigger($model->$property, $name, $event) || $wasTriggered;
404
		}
405 120
		return $wasTriggered;
406
	}
407
408
	/**
409
	 * Get properties which should be propagated.
410
	 * NOTE: This is cached, as it might be called numerous times
411
	 * @param object $model
412
	 * @return bool[]
413
	 */
414 120
	private static function getPropagatedProperties($model)
415
	{
416 120
		$key = get_class($model);
417 120
		if (empty(self::$propagated[$key]))
418
		{
419 120
			$propagated = [];
420 120
			foreach (ManganMeta::create($model)->properties('propagateEvents') as $name => $isPropagated)
421
			{
422 120
				if (!$isPropagated)
423
				{
424 120
					continue;
425
				}
426 21
				$propagated[$name] = true;
427
			}
428 120
			self::$propagated[$key] = $propagated;
429
		}
430 120
		return self::$propagated[$key];
431
	}
432
433
	/**
434
	 * Get class/interface/trait names from which class is composed.
435
	 *
436
	 * @param string $className
437
	 * @return array
438
	 */
439 107
	public static function getPartials($className)
440
	{
441 107
		if (!empty(self::$partials[$className]))
442
		{
443
			return self::$partials[$className];
444
		}
445 107
		$partials = [];
446
		// Iterate over traits
447 107
		foreach ((new ReflectionClass($className))->getTraitNames() as $trait)
448
		{
449 20
			$partials[] = $trait;
450
		}
451
452
		// Iterate over interfaces to get partials
453 107
		foreach ((new ReflectionClass($className))->getInterfaceNames() as $interface)
454
		{
455 107
			$partials[] = $interface;
456
		}
457
458
		// Iterate over parent classes
459
		do
460
		{
461 107
			$partials[] = $className;
462
		}
463 107
		while (($className = get_parent_class($className)) !== false);
464 107
		self::$partials[$className] = $partials;
465 107
		return $partials;
466
	}
467
468
	protected static function destroyEvents()
469
	{
470
		self::$events = [];
471
	}
472
473
}
474