Completed
Push — master ( 844af5...918599 )
by Peter
13:31
created

Event::getPartials()   C

Complexity

Conditions 8
Paths 18

Size

Total Lines 48
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 8.1867

Importance

Changes 1
Bugs 0 Features 0
Metric Value
dl 0
loc 48
c 1
b 0
f 0
ccs 18
cts 21
cp 0.8571
rs 5.9322
cc 8
eloc 22
nc 18
nop 1
crap 8.1867
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 23
	public static function on($model, $name, $handler, $data = null, $append = true)
165
	{
166 23
		$class = self::getName($model);
167 23
		if ($append || empty(self::$events[$name][$class]))
168
		{
169 23
			self::$events[$name][$class][] = [$handler, $data];
170
		}
171
		else
172
		{
173
			array_unshift(self::$events[$name][$class], [$handler, $data]);
174
		}
175 23
	}
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|string $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 144
	public static function trigger($model, $name, &$event = null)
230
	{
231 144
		$wasTriggered = false;
232 144
		if (empty(self::$events[$name]))
233
		{
234 141
			return self::propagate($model, $name, $event);
235
		}
236 121
		if ($event === null)
237
		{
238 117
			$event = new ModelEvent();
239
		}
240 121
		$event->handled = false;
241 121
		$event->name = $name;
242
243 121
		if ($event->sender === null && is_object($model))
244
		{
245 121
			$event->sender = $model;
246
		}
247 121
		if(is_object($model))
248
		{
249 121
			$event->currentTarget = $model;
250
		}
251 121
		$className = self::getName($model);
252
253
		// Partials holds parts of class, this include interfaces and traits
254 121
		$allPartials = self::getPartials($className);
255
256
		// Filter out empty partials
257 121
		$partials = [];
258 121
		foreach ($allPartials as $className)
259
		{
260 121
			if (empty(self::$events[$name][$className]))
261
			{
262 121
				continue;
263
			}
264 121
			$partials[] = $className;
265
		}
266
267
		// Trigger all partial events if applicable
268 121
		foreach ($partials as $className)
269
		{
270 121
			foreach (self::$events[$name][$className] as $handler)
271
			{
272
				// Assign source for easier debugging or other uses
273 121
				$event->source = $className;
274
275 121
				$event->data = $handler[1];
276 121
				call_user_func($handler[0], $event);
277 121
				$wasTriggered = true;
278
279
280
				// Event was handled, return true
281 121
				if ($event->handled)
282
				{
283 121
					return true;
284
				}
285
			}
286
		}
287
288
		// Propagate events to sub objects
289 121
		return self::propagate($model, $name, $event) || $wasTriggered;
290
	}
291
292
	/**
293
	 * Triggers a class-level event and checks if it's valid.
294
	 * If don't have event handler returns true. If event handler is set, return true if `Event::isValid`.
295
	 * This method will cause invocation of event handlers that are attached to the named event
296
	 * for the specified class and all its parent classes.
297
	 * @param AnnotatedInterface|string $model the object specifying the class-level event.
298
	 * @param string $name the event name.
299
	 * @param ModelEvent $event the event parameter. If not set, a default [[ModelEvent]] object will be created.
300
	 * @return bool True if event was triggered and is valid.
301
	 */
302 143
	public static function valid($model, $name, $event = null)
303
	{
304 143
		if (Event::trigger($model, $name, $event))
305
		{
306 117
			return $event->isValid;
307
		}
308
		else
309
		{
310 80
			return true;
311
		}
312
	}
313
314
	/**
315
	 * Triggers a class-level event and checks if it's handled.
316
	 * If don't have event handler returns true. If event handler is set, return true if `Event::handled`.
317
	 * This method will cause invocation of event handlers that are attached to the named event
318
	 * for the specified class and all its parent classes.
319
	 * @param AnnotatedInterface|string $model the object specifying the class-level event.
320
	 * @param string $name the event name.
321
	 * @param ModelEvent $event the event parameter. If not set, a default [[Event]] object will be created.
322
	 * @return bool|null True if handled, false otherways, null if not triggered
323
	 */
324 1
	public static function handled($model, $name, $event = null)
325
	{
326 1
		if (Event::trigger($model, $name, $event))
327
		{
328 1
			return $event->handled;
329
		}
330
		return true;
331
	}
332
333
	/**
334
	 * Check if model has event handler.
335
	 * **IMPORTANT**: It does not check for propagated events
336
	 *
337
	 * @param AnnotatedInterface|string $class the object specifying the class-level event
338
	 * @param string $name the event name.
339
	 * @return bool True if has handler
340
	 */
