Completed
Push — master ( 6c4e57...0b7137 )
by Peter
21:21
created

Event::getPropagatedProperties()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 18
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

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