Completed
Push — master ( 4dddb7...e1d674 )
by Peter
08:56
created

Event::destroyEvents()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 4
ccs 0
cts 3
cp 0
rs 10
cc 1
eloc 2
nc 1
nop 0
crap 2
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. This contains all event registered for application.
79
	 * @var EventInterface[]
80
	 */
81
	private static $events = [];
82
83
	/**
84
	 * Array containing partial classes for class. This holds traits, interfaces,
85
	 * parent classes names for class.
86
	 * 
87
	 * Structure is like below:
88
	 * ```
89
	 * $partials = [
90
	 * 		'\Vendor\Package\ClassOne' => [
91
	 * 			'\Vendor\Package\TraitOne',
92
	 * 			'\Vendor\Package\TraitTwo',
93
	 * 			'\Vendor\Package\InterfaceX',
94
	 * 			'\Vendor\Package\AbstractSix'
95
	 * 		],
96
	 * 		'\Vendor\Package\ClassTwo' => [
97
	 * 			'\Vendor\Package\TraitBig',
98
	 * 			'\Vendor\Package\TraitSmall',
99
	 * 			'\Vendor\Package\InterfaceY',
100
	 * 			'\Vendor\Package\AbstractGuy'
101
	 * 		],
102
	 * ];
103
	 * ```
104
	 * 
105
	 * @var string[]
106
	 */
107
	private static $partials = [];
108
109
	/**
110
	 * Propagated properties cache. It contains only properties which should
111
	 * propagate, others are skipped, thus value is always true.
112
	 * 
113
	 * Structure is like follow:
114
	 * ```
115
	 * $propagated = [
116
	 * 		'\Vendor\Package\ClassOne' => [
117
	 * 			'fieldOne' => true,
118
	 * 			'fieldTwo' => true
119
	 * 		],
120
	 * 		'\Vendor\Package\ClassTwo' => [
121
	 * 			'otherFieldOne' => true,
122
	 * 			'otherFieldTwo' => true
123
	 * 		],
124
	 * 
125
	 * ];
126
	 * ```
127
	 * @var bool[]
128
	 */
129
	private static $propagated = [];
130
131
	/**
132
	 * Attaches an event handler to a class-level event.
133
	 *
134
	 * When a class-level event is triggered, event handlers attached
135
	 * to that class and all parent classes will be invoked.
136
	 *
137
	 * For example, the following code attaches an event handler to document's
138
	 * `afterInsert` event:
139
	 *
140
	 * ~~~
141
	 * Event::on($model, EntityManager::EventAfterInsert, function ($event) {
142
	 * 		var_dump(get_class($event->sender) . ' is inserted.');
143
	 * });
144
	 * ~~~
145
	 *
146
	 * The handler will be invoked for every successful document insertion.
147
	 *
148
	 * **NOTE:** Each call will attach new event handler. When placing event
149
	 * initialization in class constructors etc. ensure that it is evaluated once,
150
	 * or it might trigger same event handler multiple times.
151
	 *
152
	 * @param AnnotatedInterface|object|string $model the object specifying the class-level event.
153
	 * @param string $name the event name.
154
	 * @param callable $handler the event handler.
155
	 * @param mixed $data the data to be passed to the event handler when the event is triggered.
156
	 * When the event handler is invoked, this data can be accessed via [[Event::data]].
157
	 * @param boolean $append whether to append new event handler to the end of the existing
158
	 * handler list. If false, the new handler will be inserted at the beginning of the existing
159
	 * handler list.
160
	 * @see off()
161
	 */
162 24
	public static function on($model, $name, $handler, $data = null, $append = true)
163
	{
164 24
		$class = self::getName($model);
165 24
		if ($append || empty(self::$events[$name][$class]))
166
		{
167 24
			self::$events[$name][$class][] = [$handler, $data];
168
		}
169
		else
170
		{
171
			array_unshift(self::$events[$name][$class], [$handler, $data]);
172
		}
173 24
	}
174
175
	/**
176
	 * Detaches an event handler from a class-level event.
177
	 *
178
	 * This method is the opposite of [[on()]].
179
	 *
180
	 * @param AnnotatedInterface|object|string $model the object specifying the class-level event.
181
	 * @param string $name the event name.
182
	 * @param callable $handler the event handler to be removed.
183
	 * If it is null, all handlers attached to the named event will be removed.
184
	 * @return boolean whether a handler is found and detached.
185
	 * @see on()
186
	 */
187 10
	public static function off($model, $name, $handler = null)
