Completed
Push — master ( 0b7d10...45312b )
by Peter
07:37
created

Event::trigger()   B

Complexity

Conditions 9
Paths 29

Size

Total Lines 57
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 27
CRAP Score 9

Importance

Changes 1
Bugs 0 Features 0
Metric Value
dl 0
loc 57
ccs 27
cts 27
cp 1
rs 7.0745
c 1
b 0
f 0
cc 9
eloc 26
nc 29
nop 3
crap 9

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
		$allPartials = self::getPartials($className);
249
250
		// Filter out empty partials
251 21
		$cb = function($className)use($name)
252
		{
253 21
			if (empty(self::$events[$name][$className]))
254
			{
255 21
				return false;
256
			}
257 21
			return true;
258 21
		};
259 21
		$partials = array_filter($allPartials, $cb);
260
261
		// Trigger all partial events if applicable
262 21
		foreach ($partials as $className)
263
		{
264 21
			foreach (self::$events[$name][$className] as $handler)
265
			{
266 21
				$event->data = $handler[1];
267 21
				call_user_func($handler[0], $event);
268 21
				$wasTriggered = true;
269
270
				// Assign source for easier debugging
271 21
				$event->source = $className;
272
273
				// Event was handled, return true
274 21
				if ($event->handled)
275
				{
276 21
					return true;
277
				}
278
			}
279
		}
280
281
		// Propagate events to sub objects
282 20
		return self::propagate($model, $name, $event) || $wasTriggered;
283
	}
284
285
	/**
286
	 * Triggers a class-level event and checks if it's valid.
287
	 * If don't have event handler returns true. If event handler is set, return true if `Event::isValid`.
288
	 * This method will cause invocation of event handlers that are attached to the named event
289
	 * for the specified class and all its parent classes.
290
	 * @param AnnotatedInterface $model the object specifying the class-level event.
291
	 * @param string $name the event name.
292
	 * @param ModelEvent $event the event parameter. If not set, a default [[ModelEvent]] object will be created.
293
	 * @return bool True if event was triggered and is valid.
294
	 */
295 112
	public static function valid(AnnotatedInterface $model, $name, $event = null)
296
	{
297 112
		if (Event::trigger($model, $name, $event))
298
		{
299 12
			return $event->isValid;
300
		}
301
		else
302
		{
303 112
			return true;
304
		}
305
	}
306
307
	/**
308
	 * Triggers a class-level event and checks if it's handled.
309
	 * If don't have event handler returns true. If event handler is set, return true if `Event::handled`.
310
	 * This method will cause invocation of event handlers that are attached to the named event
311
	 * for the specified class and all its parent classes.
312
	 * @param AnnotatedInterface $model the object specifying the class-level event.
313
	 * @param string $name the event name.
314
	 * @param ModelEvent $event the event parameter. If not set, a default [[Event]] object will be created.
315
	 * @return bool|null True if handled, false otherway, null if not triggered
316
	 */
317 1
	public static function handled(AnnotatedInterface $model, $name, $event = null)
318
	{
319 1
		if (Event::trigger($model, $name, $event))
320
		{
321 1
			return $event->handled;
322
		}
323
		return true;
324
	}
325
326
	/**
327
	 * Check if model has event handler.
328
	 * **IMPORTANT**: It does not check for propagated events
329
	 *
330
	 * @param AnnotatedInterface|string $class the object specifying the class-level event
331
	 * @param string $name the event name.
332
	 * @return bool True if has handler
333
	 */
334 67
	public static function hasHandler($class, $name)
335
	{
336
		// Partials holds parts of class, this include interfaces and traits
337 67
		$partials = self::getPartials(self::getName($class));
338 67
		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...
339
		{
340 67
			if (!empty(self::$events[$name][$className]))
341
			{
342 67
				return true;
343
			}
344
		}
345 67
		return false;
346
	}
347
348
	/**
349
	 * Get class name
350
	 * @param AnnotatedInterface|object|string $class
351
	 * @return string
352
	 */
353 80
	private static function getName($class)
354
	{
355 80
		if (is_object($class))
356
		{
357 80
			$class = get_class($class);
358
		}
359
		else
360
		{
361
			if (!ClassChecker::exists($class))
362
			{
363
				throw new UnexpectedValueException(sprintf("Class `%s` not found", $class));
364
			}
365
		}
366 80
		return ltrim($class, '\\');
367
	}
368
369
	/**
370
	 * Propagate event
371
	 * @param AnnotatedInterface $model
372
	 * @param string $name
373
	 * @param ModelEvent|null $event
374
	 */
375 113
	private static function propagate(AnnotatedInterface $model, $name, &$event = null)
376
	{
377 113
		$wasTriggered = false;
378 113
		if ($event && !$event->propagate())
379
		{
380 1
			return false;
381
		}
382
383 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...
384
		{
385 36
			if (empty($model->$property))
386
			{
387
				// Property is empty, skip
388 22
				continue;
389
			}
390
			// Trigger for arrays
391 30
			if (is_array($model->$property))
392
			{
393 12
				foreach ($model->$property as $object)
394
				{
395 12
					$wasTriggered = self::trigger($object, $name, $event) || $wasTriggered;
396
				}
397 12
				continue;
398
			}
399
			// Trigger for single value
400 21
			$wasTriggered = self::trigger($model->$property, $name, $event) || $wasTriggered;
401
		}
402 113
		return $wasTriggered;
403
	}
404
405
	/**
406
	 * Get properties which should be propagated.
407
	 * NOTE: This is cached, as it might be called numerous times
408
	 * @param object $model
409
	 * @return bool[]
410
	 */
411 113
	private static function getPropagatedProperties($model)
412
	{
413 113
		$key = get_class($model);
414 113
		if (empty(self::$propagated[$key]))
415
		{
416 112
			$propageted = [];
417 112
			foreach (ManganMeta::create($model)->properties('propagateEvents') as $name => $isPropagated)
418
			{
419 112
				if (!$isPropagated)
420
				{
421 112
					continue;
422
				}
423 21
				$propageted[$name] = true;
424
			}
425 112
			self::$propagated[$key] = $propageted;
426
		}
427 113
		return self::$propagated[$key];
428
	}
429
430 80
	public static function getPartials($className)
431
	{
432 80
		if (!empty(self::$partials[$className]))
433
		{
434
			return self::$partials[$className];
435
		}
436
		// Iterate over traits
437 80
		foreach ((new ReflectionClass($className))->getTraitNames() as $trait)
438
		{
439 15
			$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...
440
		}
441
442
		// Iterate over interfaces to get partials
443 80
		foreach ((new ReflectionClass($className))->getInterfaceNames() as $interface)
444
		{
445 80
			$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...
446
		}
447
448
		// Iterate over parent classes
449
		do
450
		{
451 80
			$partials[] = $className;
452
		}
453 80
		while (($className = get_parent_class($className)) !== false);
454 80
		self::$partials[$className] = $partials;
455 80
		return $partials;
456
	}
457
458
	protected static function destroyEvents()
459
	{
460
		self::$events = [];
461
	}
462
463
}
464