Completed
Push — string-event-subscriptions ( e8cd90 )
by Peter
21:12
created

Event::_propagate()   C

Complexity

Conditions 8
Paths 7

Size

Total Lines 32
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 32
rs 5.3846
cc 8
eloc 16
nc 7
nop 3
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|string $model the object specifying the class-level event.
105
	 * @param string $name the event name.
106
	 * @param callable $handler the event handler.
107
	 * @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
	 * @param boolean $append whether to append new event handler to the end of the existing
110
	 * handler list. If false, the new handler will be inserted at the beginning of the existing
111
	 * handler list.
112
	 * @see off()
113
	 */
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
		{
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
	 * @param AnnotatedInterface|string $model the object specifying the class-level event.
133
	 * @param string $name the event name.
134
	 * @param callable $handler the event handler to be removed.
135
	 * If it is null, all handlers attached to the named event will be removed.
136
	 * @return boolean whether a handler is found and detached.
137
	 * @see on()
138
	 */
139
	public static function off($model, $name, $handler = null)
140
	{
141
		$class = self::_getName($model);
142
		if (empty(self::$_events[$name][$class]))
143
		{
144
			return false;
145
		}
146
		if ($handler === null)
147
		{
148
			unset(self::$_events[$name][$class]);
149
			return true;
150
		}
151
		else
152
		{
153
			$removed = false;
154
			foreach (self::$_events[$name][$class] as $i => $event)
155
			{
156
				if ($event[0] === $handler)
157
				{
158
					unset(self::$_events[$name][$class][$i]);
159
					$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
	 * 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
	 * @param AnnotatedInterface $model the object specifying the class-level event.
175
	 * @param string $name the event name.
176
	 * @param ModelEvent $event the event parameter. If not set, a default [[Event]] object will be created.
177
	 * @return bool True if event was triggered.
178
	 */
179
	public static function trigger(AnnotatedInterface $model, $name, &$event = null)
180
	{
181
		$wasTriggered = false;
182
		if (empty(self::$_events[$name]))
183
		{
184
			return self::_propagate($model, $name, $event);
185
		}
186
		if ($event === null)
187
		{
188
			$event = new ModelEvent();
189
		}
190
		$event->handled = false;
191
		$event->name = $name;
192
193
		if ($event->sender === null)
194
		{
195
			$event->sender = $model;
196
		}
197
		$className = self::_getName($model);
198
199
		// Partials holds parts of class, this include interfaces and traits
200
		$partials = [];
201
		// Iterate over traits
202
		foreach ((new ReflectionClass($className))->getTraitNames() as $trait)
203
		{
204
			$partials[] = $trait;
205
		}
206
207
		// Iterate over interfaces to get partials
208
		foreach ((new ReflectionClass($className))->getInterfaceNames() as $interface)
209
		{
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
		{
223
			if (empty(self::$_events[$name][$className]))
224
			{
225
				continue;
226
			}
227
228
			foreach (self::$_events[$name][$className] as $handler)
229
			{
230
				$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
	}
245
246
	/**
247
	 * Triggers a class-level event and checks if it's valid.
248
	 * 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
			return $event->isValid;
261
		}
262
		else
263
		{
264
			return true;
265
		}
266
	}
267
268
	/**
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
	 * This method will cause invocation of event handlers that are attached to the named event
272
	 * 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
		if (Event::trigger($model, $name, $event))
281
		{
282
			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
	 * @param AnnotatedInterface|string $class the object specifying the class-level event
292
	 * @param string $name the event name.
293
	 * @return bool True if has handler
294
	 */
295
	public static function hasHandler($class, $name)
296
	{
297
		$className = self::_getName($class);
298
299
		do
300
		{
301
			if (!empty(self::$_events[$name][$className]))
302
			{
303
				return true;
304
			}
305
		}
306
		while (($className = get_parent_class($className)) !== false);
307
		return false;
308
	}
309
310
	/**
311
	 * Get class name
312
	 * @param AnnotatedInterface|string $class
313
	 * @return string
314
	 */
315
	private static function _getName($class)
316
	{
317
		if (is_object($class))
318
		{
319
			$class = get_class($class);
320
		}
321
		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