188
	{
189 10
		$class = self::getName($model);
190 10
		if (empty(self::$events[$name][$class]))
191
		{
192
			return false;
193
		}
194 10
		if ($handler === null)
195
		{
196
			unset(self::$events[$name][$class]);
197
			return true;
198
		}
199
		else
200
		{
201 10
			$removed = false;
202 10
			foreach (self::$events[$name][$class] as $i => $event)
203
			{
204 10
				if ($event[0] === $handler)
205
				{
206 10
					unset(self::$events[$name][$class][$i]);
207 10
					$removed = true;
208
				}
209
			}
210 10
			if ($removed)
211
			{
212 10
				self::$events[$name][$class] = array_values(self::$events[$name][$class]);
213
			}
214 10
			return $removed;
215
		}
216
	}
217
218
	/**
219
	 * Triggers a class-level event.
220
	 * This method will cause invocation of event handlers that are attached to the named event
221
	 * for the specified class and all its parent classes.
222
	 * @param AnnotatedInterface $model the object specifying the class-level event.
223
	 * @param string $name the event name.
224
	 * @param ModelEvent $event the event parameter. If not set, a default `ModelEvent` object will be created.
225
	 * @return bool True if event was triggered.
226
	 */
227 113
	public static function trigger(AnnotatedInterface $model, $name, &$event = null)
228
	{
229 113
		$wasTriggered = false;
230 113
		if (empty(self::$events[$name]))
231
		{
232 113
			return self::propagate($model, $name, $event);
233
		}
234 21
		if ($event === null)
235
		{
236 17
			$event = new ModelEvent();
237
		}
238 21
		$event->handled = false;
239 21
		$event->name = $name;
240
241 21
		if ($event->sender === null)
242
		{
243 17
			$event->sender = $model;
244
		}
245 21
		$className = self::getName($model);
246
247
		// Partials holds parts of class, this include interfaces and traits
248 21
		$partials = self::getPartials($className);
249
250
		// Trigger all partial events if applicable
251 21
		foreach ($partials as $className)
0 ignored issues
show
Bug introduced by
The expression $partials of type string|array<integer,?> 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...
252
		{
253 21
			if (empty(self::$events[$name][$className]))
254
			{
255 21
				continue;
256
			}
257
258 21
			foreach (self::$events[$name][$className] as $handler)
259
			{
260 21
				$event->data = $handler[1];
261 21
				call_user_func($handler[0], $event);
262 21
				$wasTriggered = true;
263
264
				// Assign source for easier debugging
265 21
				$event->source = $className;
266
267
				// Event was handled, return true
268 21
				if ($event->handled)
269
				{
270 21
					return true;
271
				}
272
			}
273
		}
274
275
		// Propagate events to sub objects
276 20
		return self::propagate($model, $name, $event) || $wasTriggered;
277
	}
278
279
	/**
280
	 * Triggers a class-level event and checks if it's valid.
281
	 * If don't have event handler returns true. If event handler is set, return true if `Event::isValid`.
282
	 * This method will cause invocation of event handlers that are attached to the named event
283
	 * for the specified class and all its parent classes.
284
	 * @param AnnotatedInterface $model the object specifying the class-level event.
285
	 * @param string $name the event name.
286
	 * @param ModelEvent $event the event parameter. If not set, a default [[ModelEvent]] object will be created.
287
	 * @return bool True if event was triggered and is valid.
288
	 */
289 112
	public static function valid(AnnotatedInterface $model, $name, $event = null)
290
	{
291 112
		if (Event::trigger($model, $name, $event))
292
		{
293 12
			return $event->isValid;
294
		}
295
		else
296
		{
297 112
			return true;
298
		}
299
	}
300
301
	/**
302
	 * Triggers a class-level event and checks if it's handled.
303
	 * If don't have event handler returns true. If event handler is set, return true if `Event::handled`.
304
	 * This method will cause invocation of event handlers that are attached to the named event
305
	 * for the specified class and all its parent classes.
306
	 * @param AnnotatedInterface $model the object specifying the class-level event.
307
	 * @param string $name the event name.
308
	 * @param ModelEvent $event the event parameter. If not set, a default [[Event]] object will be created.
309
	 * @return bool|null True if handled, false otherway, null if not triggered
310
	 */
311 1
	public static function handled(AnnotatedInterface $model, $name, $event = null)
312
	{
313 1
		if (Event::trigger($model, $name, $event))
314
		{
315 1
			return $event->handled;
316
		}
317
		return true;
318
	}