341 97
	public static function hasHandler($class, $name)
342
	{
343
		// Partials holds parts of class, this include interfaces and traits
344 97
		$partials = self::getPartials(self::getName($class));
345 97
		foreach ($partials as $className)
346
		{
347 97
			if (!empty(self::$events[$name][$className]))
348
			{
349 97
				return true;
350
			}
351
		}
352 2
		return false;
353
	}
354
355
	/**
356
	 * Get class name
357
	 * @param AnnotatedInterface|object|string $class
358
	 * @return string
359
	 */
360 121
	private static function getName($class)
361
	{
362 121
		if (is_object($class))
363
		{
364 121
			$class = get_class($class);
365
		}
366
		else
367
		{
368 3
			if (!ClassChecker::exists($class))
369
			{
370
				throw new UnexpectedValueException(sprintf("Class `%s` not found", $class));
371
			}
372
		}
373 121
		return ltrim($class, '\\');
374
	}
375
376
	/**
377
	 * Propagate event
378
	 * @param AnnotatedInterface|string $model
379
	 * @param string $name
380
	 * @param ModelEvent|null $event
381
	 */
382 144
	private static function propagate($model, $name, &$event = null)
383
	{
384 144
		$wasTriggered = false;
385 144
		if ($event && !$event->propagate())
386
		{
387 1
			return false;
388
		}
389
390 143
		foreach (self::getPropagatedProperties($model) as $property => $propagate)
391
		{
392 36
			if(!is_object($model))
393
			{
394
				continue;
395
			}
396 36
			if (empty($model->$property))
397
			{
398
				// Property is empty, skip
399 9
				continue;
400
			}
401
			// Trigger for arrays
402 34
			if (is_array($model->$property))
403
			{
404 14
				foreach ($model->$property as $object)
405
				{
406 14
					$wasTriggered = self::trigger($object, $name, $event) || $wasTriggered;
407
				}
408 14
				continue;
409
			}
410 25
			$wasTriggered = self::trigger($model->$property, $name, $event) || $wasTriggered;
411
		}
412 143
		return $wasTriggered;
413
	}
414
415
	/**
416
	 * Get properties which should be propagated.
417
	 * NOTE: This is cached, as it might be called numerous times
418
	 * @param object|string $model
419
	 * @return bool[]
420
	 */
421 143
	private static function getPropagatedProperties($model)
422
	{
423 143
		$key = get_class($model);
424 143
		if (empty(self::$propagated[$key]))
425
		{
426 143
			$propagated = [];
427 143
			foreach (ManganMeta::create($model)->properties('propagateEvents') as $name => $isPropagated)
428
			{
429 143
				if (!$isPropagated)
430
				{
431 143
					continue;
432
				}
433 26
				$propagated[$name] = true;
434
			}
435 143
			self::$propagated[$key] = $propagated;
436
		}
437 143
		return self::$propagated[$key];
438
	}
439
440
	/**
441
	 * Get class/interface/trait names from which class is composed.
442
	 *
443
	 * @param string $className
444
	 * @return array
445
	 */
446 121
	public static function getPartials($className)
447
	{
448 121
		if (array_key_exists($className, self::$partials))
449
		{
450
			return self::$partials[$className];
451
		}
452 121
		if(!ClassChecker::exists($className))
453
		{
454
			self::$partials[$className] = [];
455
			return [];
456
		}
457 121
		$partials = [];
458
		// Iterate over traits
459 121
		foreach ((new ReflectionClass($className))->getTraitNames() as $trait)
460
		{
461 24
			$partials[] = $trait;
462
		}
463
464
		// Iterate over interfaces to get partials
465 121
		foreach ((new ReflectionClass($className))->getInterfaceNames() as $interface)
466
		{
467 121
			$partials[] = $interface;
468
		}
469
470
		// Iterate over parent classes
471
		do
472
		{
473 121
			$partials[] = $className;
474
475
			// Iterate over traits of parent class
476 121
			foreach ((new ReflectionClass($className))->getTraitNames() as $trait)
477
			{
478 64
				$partials[] = $trait;
479
			}
480
481
			// Iterate over interfaces of parent class
482 121
			foreach ((new ReflectionClass($className))->getInterfaceNames() as $interface)
483
			{
484 121
				$partials[] = $interface;
485
			}
486
487
		}
488 121
		while (($className = get_parent_class($className)) !== false);
489 121
		$partials = array_unique($partials);
490 121
		sort($partials);
491 121
		self::$partials[$className] = $partials;
492 121
		return $partials;
493
	}
494
495
	protected static function destroyEvents()
496
	{
497
		self::$events = [];
498
	}
499
500
}
501