Completed
Push — master ( 0ad795...d4a99d )
by Peter
21:24
created

Event::getPartials()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 27
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

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