Passed
Push — 5.x ( 88ff80...0e9d8a )
by Jeroen
12:39 queued 15s
created

EventsService::callHandler()   A

Complexity

Conditions 5
Paths 16

Size

Total Lines 41
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 24
CRAP Score 5.0342

Importance

Changes 0
Metric Value
cc 5
eloc 24
nc 16
nop 4
dl 0
loc 41
ccs 24
cts 27
cp 0.8889
crap 5.0342
rs 9.2248
c 0
b 0
f 0
1
<?php
2
3
namespace Elgg;
4
5
use Elgg\EventsService\MethodMatcher;
6
use Elgg\Traits\Debug\Profilable;
7
use Elgg\Traits\Loggable;
8
use Psr\Log\LogLevel;
9
10
/**
11
 * Events service
12
 *
13
 * Use elgg()->events
14
 */
15
class EventsService {
16
	
17
	use Loggable;
18
	use Profilable;
19
	
20
	const REG_KEY_PRIORITY = 0;
21
	const REG_KEY_INDEX = 1;
22
	const REG_KEY_HANDLER = 2;
23
	
24
	const OPTION_STOPPABLE = 'stoppable';
25
	const OPTION_USE_TIMER = 'use_timer';
26
	const OPTION_TIMER_KEYS = 'timer_keys';
27
	const OPTION_BEGIN_CALLBACK = 'begin_callback';
28
	const OPTION_END_CALLBACK = 'end_callback';
29
30
	/**
31
	 * @var HandlersService
32
	 */
33
	protected $handlers;
34
	
35
	/**
36
	 * @var int
37
	 */
38
	protected $next_index = 0;
39
	
40
	/**
41
	 * @var array [name][type][] = registration
42
	 */
43
	protected $registrations = [];
44
	
45
	/**
46
	 * @var array
47
	 */
48
	protected $backups = [];
49
50
	/**
51
	 * Constructor
52
	 *
53
	 * @param HandlersService $handlers Handlers
54
	 */
55 6903
	public function __construct(HandlersService $handlers) {
56 6903
		$this->handlers = $handlers;
57
	}
58
59
	/**
60
	 * Triggers an Elgg event
61
	 *
62
	 * @param string $name    The event name
63
	 * @param string $type    The event type
64
	 * @param mixed  $object  The object involved in the event
65
	 * @param array  $options (internal) options for triggering the event
66
	 *
67
	 * @see elgg_trigger_event()
68
	 * @see elgg_trigger_after_event()
69
	 * @see elgg_trigger_before_event()
70
	 *
71
	 * @return bool
72
	 */
73 3579
	public function trigger(string $name, string $type, $object = null, array $options = []): bool {
74 3579
		$options = array_merge([
75 3579
			self::OPTION_STOPPABLE => true,
76 3579
		], $options);
77
		
78
		// allow for the profiling of system events (when enabled)
79 3579
		if ($this->hasTimer() && $type === 'system' && $name !== 'shutdown') {
80
			$options[self::OPTION_USE_TIMER] = true;
81
			$options[self::OPTION_TIMER_KEYS] = ["[{$name},{$type}]"];
82
		}
83
		
84
		// get registered handlers
85 3579
		$handlers = $this->getOrderedHandlers($name, $type);
86
87
		// This starts as a string, but if a handler type-hints an object we convert it on-demand inside
88
		// \Elgg\HandlersService::call and keep it alive during all handler calls. We do this because
89
		// creating objects for every triggering is expensive.
90
		/* @var $event Event|string */
91 3579
		$event = 'event';
92 3579
		$event_args = [
93 3579
			$name,
94 3579
			$type,
95 3579
			null,
96 3579
			[
97 3579
				'object' => $object,
98 3579
				'_elgg_sequence_id' => elgg_extract('_elgg_sequence_id', $options),
99 3579
			],
100 3579
		];
101 3579
		foreach ($handlers as $handler) {
102 3200
			list($success, $return, $event) = $this->callHandler($handler, $event, $event_args, $options);
103
104 3200
			if (!$success) {
105 1
				continue;
106
			}
107
108 3199
			if (!empty($options[self::OPTION_STOPPABLE]) && ($return === false)) {
109 12
				return false;
110
			}
111
		}
112
113 3575
		return true;
114
	}
115
	
116
	/**
117
	 * Triggers a event that is allowed to return a mixed result
118
	 *
119
	 * @param string $name    The name of the event
120
	 * @param string $type    The type of the event
121
	 * @param mixed  $params  Supplied params for the event
122
	 * @param mixed  $value   The value of the event, this can be altered by registered callbacks
123
	 * @param array  $options (internal) options for triggering the event
124
	 *
125
	 * @return mixed
126
	 *
127
	 * @see elgg_trigger_event_results()
128
	 */
129 4868
	public function triggerResults(string $name, string $type, array $params = [], $value = null, array $options = []) {
130
		// This starts as a string, but if a handler type-hints an object we convert it on-demand inside
131
		// \Elgg\HandlersService::call and keep it alive during all handler calls. We do this because
132
		// creating objects for every triggering is expensive.
133
		/* @var $event Event|string */
134 4868
		$event = 'event';
135 4868
		foreach ($this->getOrderedHandlers($name, $type) as $handler) {
136 3589
			$event_args = [$name, $type, $value, $params];
137
			
138 3589
			list($success, $return, $event) = $this->callHandler($handler, $event, $event_args, $options);
139
			
140 3585
			if (!$success) {
141 1
				continue;
142
			}
143
			
144 3584
			if ($return !== null) {
145 2746
				$value = $return;
146 2746
				$event->setValue($value);
147
			}
148
		}
149
		
150 4865
		return $value;
151
	}
152
153
	/**
154
	 * Trigger a "Before event" indicating a process is about to begin.
155
	 *
156
	 * Like regular events, a handler returning false will cancel the process and false
157
	 * will be returned.
158
	 *
159
	 * To register for a before event, append ":before" to the event name when registering.
160
	 *
161
	 * @param string $name    The event type. The fired event type will be appended with ":before".
162
	 * @param string $type    The object type
163
	 * @param mixed  $object  The object involved in the event
164
	 * @param array  $options (internal) options for triggering the event
165
	 *
166
	 * @return bool False if any handler returned false, otherwise true
167
	 *
168
	 * @see EventsService::trigger()
169
	 * @see EventsService::triggerAfter()
170
	 * @since 2.0.0
171
	 */
172 3506
	public function triggerBefore(string $name, string $type, $object = null, array $options = []): bool {
173 3506
		return $this->trigger("{$name}:before", $type, $object, $options);
174
	}
175
176
	/**
177
	 * Trigger an "After event" indicating a process has finished.
178
	 *
179
	 * Unlike regular events, all the handlers will be called, their return values ignored.
180
	 *
181
	 * To register for an after event, append ":after" to the event name when registering.
182
	 *
183
	 * @param string $name    The event name. The fired event type will be appended with ":after".
184
	 * @param string $type    The event type
185
	 * @param mixed  $object  The object involved in the event
186
	 * @param array  $options (internal) options for triggering the event
187
	 *
188
	 * @return void
189
	 *
190
	 * @see EventsService::trigger()
191
	 * @see EventsService::triggerBefore()
192
	 * @since 2.0.0
193
	 */
194 3520
	public function triggerAfter(string $name, string $type, $object = null, array $options = []): void {
195 3520
		$options[self::OPTION_STOPPABLE] = false;
196
		
197 3520
		$this->trigger("{$name}:after", $type, $object, $options);
198
	}
199
200
	/**
201
	 * Trigger a sequence of <event>:before, <event>, and <event>:after handlers.
202
	 * Allows <event>:before to terminate the sequence by returning false from a handler
203
	 * Allows running a callable on successful <event> before <event>:after is triggered
204
	 * Returns the result of the callable or bool
205
	 *
206
	 * @param string   $name     The event name
207
	 * @param string   $type     The event type
208
	 * @param mixed    $object   The object involved in the event
209
	 * @param callable $callable Callable to run on successful event, before event:after
210
	 * @param array    $options  (internal) options for triggering the event
211
	 *
212
	 * @return bool
213
	 */
214 2922
	public function triggerSequence(string $name, string $type, $object = null, callable $callable = null, array $options = []): bool {
215
		// generate a unique ID to identify this sequence
216 2922
		$options['_elgg_sequence_id'] = uniqid("{$name}{$type}", true);
217
		
218 2922
		if (!$this->triggerBefore($name, $type, $object, $options)) {
219
			return false;
220
		}
221
222 2922
		$result = $this->trigger($name, $type, $object, $options);
223 2922
		if ($result === false) {
224
			return false;
225
		}
226
227 2922
		if ($callable) {
228 876
			$result = call_user_func($callable, $object);
229
		}
230
231 2922
		$this->triggerAfter($name, $type, $object, $options);
232
233 2922
		return $result;
234
	}
235
236
	/**
237
	 * Trigger an sequence of <event>:before, <event>, and <event>:after handlers.
238
	 * Allows <event>:before to terminate the sequence by returning false from a handler
239
	 * Allows running a callable on successful <event> before <event>:after is triggered
240
	 *
241
	 * @param string   $name     The event name
242
	 * @param string   $type     The event type
243
	 * @param mixed    $params   Supplied params for the event
244
	 * @param mixed    $value    The value of the event, this can be altered by registered callbacks
245
	 * @param callable $callable Callable to run on successful event, before event:after
246
	 * @param array    $options  (internal) options for triggering the event
247
	 *
248
	 * @return mixed
249
	 */
250 6
	public function triggerResultsSequence(string $name, string $type, array $params = [], $value = null, callable $callable = null, array $options = []) {
251
		// generate a unique ID to identify this sequence
252 6
		$unique_id = uniqid("{$name}{$type}results", true);
253 6
		$options['_elgg_sequence_id'] = $unique_id;
254 6
		$params['_elgg_sequence_id'] = $unique_id;
255
		
256 6
		if (!$this->triggerBefore($name, $type, $params, $options)) {
257
			return false;
258
		}
259
260 6
		$result = $this->triggerResults($name, $type, $params, $value, $options);
261 6
		if ($result === false) {
262
			return false;
263
		}
264
265 6
		if ($callable) {
266 5
			$result = call_user_func($callable, $params);
267
		}
268
269 6
		$this->triggerAfter($name, $type, $params, $options);
270
271 6
		return $result;
272
	}
273
274
	/**
275
	 * Trigger an event sequence normally, but send a notice about deprecated use if any handlers are registered.
276
	 *
277
	 * @param string $name    The event name
278
	 * @param string $type    The event type
279
	 * @param mixed  $object  The object involved in the event
280
	 * @param string $message The deprecation message
281
	 * @param string $version Human-readable *release* version: 1.9, 1.10, ...
282
	 * @param array  $options (internal) options for triggering the event
283
	 *
284
	 * @return bool
285
	 *
286
	 * @see elgg_trigger_deprecated_event()
287
	 */
288 3
	public function triggerDeprecated(string $name, string $type, $object = null, string $message = '', string $version = '', array $options = []): bool {
289 3
		$message = "The '{$name}', '{$type}' event is deprecated. {$message}";
290 3
		$this->checkDeprecation($name, $type, $message, $version);
291
		
292 3
		return $this->trigger($name, $type, $object, $options);
293
	}
294
295
	/**
296
	 * Trigger an event sequence normally, but send a notice about deprecated use if any handlers are registered.
297
	 *
298
	 * @param string $name        The event name
299
	 * @param string $type        The event type
300
	 * @param array  $params      The parameters related to the event
301
	 * @param mixed  $returnvalue The return value
302
	 * @param string $message     The deprecation message
303
	 * @param string $version     Human-readable *release* version: 1.9, 1.10, ...
304
	 * @param array  $options     (internal) options for triggering the event
305
	 *
306
	 * @return mixed
307
	 *
308
	 * @see elgg_trigger_deprecated_event_results()
309
	 */
310 3
	public function triggerDeprecatedResults(string $name, string $type, array $params = [], $returnvalue = null, string $message = '', string $version = '', array $options = []) {
311 3
		$message = "The '{$name}', '{$type}' event is deprecated. {$message}";
312 3
		$this->checkDeprecation($name, $type, $message, $version);
313
		
314 3
		return $this->triggerResults($name, $type, $params, $returnvalue, $options);
315
	}
316
	
317
	/**
318
	 * Register a callback as a event handler.
319
	 *
320
	 * @param string   $name     The name of the event
321
	 * @param string   $type     The type of the event
322
	 * @param callable $callback The name of a valid function or an array with object and method
323
	 * @param int      $priority The priority - 500 is default, lower numbers called first
324
	 *
325
	 * @return bool
326
	 *
327
	 * @warning This doesn't check if a callback is valid to be called, only if it is in the
328
	 *          correct format as a callable.
329
	 */
330 2906
	public function registerHandler(string $name, string $type, $callback, int $priority = 500): bool {
331 2906
		if (empty($name) || empty($type) || !is_callable($callback, true)) {
332 1
			return false;
333
		}
334
		
335 2906
		if (($name == 'view' || $name == 'view_vars') && $type !== 'all') {
336 2378
			$type = ViewsService::canonicalizeViewName($type);
337
		}
338
				
339 2906
		$services = _elgg_services();
340 2906
		if (in_array($this->getLogger()->getLevel(false), [LogLevel::WARNING, LogLevel::NOTICE, LogLevel::INFO, LogLevel::DEBUG])) {
341 2823
			if (!$services->handlers->isCallable($callback)) {
342 10
				$this->getLogger()->warning('Handler: ' . $services->handlers->describeCallable($callback) . ' is not callable');
343
			}
344
		}
345
		
346 2906
		$this->registrations[$name][$type][] = [
347 2906
			self::REG_KEY_PRIORITY => $priority,
348 2906
			self::REG_KEY_INDEX => $this->next_index,
349 2906
			self::REG_KEY_HANDLER => $callback,
350 2906
		];
351 2906
		$this->next_index++;
352
		
353 2906
		return true;
354
	}
355
	
356
	/**
357
	 * Unregister a callback as an event handler.
358
	 *
359
	 * @param string   $name     The name of the event
360
	 * @param string   $type     The name of the type of entity (eg "user", "object" etc)
361
	 * @param callable $callback The PHP callback to be removed. Since 1.11, static method
362
	 *                           callbacks will match dynamic methods
363
	 *
364
	 * @return void
365
	 */
366 7068
	public function unregisterHandler(string $name, string $type, $callback): void {
367 7068
		if (($name === 'view' || $name === 'view_vars') && $type !== 'all') {
368 1
			$type = ViewsService::canonicalizeViewName($type);
369
		}
370
		
371 7068
		if (empty($this->registrations[$name][$type])) {
372 4637
			return;
373
		}
374
		
375 2508
		$matcher = $this->getMatcher($callback);
376
		
377 2508
		foreach ($this->registrations[$name][$type] as $i => $registration) {
378 2508
			if ($matcher instanceof MethodMatcher) {
379 2480
				if (!$matcher->matches($registration[self::REG_KEY_HANDLER])) {
380 2480
					continue;
381
				}
382 34
			} elseif ($registration[self::REG_KEY_HANDLER] != $callback) {
383 14
				continue;
384
			}
385
			
386 2461
			unset($this->registrations[$name][$type][$i]);
387 2461
			return;
388
		}
389
	}
390
	
391
	/**
392
	 * Clears all callback registrations for an event.
393
	 *
394
	 * @param string $name The name of the event
395
	 * @param string $type The type of the event
396
	 *
397
	 * @return void
398
	 */
399 1
	public function clearHandlers(string $name, string $type): void {
400 1
		unset($this->registrations[$name][$type]);
401
	}
402
	
403
	/**
404
	 * Returns all registered handlers as array(
405
	 * $name => array(
406
	 *     $type => array(
407
	 *         $priority => array(
408
	 *             callback,
409
	 *             callback,
410
	 *         )
411
	 *     )
412
	 * )
413
	 *
414
	 * @return array
415
	 * @internal
416
	 */
417 8
	public function getAllHandlers(): array {
418 8
		$ret = [];
419 8
		foreach ($this->registrations as $name => $types) {
420 8
			foreach ($types as $type => $registrations) {
421 8
				foreach ($registrations as $registration) {
422 8
					$priority = $registration[self::REG_KEY_PRIORITY];
423 8
					$ret[$name][$type][$priority][] = $registration[self::REG_KEY_HANDLER];
424
				}
425
			}
426
		}
427
		
428 8
		return $ret;
429
	}
430
	
431
	/**
432
	 * Is a handler registered for this specific name and type? "all" handlers are not considered.
433
	 *
434
	 * If you need to consider "all" handlers, you must check them independently, or use
435
	 * (bool) elgg()->events->getOrderedHandlers().
436
	 *
437
	 * @param string $name The name of the event
438
	 * @param string $type The type of the event
439
	 * @return boolean
440
	 */
441 2425
	public function hasHandler(string $name, string $type): bool {
442 2425
		return !empty($this->registrations[$name][$type]);
443
	}
444
	
445
	/**
446
	 * Returns an ordered array of handlers registered for $name and $type.
447
	 *
448
	 * @param string $name The name of the event
449
	 * @param string $type The type of the event
450
	 *
451
	 * @return callable[]
452
	 */
453 4900
	public function getOrderedHandlers(string $name, string $type): array {
454 4900
		$registrations = [];
455
		
456 4900
		if (!empty($this->registrations[$name][$type])) {
457 3359
			if ($name !== 'all' && $type !== 'all') {
458 3357
				array_splice($registrations, count($registrations), 0, $this->registrations[$name][$type]);
459
			}
460
		}
461
		
462 4900
		if (!empty($this->registrations['all'][$type])) {
463 1085
			if ($type !== 'all') {
464 4
				array_splice($registrations, count($registrations), 0, $this->registrations['all'][$type]);
465
			}
466
		}
467
		
468 4900
		if (!empty($this->registrations[$name]['all'])) {
469 1590
			if ($name !== 'all') {
470 1590
				array_splice($registrations, count($registrations), 0, $this->registrations[$name]['all']);
471
			}
472
		}
473
		
474 4900
		if (!empty($this->registrations['all']['all'])) {
475 3448
			array_splice($registrations, count($registrations), 0, $this->registrations['all']['all']);
476
		}
477
		
478 4900
		usort($registrations, function ($a, $b) {
479
			// priority first
480 3243
			if ($a[self::REG_KEY_PRIORITY] < $b[self::REG_KEY_PRIORITY]) {
481 3220
				return -1;
482
			}
483
			
484 3226
			if ($a[self::REG_KEY_PRIORITY] > $b[self::REG_KEY_PRIORITY]) {
485 3020
				return 1;
486
			}
487
			
488
			// then insertion order
489 3152
			return ($a[self::REG_KEY_INDEX] < $b[self::REG_KEY_INDEX]) ? -1 : 1;
490 4900
		});
491
			
492 4900
		$handlers = [];
493 4900
		foreach ($registrations as $registration) {
494 3604
			$handlers[] = $registration[self::REG_KEY_HANDLER];
495
		}
496
		
497 4900
		return $handlers;
498
	}
499
	
500
	/**
501
	 * Create a matcher for the given callable (if it's for a static or dynamic method)
502
	 *
503
	 * @param callable $spec Callable we're creating a matcher for
504
	 *
505
	 * @return MethodMatcher|null
506
	 */
507 2508
	protected function getMatcher($spec): ?MethodMatcher {
508 2508
		if (is_string($spec) && str_contains($spec, '::')) {
509 2331
			list ($type, $method) = explode('::', $spec, 2);
510 2331
			return new MethodMatcher($type, $method);
511
		}
512
		
513 182
		if (!is_array($spec) || empty($spec[0]) || empty($spec[1]) || !is_string($spec[1])) {
514 34
			return null;
515
		}
516
		
517 153
		if (is_object($spec[0])) {
518 148
			$spec[0] = get_class($spec[0]);
519
		}
520
		
521 153
		if (!is_string($spec[0])) {
522
			return null;
523
		}
524
		
525 153
		return new MethodMatcher($spec[0], $spec[1]);
526
	}
527
	
528
	/**
529
	 * Temporarily remove all event registrations (before tests)
530
	 *
531
	 * Call backup() before your tests and restore() after.
532
	 *
533
	 * @note This behaves like a stack. You must call restore() for each backup() call.
534
	 *
535
	 * @return void
536
	 */
537 459
	public function backup(): void {
538 459
		$this->backups[] = $this->registrations;
539 459
		$this->registrations = [];
540
	}
541
	
542
	/**
543
	 * Restore backed up event registrations (after tests)
544
	 *
545
	 * @return void
546
	 */
547 456
	public function restore(): void {
548 456
		$backup = array_pop($this->backups);
549 456
		if (is_array($backup)) {
550 451
			$this->registrations = $backup;
551
		}
552
	}
553
	
554
	/**
555
	 * Check if handlers are registered on a deprecated event. If so Display a message
556
	 *
557
	 * @param string $name    the name of the event
558
	 * @param string $type    the type of the event
559
	 * @param string $message The deprecation message
560
	 * @param string $version Human-readable *release* version: 1.9, 1.10, ...
561
	 *
562
	 * @return void
563
	 */
564 5
	protected function checkDeprecation(string $name, string $type, string $message, string $version): void {
565 5
		$message = trim($message);
566 5
		if (empty($message)) {
567
			return;
568
		}
569
		
570 5
		if (!$this->hasHandler($name, $type)) {
571 3
			return;
572
		}
573
		
574 2
		$this->logDeprecatedMessage($message, $version);
575
	}
576
	
577
	/**
578
	 * @param callable $callable Callable
579
	 * @param mixed    $event    Event object
580
	 * @param array    $args     Event arguments
581
	 * @param array    $options  (internal) options for triggering the event
582
	 *
583
	 * @return array [success, result, object]
584
	 */
585 3603
	protected function callHandler($callable, $event, array $args, array $options = []): array {
586
		// call a function before the actual callable
587 3603
		$begin_callback = elgg_extract(self::OPTION_BEGIN_CALLBACK, $options);
588 3603
		if (is_callable($begin_callback)) {
589 6
			call_user_func($begin_callback, [
1 ignored issue
show
Bug introduced by
It seems like $begin_callback can also be of type null; however, parameter $callback of call_user_func() does only seem to accept callable, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

589
			call_user_func(/** @scrutinizer ignore-type */ $begin_callback, [
Loading history...
590 6
				'callable' => $callable,
591 6
				'readable_callable' => $this->handlers->describeCallable($callable),
592 6
				'event' => $event,
593 6
				'arguments' => $args,
594 6
			]);
595
		}
596
		
597
		// time the callable function
598 3603
		$use_timer = (bool) elgg_extract(self::OPTION_USE_TIMER, $options, false);
599 3603
		$timer_keys = (array) elgg_extract(self::OPTION_TIMER_KEYS, $options, []);
600 3603
		if ($use_timer) {
601
			$timer_keys[] = $this->handlers->describeCallable($callable);
602
			$this->beginTimer($timer_keys);
603
		}
604
		
605
		// execute the callable function
606 3603
		$results = $this->handlers->call($callable, $event, $args);
607
		
608
		// end the timer
609 3599
		if ($use_timer) {
610
			$this->endTimer($timer_keys);
611
		}
612
		
613
		// call a function after the actual callable
614 3599
		$end_callback = elgg_extract(self::OPTION_END_CALLBACK, $options);
615 3599
		if (is_callable($end_callback)) {
616 6
			call_user_func($end_callback, [
617 6
				'callable' => $callable,
618 6
				'readable_callable' => $this->handlers->describeCallable($callable),
619 6
				'event' => $event,
620 6
				'arguments' => $args,
621 6
				'results' => $results,
622 6
			]);
623
		}
624
		
625 3599
		return $results;
626
	}
627
}
628