Completed
Push — master ( 69ef22...b74acb )
by Peter
06:58
created

Event::on()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3.0416

Importance

Changes 0
Metric Value
dl 0
loc 12
ccs 5
cts 6
cp 0.8333
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 6
nc 2
nop 5
crap 3.0416
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 134
	public static function trigger($model, $name, &$event = null)
230
	{
231 134
		$wasTriggered = false;
232 134
		if (empty(self::$events[$name]))
233
		{
234 131
			return self::propagate($model, $name, $event);
235
		}
236 118
		if ($event === null)
237
		{
238 114
			$event = new ModelEvent();
239
		}
240 118
		$event->handled = false;
241 118
		$event->name = $name;
242
243 118
		if ($event->sender === null && is_object($model))
244
		{
245 118
			$event->sender = $model;
246
		}
247 118
		if(is_object($model))
248
		{
249 118
			$event->currentTarget = $model;
250
		}
251 118
		$className = self::getName($model);
252
253
		// Partials holds parts of class, this include interfaces and traits
254 118
		$allPartials = self::getPartials($className);
255
256
		// Filter out empty partials
257 118
		$partials = [];
258 118
		foreach ($allPartials as $className)
0 ignored issues
show
Bug introduced by
The expression $allPartials of type string|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
259
		{
260 118
			if (empty(self::$events[$name][$className]))
261
			{
262 118
				continue;
263
			}
264 118
			$partials[] = $className;
265
		}
266
267
		// Trigger all partial events if applicable
268 118
		foreach ($partials as $className)
269
		{
270 118
			foreach (self::$events[$name][$className] as $handler)
271
			{
272
				// Assign source for easier debugging or other uses
273 118
				$event->source = $className;
274
275 118
				$event->data = $handler[1];
276 118
				call_user_func($handler[0], $event);
277 118
				$wasTriggered = true;
278
279
280
				// Event was handled, return true
281 118
				if ($event->handled)
282
				{
283 118
					return true;
284
				}
285
			}
286
		}
287
288
		// Propagate events to sub objects
289 118
		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 133
	public static function valid($model, $name, $event = null)
303
	{
304 133
		if (Event::trigger($model, $name, $event))
305
		{
306 114
			return $event->isValid;
307
		}
308
		else
309
		{
310 70
			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 95
	public static function hasHandler($class, $name)
342
	{
343
		// Partials holds parts of class, this include interfaces and traits
344 95
		$partials = self::getPartials(self::getName($class));
345 95
		foreach ($partials as $className)
0 ignored issues
show
Bug introduced by
The expression $partials of type string|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
346
		{
347 95
			if (!empty(self::$events[$name][$className]))
348
			{
349 95
				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 118
	private static function getName($class)
361
	{
362 118
		if (is_object($class))
363
		{
364 118
			$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 118
		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 134
	private static function propagate($model, $name, &$event = null)
383
	{
384 134
		$wasTriggered = false;
385 134
		if ($event && !$event->propagate())
386
		{
387 1
			return false;
388
		}
389
390 133
		foreach (self::getPropagatedProperties($model) as $property => $propagate)
391
		{
392 33
			if(!is_object($model))
393
			{
394
				continue;
395
			}
396 33
			if (empty($model->$property))
397
			{
398
				// Property is empty, skip
399 9
				continue;
400
			}
401
			// Trigger for arrays
402 31
			if (is_array($model->$property))
403
			{
404 12
				foreach ($model->$property as $object)
405
				{
406 12
					$wasTriggered = self::trigger($object, $name, $event) || $wasTriggered;
407
				}
408 12
				continue;
409
			}
410 22
			$wasTriggered = self::trigger($model->$property, $name, $event) || $wasTriggered;
411
		}
412 133
		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 133
	private static function getPropagatedProperties($model)
422
	{
423 133
		$key = get_class($model);
424 133
		if (empty(self::$propagated[$key]))
425
		{
426 133
			$propagated = [];
427 133
			foreach (ManganMeta::create($model)->properties('propagateEvents') as $name => $isPropagated)
428
			{
429 133
				if (!$isPropagated)
430
				{
431 133
					continue;
432
				}
433 24
				$propagated[$name] = true;
434
			}
435 133
			self::$propagated[$key] = $propagated;
436
		}
437 133
		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 118
	public static function getPartials($className)
447
	{
448 118
		if (array_key_exists($className, self::$partials))
449
		{
450
			return self::$partials[$className];
451
		}
452 118
		if(!ClassChecker::exists($className))
453
		{
454
			self::$partials[$className] = [];
455
			return [];
456
		}
457 118
		$partials = [];
458
		// Iterate over traits
459 118
		foreach ((new ReflectionClass($className))->getTraitNames() as $trait)
460
		{
461 23
			$partials[] = $trait;
462
		}
463
464
		// Iterate over interfaces to get partials
465 118
		foreach ((new ReflectionClass($className))->getInterfaceNames() as $interface)
466
		{
467 118
			$partials[] = $interface;
468
		}
469
470
		// Iterate over parent classes
471
		do
472
		{
473 118
			$partials[] = $className;
474
		}
475 118
		while (($className = get_parent_class($className)) !== false);
476 118
		self::$partials[$className] = $partials;
477 118
		return $partials;
478
	}
479
480
	protected static function destroyEvents()
481
	{
482
		self::$events = [];
483
	}
484
485
}
486