Elgg  Version 6.3
EventsService.php
Go to the documentation of this file.
1 <?php
2 
3 namespace Elgg;
4 
7 use Elgg\Traits\Loggable;
8 use Psr\Log\LogLevel;
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  const OPTION_CONTINUE_ON_EXCEPTION = 'continue_on_exception';
30 
31  protected int $next_index = 0;
32 
33  protected array $ordered_handlers_cache = [];
34 
38  protected array $registrations = [];
39 
40  protected array $backups = [];
41 
47  public function __construct(protected HandlersService $handlers) {
48  }
49 
65  public function trigger(string $name, string $type, $object = null, array $options = []): bool {
66  $options = array_merge([
67  self::OPTION_STOPPABLE => true,
68  ], $options);
69 
70  // allow for the profiling of system events (when enabled)
71  if ($this->hasTimer() && $type === 'system' && $name !== 'shutdown') {
72  $options[self::OPTION_USE_TIMER] = true;
73  $options[self::OPTION_TIMER_KEYS] = ["[{$name},{$type}]"];
74  }
75 
76  // get registered handlers
77  $handlers = $this->getOrderedHandlers($name, $type);
78 
79  // This starts as a string, but if a handler type-hints an object we convert it on-demand inside
80  // \Elgg\HandlersService::call and keep it alive during all handler calls. We do this because
81  // creating objects for every triggering is expensive.
82  /* @var $event Event|string */
83  $event = 'event';
84  $event_args = [
85  $name,
86  $type,
87  null,
88  [
89  'object' => $object,
90  '_elgg_sequence_id' => elgg_extract('_elgg_sequence_id', $options),
91  ],
92  ];
93  foreach ($handlers as $handler) {
94  try {
95  list($success, $return, $event) = $this->callHandler($handler, $event, $event_args, $options);
96 
97  if (!$success) {
98  continue;
99  }
100 
101  if (!empty($options[self::OPTION_STOPPABLE]) && ($return === false)) {
102  return false;
103  }
104  } catch (\Throwable $t) {
105  if (!empty($options[self::OPTION_CONTINUE_ON_EXCEPTION])) {
106  $handler_string = $this->handlers->describeCallable($handler);
107 
108  $this->getLogger()->error("Callback '{$handler_string}' for the event '{$name}', '{$type}' caused an exception: {$t->getMessage()}");
109  continue;
110  }
111 
112  throw $t;
113  }
114  }
115 
116  return true;
117  }
118 
133  public function triggerResults(string $name, string $type, array $params = [], $value = null, array $options = []) {
134  // This starts as a string, but if a handler type-hints an object we convert it on-demand inside
135  // \Elgg\HandlersService::call and keep it alive during all handler calls. We do this because
136  // creating objects for every triggering is expensive.
137  /* @var $event Event|string */
138  $event = 'event';
139  foreach ($this->getOrderedHandlers($name, $type) as $handler) {
140  try {
141  $event_args = [$name, $type, $value, $params];
142 
143  list($success, $return, $event) = $this->callHandler($handler, $event, $event_args, $options);
144 
145  if (!$success) {
146  continue;
147  }
148 
149  if ($return !== null) {
150  $value = $return;
151  $event->setValue($value);
152  }
153  } catch (\Throwable $t) {
154  if (!empty($options[self::OPTION_CONTINUE_ON_EXCEPTION])) {
155  $handler_string = $this->handlers->describeCallable($handler);
156 
157  $this->getLogger()->error("Callback '{$handler_string}' for the event '{$name}', '{$type}' caused an exception: {$t->getMessage()}");
158  continue;
159  }
160 
161  throw $t;
162  }
163  }
164 
165  return $value;
166  }
167 
187  public function triggerBefore(string $name, string $type, $object = null, array $options = []): bool {
188  return $this->trigger("{$name}:before", $type, $object, $options);
189  }
190 
209  public function triggerAfter(string $name, string $type, $object = null, array $options = []): void {
210  $options[self::OPTION_STOPPABLE] = false;
211 
212  $this->trigger("{$name}:after", $type, $object, $options);
213  }
214 
229  public function triggerSequence(string $name, string $type, $object = null, ?callable $callable = null, array $options = []): bool {
230  // generate a unique ID to identify this sequence
231  $options['_elgg_sequence_id'] = uniqid("{$name}{$type}", true);
232 
233  if (!$this->triggerBefore($name, $type, $object, $options)) {
234  return false;
235  }
236 
237  $result = $this->trigger($name, $type, $object, $options);
238  if ($result === false) {
239  return false;
240  }
241 
242  if ($callable) {
243  $result = call_user_func($callable, $object);
244  }
245 
246  if ($result !== false) {
247  $this->triggerAfter($name, $type, $object, $options);
248  }
249 
250  return $result;
251  }
252 
267  public function triggerResultsSequence(string $name, string $type, array $params = [], $value = null, ?callable $callable = null, array $options = []) {
268  // generate a unique ID to identify this sequence
269  $unique_id = uniqid("{$name}{$type}results", true);
270  $options['_elgg_sequence_id'] = $unique_id;
271  $params['_elgg_sequence_id'] = $unique_id;
272 
273  if (!$this->triggerBefore($name, $type, $params, $options)) {
274  return false;
275  }
276 
277  $result = $this->triggerResults($name, $type, $params, $value, $options);
278  if ($result === false) {
279  return false;
280  }
281 
282  if ($callable) {
283  $result = call_user_func($callable, $params);
284  }
285 
286  if ($result !== false) {
287  $this->triggerAfter($name, $type, $params, $options);
288  }
289 
290  return $result;
291  }
292 
307  public function triggerDeprecated(string $name, string $type, $object = null, string $message = '', string $version = '', array $options = []): bool {
308  $message = "The '{$name}', '{$type}' event is deprecated. {$message}";
309  $this->checkDeprecation($name, $type, $message, $version);
310 
311  return $this->trigger($name, $type, $object, $options);
312  }
313 
329  public function triggerDeprecatedResults(string $name, string $type, array $params = [], $returnvalue = null, string $message = '', string $version = '', array $options = []) {
330  $message = "The '{$name}', '{$type}' event is deprecated. {$message}";
331  $this->checkDeprecation($name, $type, $message, $version);
332 
333  return $this->triggerResults($name, $type, $params, $returnvalue, $options);
334  }
335 
349  public function registerHandler(string $name, string $type, $callback, int $priority = 500): bool {
350  if (empty($name) || empty($type) || !is_callable($callback, true)) {
351  return false;
352  }
353 
354  if (in_array($this->getLogger()->getLevel(false), [LogLevel::WARNING, LogLevel::NOTICE, LogLevel::INFO, LogLevel::DEBUG])) {
355  if (!$this->handlers->isCallable($callback)) {
356  $this->getLogger()->warning('Handler: ' . $this->handlers->describeCallable($callback) . ' is not callable');
357  }
358  }
359 
360  $this->registrations[$name][$type]["{$priority}_{$this->next_index}"] = [
361  self::REG_KEY_PRIORITY => $priority,
362  self::REG_KEY_INDEX => $this->next_index,
363  self::REG_KEY_HANDLER => $callback,
364  ];
365  $this->next_index++;
366 
367  unset($this->ordered_handlers_cache);
368 
369  return true;
370  }
371 
382  public function unregisterHandler(string $name, string $type, $callback): void {
383  if (empty($this->registrations[$name][$type])) {
384  return;
385  }
386 
387  $matcher = $this->getMatcher($callback);
388 
389  foreach ($this->registrations[$name][$type] as $i => $registration) {
390  if ($matcher instanceof MethodMatcher) {
391  if (!$matcher->matches($registration[self::REG_KEY_HANDLER])) {
392  continue;
393  }
394  } elseif ($registration[self::REG_KEY_HANDLER] != $callback) {
395  continue;
396  }
397 
398  unset($this->registrations[$name][$type][$i]);
399  unset($this->ordered_handlers_cache);
400 
401  return;
402  }
403  }
404 
413  public function clearHandlers(string $name, string $type): void {
414  unset($this->registrations[$name][$type]);
415  unset($this->ordered_handlers_cache);
416  }
417 
432  public function getAllHandlers(): array {
433  $ret = [];
434  foreach ($this->registrations as $name => $types) {
435  foreach ($types as $type => $registrations) {
436  foreach ($registrations as $registration) {
437  $priority = $registration[self::REG_KEY_PRIORITY];
438  $ret[$name][$type][$priority][] = $registration[self::REG_KEY_HANDLER];
439  }
440  }
441  }
442 
443  return $ret;
444  }
445 
456  public function hasHandler(string $name, string $type): bool {
457  return !empty($this->registrations[$name][$type]);
458  }
459 
468  public function getOrderedHandlers(string $name, string $type): array {
469  $registrations = [];
470 
471  if (isset($this->ordered_handlers_cache[$name . $type])) {
472  return $this->ordered_handlers_cache[$name . $type];
473  }
474 
475  if (!empty($this->registrations[$name][$type])) {
476  if ($name !== 'all' && $type !== 'all') {
477  $registrations = $this->registrations[$name][$type];
478  }
479  }
480 
481  if (!empty($this->registrations['all'][$type])) {
482  if ($type !== 'all') {
483  $registrations += $this->registrations['all'][$type];
484  }
485  }
486 
487  if (!empty($this->registrations[$name]['all'])) {
488  if ($name !== 'all') {
489  $registrations += $this->registrations[$name]['all'];
490  }
491  }
492 
493  if (!empty($this->registrations['all']['all'])) {
494  $registrations += $this->registrations['all']['all'];
495  }
496 
497  ksort($registrations, SORT_NATURAL);
498 
499  $handlers = [];
500  foreach ($registrations as $registration) {
501  $handlers[] = $registration[self::REG_KEY_HANDLER];
502  }
503 
504  $this->ordered_handlers_cache[$name . $type] = $handlers;
505 
506  return $handlers;
507  }
508 
516  protected function getMatcher($spec): ?MethodMatcher {
517  if (is_string($spec) && str_contains($spec, '::')) {
518  list ($type, $method) = explode('::', $spec, 2);
519  return new MethodMatcher($type, $method);
520  }
521 
522  if (!is_array($spec) || empty($spec[0]) || empty($spec[1]) || !is_string($spec[1])) {
523  return null;
524  }
525 
526  if (is_object($spec[0])) {
527  $spec[0] = get_class($spec[0]);
528  }
529 
530  if (!is_string($spec[0])) {
531  return null;
532  }
533 
534  return new MethodMatcher($spec[0], $spec[1]);
535  }
536 
546  public function backup(): void {
547  $this->backups[] = $this->registrations;
548  $this->registrations = [];
549  unset($this->ordered_handlers_cache);
550  }
551 
557  public function restore(): void {
558  $backup = array_pop($this->backups);
559  if (is_array($backup)) {
560  $this->registrations = $backup;
561  }
562 
563  unset($this->ordered_handlers_cache);
564  }
565 
576  protected function checkDeprecation(string $name, string $type, string $message, string $version): void {
577  $message = trim($message);
578  if (empty($message)) {
579  return;
580  }
581 
582  if (!$this->hasHandler($name, $type)) {
583  return;
584  }
585 
586  $this->logDeprecatedMessage($message, $version);
587  }
588 
597  protected function callHandler($callable, $event, array $args, array $options = []): array {
598  // call a function before the actual callable
599  $begin_callback = elgg_extract(self::OPTION_BEGIN_CALLBACK, $options);
600  if (is_callable($begin_callback)) {
601  call_user_func($begin_callback, [
602  'callable' => $callable,
603  'readable_callable' => $this->handlers->describeCallable($callable),
604  'event' => $event,
605  'arguments' => $args,
606  ]);
607  }
608 
609  // time the callable function
610  $use_timer = (bool) elgg_extract(self::OPTION_USE_TIMER, $options, false);
611  $timer_keys = (array) elgg_extract(self::OPTION_TIMER_KEYS, $options, []);
612  if ($use_timer) {
613  $timer_keys[] = $this->handlers->describeCallable($callable);
614  $this->beginTimer($timer_keys);
615  }
616 
617  // execute the callable function
618  $results = $this->handlers->call($callable, $event, $args);
619 
620  // end the timer
621  if ($use_timer) {
622  $this->endTimer($timer_keys);
623  }
624 
625  // call a function after the actual callable
626  $end_callback = elgg_extract(self::OPTION_END_CALLBACK, $options);
627  if (is_callable($end_callback)) {
628  call_user_func($end_callback, [
629  'callable' => $callable,
630  'readable_callable' => $this->handlers->describeCallable($callable),
631  'event' => $event,
632  'arguments' => $args,
633  'results' => $results,
634  ]);
635  }
636 
637  return $results;
638  }
639 }
getLogger()
Returns logger.
Definition: Loggable.php:37
if(! $user||! $user->canDelete()) $name
Definition: delete.php:22
$type
Definition: delete.php:21
$params
Saves global plugin settings.
Definition: save.php:13
$handler
Definition: add.php:7
return[ 'admin/delete_admin_notices'=>['access'=> 'admin'], 'admin/menu/save'=>['access'=> 'admin'], 'admin/plugins/activate'=>['access'=> 'admin'], 'admin/plugins/activate_all'=>['access'=> 'admin'], 'admin/plugins/deactivate'=>['access'=> 'admin'], 'admin/plugins/deactivate_all'=>['access'=> 'admin'], 'admin/plugins/set_priority'=>['access'=> 'admin'], 'admin/security/security_txt'=>['access'=> 'admin'], 'admin/security/settings'=>['access'=> 'admin'], 'admin/security/regenerate_site_secret'=>['access'=> 'admin'], 'admin/site/cache/invalidate'=>['access'=> 'admin'], 'admin/site/flush_cache'=>['access'=> 'admin'], 'admin/site/icons'=>['access'=> 'admin'], 'admin/site/set_maintenance_mode'=>['access'=> 'admin'], 'admin/site/set_robots'=>['access'=> 'admin'], 'admin/site/theme'=>['access'=> 'admin'], 'admin/site/unlock_upgrade'=>['access'=> 'admin'], 'admin/site/settings'=>['access'=> 'admin'], 'admin/upgrade'=>['access'=> 'admin'], 'admin/upgrade/reset'=>['access'=> 'admin'], 'admin/user/ban'=>['access'=> 'admin'], 'admin/user/bulk/ban'=>['access'=> 'admin'], 'admin/user/bulk/delete'=>['access'=> 'admin'], 'admin/user/bulk/unban'=>['access'=> 'admin'], 'admin/user/bulk/validate'=>['access'=> 'admin'], 'admin/user/change_email'=>['access'=> 'admin'], 'admin/user/delete'=>['access'=> 'admin'], 'admin/user/login_as'=>['access'=> 'admin'], 'admin/user/logout_as'=>[], 'admin/user/makeadmin'=>['access'=> 'admin'], 'admin/user/resetpassword'=>['access'=> 'admin'], 'admin/user/removeadmin'=>['access'=> 'admin'], 'admin/user/unban'=>['access'=> 'admin'], 'admin/user/validate'=>['access'=> 'admin'], 'annotation/delete'=>[], 'avatar/upload'=>[], 'comment/save'=>[], 'diagnostics/download'=>['access'=> 'admin'], 'entity/chooserestoredestination'=>[], 'entity/delete'=>[], 'entity/mute'=>[], 'entity/restore'=>[], 'entity/subscribe'=>[], 'entity/trash'=>[], 'entity/unmute'=>[], 'entity/unsubscribe'=>[], 'login'=>['access'=> 'logged_out'], 'logout'=>[], 'notifications/mute'=>['access'=> 'public'], 'plugins/settings/remove'=>['access'=> 'admin'], 'plugins/settings/save'=>['access'=> 'admin'], 'plugins/usersettings/save'=>[], 'register'=>['access'=> 'logged_out', 'middleware'=>[\Elgg\Router\Middleware\RegistrationAllowedGatekeeper::class,],], 'river/delete'=>[], 'settings/notifications'=>[], 'settings/notifications/subscriptions'=>[], 'user/changepassword'=>['access'=> 'public'], 'user/requestnewpassword'=>['access'=> 'public'], 'useradd'=>['access'=> 'admin'], 'usersettings/save'=>[], 'widgets/add'=>[], 'widgets/delete'=>[], 'widgets/move'=>[], 'widgets/save'=>[],]
Definition: actions.php:73
Identify a static/dynamic method callable, even if contains an object to which you don't have a refer...
Events service.
unregisterHandler(string $name, string $type, $callback)
Unregister a callback as an event handler.
callHandler($callable, $event, array $args, array $options=[])
__construct(protected HandlersService $handlers)
Constructor.
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...
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...
getAllHandlers()
Returns all registered handlers as array( $name => array( $type => array( $priority => array( callbac...
triggerSequence(string $name, string $type, $object=null, ?callable $callable=null, array $options=[])
Trigger a sequence of <event>:before, <event>, and <event>:after handlers.
getMatcher($spec)
Create a matcher for the given callable (if it's for a static or dynamic method)
triggerResults(string $name, string $type, array $params=[], $value=null, array $options=[])
Trigger an event allowed to return a mixed result.
registerHandler(string $name, string $type, $callback, int $priority=500)
Register a callback as a event handler.
trigger(string $name, string $type, $object=null, array $options=[])
Trigger an Elgg event.
restore()
Restore backed up event registrations (after tests)
checkDeprecation(string $name, string $type, string $message, string $version)
Check if handlers are registered on a deprecated event.
triggerAfter(string $name, string $type, $object=null, array $options=[])
Trigger an "After event" indicating a process has finished.
getOrderedHandlers(string $name, string $type)
Returns an ordered array of handlers registered for $name and $type.
clearHandlers(string $name, string $type)
Clears all callback registrations for an event.
backup()
Temporarily remove all event registrations (before tests)
hasHandler(string $name, string $type)
Is a handler registered for this specific name and type? "all" handlers are not considered.
triggerResultsSequence(string $name, string $type, array $params=[], $value=null, ?callable $callable=null, array $options=[])
Trigger a sequence of <event>:before, <event>, and <event>:after handlers.
triggerBefore(string $name, string $type, $object=null, array $options=[])
Trigger a "Before event" indicating a process is about to begin.
Helpers for providing callable-based APIs.
if($who_can_change_language==='nobody') elseif($who_can_change_language==='admin_only' &&!elgg_is_admin_logged_in()) $options
Definition: language.php:20
$version
if($email instanceof \Elgg\Email) $object
Definition: body.php:24
if($item instanceof \ElggEntity) elseif($item instanceof \ElggRiverItem) elseif($item instanceof \ElggRelationship) elseif(is_callable([ $item, 'getType']))
Definition: item.php:48
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:240
$value
Definition: generic.php:51
$args
Some servers don't allow PHP to check the rewrite, so try via AJAX.
endTimer(array $keys)
Ends the timer (when enabled)
Definition: Profilable.php:59
trait Profilable
Make an object accept a timer.
Definition: Profilable.php:12
hasTimer()
Has a timer been set.
Definition: Profilable.php:31
beginTimer(array $keys)
Start the timer (when enabled)
Definition: Profilable.php:43
if(parse_url(elgg_get_site_url(), PHP_URL_PATH) !=='/') if(file_exists(elgg_get_root_path() . 'robots.txt'))
Set robots.txt.
Definition: robots.php:10
$priority
$results
Definition: content.php:22