319
320
	/**
321
	 * Check if model has event handler.
322
	 * **IMPORTANT**: It does not check for propagated events
323
	 * 
324
	 * @param AnnotatedInterface|string $class the object specifying the class-level event
325
	 * @param string $name the event name.
326
	 * @return bool True if has handler
327
	 */
328 60
	public static function hasHandler($class, $name)
329
	{
330
		// Partials holds parts of class, this include interfaces and traits
331 60
		$partials = self::getPartials(self::getName($class));
332 60
		foreach ($partials as $className)
1 ignored issue
show
Bug introduced by
The expression $partials of type string|array<integer,?> 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...
333
		{
334 60
			if (!empty(self::$events[$name][$className]))
335
			{
336 60
				return true;
337
			}
338
		}
339 60
		return false;
340
	}
341
342
	/**
343
	 * Get class name
344
	 * @param AnnotatedInterface|object|string $class
345
	 * @return string
346
	 */
347 73
	private static function getName($class)
348
	{
349 73
		if (is_object($class))
350
		{
351 73
			$class = get_class($class);
352
		}
353
		else
354
		{
355
			if (!ClassChecker::exists($class))
356
			{
357
				throw new UnexpectedValueException(sprintf("Class `%s` not found", $class));
358
			}
359
		}
360 73
		return ltrim($class, '\\');
361
	}
362
363
	/**
364
	 * Propagate event
365
	 * @param AnnotatedInterface $model
366
	 * @param string $name
367
	 * @param ModelEvent|null $event
368
	 */
369 113
	private static function propagate(AnnotatedInterface $model, $name, &$event = null)
370
	{
371 113
		$wasTriggered = false;
372 113
		if ($event && !$event->propagate())
373
		{
374 1
			return false;
375
		}
376
377 113
		foreach (self::getPropagatedProperties($model) as $property => $propagate)
1 ignored issue
show
Bug introduced by
The expression self::getPropagatedProperties($model) of type boolean|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...
378
		{
379 36
			if (empty($model->$property))
380
			{
381
				// Property is empty, skip
382 22
				continue;
383
			}
384
			// Trigger for arrays
385 30
			if (is_array($model->$property))
386
			{
387 12
				foreach ($model->$property as $object)
388
				{
389 12
					$wasTriggered = self::trigger($object, $name, $event) || $wasTriggered;
390
				}
391 12
				continue;
392
			}
393
			// Trigger for single value
394 21
			$wasTriggered = self::trigger($model->$property, $name, $event) || $wasTriggered;
395
		}
396 113
		return $wasTriggered;
397
	}
398
399
	/**
400
	 * Get properties which should be propagated.
401
	 * NOTE: This is cached, as it might be called numerous times
402
	 * @param object $model
403
	 * @return bool[]
404
	 */
405 113
	private static function getPropagatedProperties($model)
406
	{
407 113
		$key = get_class($model);
408 113
		if (empty(self::$propagated[$key]))
409
		{
410 112
			$propageted = [];
411 112
			foreach (ManganMeta::create($model)->properties('propagateEvents') as $name => $isPropagated)
412
			{
413 112
				if (!$isPropagated)
414
				{
415 112
					continue;
416
				}
417 21
				$propageted[$name] = true;
418
			}
419 112
			self::$propagated[$key] = $propageted;
420
		}
421 113
		return self::$propagated[$key];
422
	}
423
424 73
	public static function getPartials($className)
425
	{
426 73
		if (!empty(self::$partials[$className]))
427
		{
428
			return self::$partials[$className];
429
		}
430
		// Iterate over traits
431 73
		foreach ((new ReflectionClass($className))->getTraitNames() as $trait)
432
		{
433 14
			$partials[] = $trait;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$partials was never initialized. Although not strictly required by PHP, it is generally a good practice to add $partials = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
434
		}
435
436
		// Iterate over interfaces to get partials
437 73
		foreach ((new ReflectionClass($className))->getInterfaceNames() as $interface)
438
		{
439 73
			$partials[] = $interface;
0 ignored issues
show
Bug introduced by
The variable $partials does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
440
		}
441
442
		// Iterate over parent classes
443
		do
444
		{
445 73
			$partials[] = $className;
446
		}
447 73
		while (($className = get_parent_class($className)) !== false);
448 73
		self::$partials[$className] = $partials;
449 73
		return $partials;
450
	}
451
452
	protected static function destroyEvents()
453
	{
454
		self::$events = [];
455
	}
456
457
}
458