Elgg  Version master
EventsService.php
Go to the documentation of this file.
1 <?php
2 
3 namespace Elgg;
4 
9 
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  protected int $next_index = 0;
31 
35  protected array $registrations = [];
36 
37  protected array $backups = [];
38 
44  public function __construct(protected HandlersService $handlers) {
45  }
46 
61  public function trigger(string $name, string $type, $object = null, array $options = []): bool {
62  $options = array_merge([
63  self::OPTION_STOPPABLE => true,
64  ], $options);
65 
66  // allow for the profiling of system events (when enabled)
67  if ($this->hasTimer() && $type === 'system' && $name !== 'shutdown') {
68  $options[self::OPTION_USE_TIMER] = true;
69  $options[self::OPTION_TIMER_KEYS] = ["[{$name},{$type}]"];
70  }
71 
72  // get registered handlers
73  $handlers = $this->getOrderedHandlers($name, $type);
74 
75  // This starts as a string, but if a handler type-hints an object we convert it on-demand inside
76  // \Elgg\HandlersService::call and keep it alive during all handler calls. We do this because
77  // creating objects for every triggering is expensive.
78  /* @var $event Event|string */
79  $event = 'event';
80  $event_args = [
81  $name,
82  $type,
83  null,
84  [
85  'object' => $object,
86  '_elgg_sequence_id' => elgg_extract('_elgg_sequence_id', $options),
87  ],
88  ];
89  foreach ($handlers as $handler) {
90  list($success, $return, $event) = $this->callHandler($handler, $event, $event_args, $options);
91 
92  if (!$success) {
93  continue;
94  }
95 
96  if (!empty($options[self::OPTION_STOPPABLE]) && ($return === false)) {
97  return false;
98  }
99  }
100 
101  return true;
102  }
103 
117  public function triggerResults(string $name, string $type, array $params = [], $value = null, array $options = []) {
118  // This starts as a string, but if a handler type-hints an object we convert it on-demand inside
119  // \Elgg\HandlersService::call and keep it alive during all handler calls. We do this because
120  // creating objects for every triggering is expensive.
121  /* @var $event Event|string */
122  $event = 'event';
123  foreach ($this->getOrderedHandlers($name, $type) as $handler) {
124  $event_args = [$name, $type, $value, $params];
125 
126  list($success, $return, $event) = $this->callHandler($handler, $event, $event_args, $options);
127 
128  if (!$success) {
129  continue;
130  }
131 
132  if ($return !== null) {
133  $value = $return;
134  $event->setValue($value);
135  }
136  }
137 
138  return $value;
139  }
140 
160  public function triggerBefore(string $name, string $type, $object = null, array $options = []): bool {
161  return $this->trigger("{$name}:before", $type, $object, $options);
162  }
163 
182  public function triggerAfter(string $name, string $type, $object = null, array $options = []): void {
183  $options[self::OPTION_STOPPABLE] = false;
184 
185  $this->trigger("{$name}:after", $type, $object, $options);
186  }
187 
202  public function triggerSequence(string $name, string $type, $object = null, callable $callable = null, array $options = []): bool {
203  // generate a unique ID to identify this sequence
204  $options['_elgg_sequence_id'] = uniqid("{$name}{$type}", true);
205 
206  if (!$this->triggerBefore($name, $type, $object, $options)) {
207  return false;
208  }
209 
210  $result = $this->trigger($name, $type, $object, $options);
211  if ($result === false) {
212  return false;
213  }
214 
215  if ($callable) {
216  $result = call_user_func($callable, $object);
217  }
218 
219  if ($result !== false) {
220  $this->triggerAfter($name, $type, $object, $options);
221  }
222 
223  return $result;
224  }
225 
240  public function triggerResultsSequence(string $name, string $type, array $params = [], $value = null, callable $callable = null, array $options = []) {
241  // generate a unique ID to identify this sequence
242  $unique_id = uniqid("{$name}{$type}results", true);
243  $options['_elgg_sequence_id'] = $unique_id;
244  $params['_elgg_sequence_id'] = $unique_id;
245 
246  if (!$this->triggerBefore($name, $type, $params, $options)) {
247  return false;
248  }
249 
250  $result = $this->triggerResults($name, $type, $params, $value, $options);
251  if ($result === false) {
252  return false;
253  }
254 
255  if ($callable) {
256  $result = call_user_func($callable, $params);
257  }
258 
259  if ($result !== false) {
260  $this->triggerAfter($name, $type, $params, $options);
261  }
262 
263  return $result;
264  }
265 
280  public function triggerDeprecated(string $name, string $type, $object = null, string $message = '', string $version = '', array $options = []): bool {
281  $message = "The '{$name}', '{$type}' event is deprecated. {$message}";
282  $this->checkDeprecation($name, $type, $message, $version);
283 
284  return $this->trigger($name, $type, $object, $options);
285  }
286 
302  public function triggerDeprecatedResults(string $name, string $type, array $params = [], $returnvalue = null, string $message = '', string $version = '', array $options = []) {
303  $message = "The '{$name}', '{$type}' event is deprecated. {$message}";
304  $this->checkDeprecation($name, $type, $message, $version);
305 
306  return $this->triggerResults($name, $type, $params, $returnvalue, $options);
307  }
308 
322  public function registerHandler(string $name, string $type, $callback, int $priority = 500): bool {
323  if (empty($name) || empty($type) || !is_callable($callback, true)) {
324  return false;
325  }
326 
327  if (in_array($this->getLogger()->getLevel(false), [LogLevel::WARNING, LogLevel::NOTICE, LogLevel::INFO, LogLevel::DEBUG])) {
328  if (!$this->handlers->isCallable($callback)) {
329  $this->getLogger()->warning('Handler: ' . $this->handlers->describeCallable($callback) . ' is not callable');
330  }
331  }
332 
333  $this->registrations[$name][$type][] = [
334  self::REG_KEY_PRIORITY => $priority,
335  self::REG_KEY_INDEX => $this->next_index,
336  self::REG_KEY_HANDLER => $callback,
337  ];
338  $this->next_index++;
339 
340  return true;
341  }
342 
353  public function unregisterHandler(string $name, string $type, $callback): void {
354  if (empty($this->registrations[$name][$type])) {
355  return;
356  }
357 
358  $matcher = $this->getMatcher($callback);
359 
360  foreach ($this->registrations[$name][$type] as $i => $registration) {
361  if ($matcher instanceof MethodMatcher) {
362  if (!$matcher->matches($registration[self::REG_KEY_HANDLER])) {
363  continue;
364  }
365  } elseif ($registration[self::REG_KEY_HANDLER] != $callback) {
366  continue;
367  }
368 
369  unset($this->registrations[$name][$type][$i]);
370  return;
371  }
372  }
373 
382  public function clearHandlers(string $name, string $type): void {
383  unset($this->registrations[$name][$type]);
384  }
385 
400  public function getAllHandlers(): array {
401  $ret = [];
402  foreach ($this->registrations as $name => $types) {
403  foreach ($types as $type => $registrations) {
404  foreach ($registrations as $registration) {
405  $priority = $registration[self::REG_KEY_PRIORITY];
406  $ret[$name][$type][$priority][] = $registration[self::REG_KEY_HANDLER];
407  }
408  }
409  }
410 
411  return $ret;
412  }
413 
424  public function hasHandler(string $name, string $type): bool {
425  return !empty($this->registrations[$name][$type]);
426  }
427 
436  public function getOrderedHandlers(string $name, string $type): array {
437  $registrations = [];
438 
439  if (!empty($this->registrations[$name][$type])) {
440  if ($name !== 'all' && $type !== 'all') {
441  array_splice($registrations, count($registrations), 0, $this->registrations[$name][$type]);
442  }
443  }
444 
445  if (!empty($this->registrations['all'][$type])) {
446  if ($type !== 'all') {
447  array_splice($registrations, count($registrations), 0, $this->registrations['all'][$type]);
448  }
449  }
450 
451  if (!empty($this->registrations[$name]['all'])) {
452  if ($name !== 'all') {
453  array_splice($registrations, count($registrations), 0, $this->registrations[$name]['all']);
454  }
455  }
456 
457  if (!empty($this->registrations['all']['all'])) {
458  array_splice($registrations, count($registrations), 0, $this->registrations['all']['all']);
459  }
460 
461  usort($registrations, function ($a, $b) {
462  // priority first
463  if ($a[self::REG_KEY_PRIORITY] < $b[self::REG_KEY_PRIORITY]) {
464  return -1;
465  }
466 
467  if ($a[self::REG_KEY_PRIORITY] > $b[self::REG_KEY_PRIORITY]) {
468  return 1;
469  }
470 
471  // then insertion order
472  return ($a[self::REG_KEY_INDEX] < $b[self::REG_KEY_INDEX]) ? -1 : 1;
473  });
474 
475  $handlers = [];
476  foreach ($registrations as $registration) {
477  $handlers[] = $registration[self::REG_KEY_HANDLER];
478  }
479 
480  return $handlers;
481  }
482 
490  protected function getMatcher($spec): ?MethodMatcher {
491  if (is_string($spec) && str_contains($spec, '::')) {
492  list ($type, $method) = explode('::', $spec, 2);
493  return new MethodMatcher($type, $method);
494  }
495 
496  if (!is_array($spec) || empty($spec[0]) || empty($spec[1]) || !is_string($spec[1])) {
497  return null;
498  }
499 
500  if (is_object($spec[0])) {
501  $spec[0] = get_class($spec[0]);
502  }
503 
504  if (!is_string($spec[0])) {
505  return null;
506  }
507 
508  return new MethodMatcher($spec[0], $spec[1]);
509  }
510 
520  public function backup(): void {
521  $this->backups[] = $this->registrations;
522  $this->registrations = [];
523  }
524 
530  public function restore(): void {
531  $backup = array_pop($this->backups);
532  if (is_array($backup)) {
533  $this->registrations = $backup;
534  }
535  }
536 
547  protected function checkDeprecation(string $name, string $type, string $message, string $version): void {
548  $message = trim($message);
549  if (empty($message)) {
550  return;
551  }
552 
553  if (!$this->hasHandler($name, $type)) {
554  return;
555  }
556 
557  $this->logDeprecatedMessage($message, $version);
558  }
559 
568  protected function callHandler($callable, $event, array $args, array $options = []): array {
569  // call a function before the actual callable
570  $begin_callback = elgg_extract(self::OPTION_BEGIN_CALLBACK, $options);
571  if (is_callable($begin_callback)) {
572  call_user_func($begin_callback, [
573  'callable' => $callable,
574  'readable_callable' => $this->handlers->describeCallable($callable),
575  'event' => $event,
576  'arguments' => $args,
577  ]);
578  }
579 
580  // time the callable function
581  $use_timer = (bool) elgg_extract(self::OPTION_USE_TIMER, $options, false);
582  $timer_keys = (array) elgg_extract(self::OPTION_TIMER_KEYS, $options, []);
583  if ($use_timer) {
584  $timer_keys[] = $this->handlers->describeCallable($callable);
585  $this->beginTimer($timer_keys);
586  }
587 
588  // execute the callable function
589  $results = $this->handlers->call($callable, $event, $args);
590 
591  // end the timer
592  if ($use_timer) {
593  $this->endTimer($timer_keys);
594  }
595 
596  // call a function after the actual callable
597  $end_callback = elgg_extract(self::OPTION_END_CALLBACK, $options);
598  if (is_callable($end_callback)) {
599  call_user_func($end_callback, [
600  'callable' => $callable,
601  'readable_callable' => $this->handlers->describeCallable($callable),
602  'event' => $event,
603  'arguments' => $args,
604  'results' => $results,
605  ]);
606  }
607 
608  return $results;
609  }
610 }
trait Profilable
Make an object accept a timer.
Definition: Profilable.php:12
$params
Saves global plugin settings.
Definition: save.php:13
registerHandler(string $name, string $type, $callback, int $priority=500)
Register a callback as a event handler.
Helpers for providing callable-based APIs.
hasHandler(string $name, string $type)
Is a handler registered for this specific name and type? "all" handlers are not considered.
if(!$user||!$user->canDelete()) $name
Definition: delete.php:22
triggerAfter(string $name, string $type, $object=null, array $options=[])
Trigger an "After event" indicating a process has finished.
$version
c Accompany it with the information you received as to the offer to distribute corresponding source complete source code means all the source code for all modules it plus any associated interface definition plus the scripts used to control compilation and installation of the executable as a special the source code distributed need not include anything that is normally and so on of the operating system on which the executable unless that component itself accompanies the executable If distribution of executable or object code is made by offering access to copy from a designated then offering equivalent access to copy the source code from the same place counts as distribution of the source even though third parties are not compelled to copy the source along with the object code You may not or distribute the Program except as expressly provided under this License Any attempt otherwise to sublicense or distribute the Program is void
Definition: LICENSE.txt:215
Events service.
getAllHandlers()
Returns all registered handlers as array( $name => array( $type => array( $priority => array( callbac...
$type
Definition: delete.php:21
$args
Some servers don&#39;t allow PHP to check the rewrite, so try via AJAX.
hasTimer()
Has a timer been set.
Definition: Profilable.php:31
triggerBefore(string $name, string $type, $object=null, array $options=[])
Trigger a "Before event" indicating a process is about to begin.
if($item instanceof\ElggEntity) elseif($item instanceof\ElggRiverItem) elseif($item instanceof\ElggRelationship) elseif(is_callable([$item, 'getType']))
Definition: item.php:48
$value
Definition: generic.php:51
elgg_extract($key, $array, $default=null, bool $strict=true)
Checks for $array[$key] and returns its value if it exists, else returns $default.
Definition: elgglib.php:254
if($who_can_change_language=== 'nobody') elseif($who_can_change_language=== 'admin_only'&&!elgg_is_admin_logged_in()) $options
Definition: language.php:20
trait Loggable
Enables adding a logger.
Definition: Loggable.php:14
triggerDeprecated(string $name, string $type, $object=null, string $message= '', string $version= '', array $options=[])
Trigger an event sequence normally, but send a notice about deprecated use if any handlers are regist...
triggerDeprecatedResults(string $name, string $type, array $params=[], $returnvalue=null, string $message= '', string $version= '', array $options=[])
Trigger an event sequence normally, but send a notice about deprecated use if any handlers are regist...
trigger(string $name, string $type, $object=null, array $options=[])
Triggers an Elgg event.
triggerResults(string $name, string $type, array $params=[], $value=null, array $options=[])
Triggers a event that is allowed to return a mixed result.
triggerResultsSequence(string $name, string $type, array $params=[], $value=null, callable $callable=null, array $options=[])
Trigger an sequence of <event>:before, <event>, and <event>:after handlers.
getOrderedHandlers(string $name, string $type)
Returns an ordered array of handlers registered for $name and $type.
restore()
Restore backed up event registrations (after tests)
$results
Definition: content.php:22
triggerSequence(string $name, string $type, $object=null, callable $callable=null, array $options=[])
Trigger a sequence of <event>:before, <event>, and <event>:after handlers.
__construct(protected HandlersService $handlers)
Constructor.
getLogger()
Returns logger.
Definition: Loggable.php:37
if($email instanceof\Elgg\Email) $object
Definition: body.php:24
Identify a static/dynamic method callable, even if contains an object to which you don&#39;t have a refer...
beginTimer(array $keys)
Start the timer (when enabled)
Definition: Profilable.php:43
backup()
Temporarily remove all event registrations (before tests)
callHandler($callable, $event, array $args, array $options=[])
checkDeprecation(string $name, string $type, string $message, string $version)
Check if handlers are registered on a deprecated event.
unregisterHandler(string $name, string $type, $callback)
Unregister a callback as an event handler.
$handler
Definition: add.php:7
getMatcher($spec)
Create a matcher for the given callable (if it&#39;s for a static or dynamic method)
clearHandlers(string $name, string $type)
Clears all callback registrations for an event.
$priority
endTimer(array $keys)
Ends the timer (when enabled)
Definition: Profilable.php:59
logDeprecatedMessage(string $message, string $version)
Sends a message about deprecated use of a function, view, etc.
Definition: Loggable.php:76