Elgg  Version master
EventsService.php
Go to the documentation of this file.
1 <?php
2 
3 namespace Elgg;
4 
8 use Elgg\Traits\Loggable;
9 use Psr\Log\LogLevel;
10 
17 
18  use Loggable;
19  use Profilable;
20 
21  const REG_KEY_PRIORITY = 0;
22  const REG_KEY_INDEX = 1;
23  const REG_KEY_HANDLER = 2;
24 
25  const OPTION_STOPPABLE = 'stoppable';
26  const OPTION_USE_TIMER = 'use_timer';
27  const OPTION_TIMER_KEYS = 'timer_keys';
28  const OPTION_BEGIN_CALLBACK = 'begin_callback';
29  const OPTION_END_CALLBACK = 'end_callback';
30  const OPTION_CONTINUE_ON_EXCEPTION = 'continue_on_exception';
31 
32  protected int $next_index = 0;
33 
34  protected array $ordered_handlers_cache = [];
35 
39  protected array $registrations = [];
40 
41  protected array $backups = [];
42 
48  public function __construct(protected HandlersService $handlers) {
49  }
50 
66  public function trigger(string $name, string $type, $object = null, array $options = []): bool {
67  $options = array_merge([
68  self::OPTION_STOPPABLE => true,
69  ], $options);
70 
71  // allow for the profiling of system events (when enabled)
72  if ($this->hasTimer() && $type === 'system' && $name !== 'shutdown') {
73  $options[self::OPTION_USE_TIMER] = true;
74  $options[self::OPTION_TIMER_KEYS] = ["[{$name},{$type}]"];
75  }
76 
77  // get registered handlers
78  $handlers = $this->getOrderedHandlers($name, $type);
79 
80  // This starts as a string, but if a handler type-hints an object we convert it on-demand inside
81  // \Elgg\HandlersService::call and keep it alive during all handler calls. We do this because
82  // creating objects for every triggering is expensive.
83  /* @var $event Event|string */
84  $event = 'event';
85  $event_args = [
86  $name,
87  $type,
88  null,
89  [
90  'object' => $object,
91  '_elgg_sequence_id' => elgg_extract('_elgg_sequence_id', $options),
92  ],
93  ];
94  foreach ($handlers as $handler) {
95  try {
96  list($success, $return, $event) = $this->callHandler($handler, $event, $event_args, $options);
97 
98  if (!$success) {
99  continue;
100  }
101 
102  if (!empty($options[self::OPTION_STOPPABLE]) && ($return === false)) {
103  return false;
104  }
105  } catch (\Throwable $t) {
106  if (!empty($options[self::OPTION_CONTINUE_ON_EXCEPTION])) {
107  $handler_string = $this->handlers->describeCallable($handler);
108 
109  $this->getLogger()->error("Callback '{$handler_string}' for the event '{$name}', '{$type}' caused an exception: {$t->getMessage()}");
110  continue;
111  }
112 
113  throw $t;
114  }
115  }
116 
117  return true;
118  }
119 
134  public function triggerResults(string $name, string $type, array $params = [], $value = null, array $options = []) {
135  // This starts as a string, but if a handler type-hints an object we convert it on-demand inside
136  // \Elgg\HandlersService::call and keep it alive during all handler calls. We do this because
137  // creating objects for every triggering is expensive.
138  /* @var $event Event|string */
139  $event = 'event';
140  foreach ($this->getOrderedHandlers($name, $type) as $handler) {
141  try {
142  $event_args = [$name, $type, $value, $params];
143 
144  list($success, $return, $event) = $this->callHandler($handler, $event, $event_args, $options);
145 
146  if (!$success) {
147  continue;
148  }
149 
150  if ($return !== null) {
151  $value = $return;
152  $event->setValue($value);
153  }
154  } catch (\Throwable $t) {
155  if (!empty($options[self::OPTION_CONTINUE_ON_EXCEPTION])) {
156  $handler_string = $this->handlers->describeCallable($handler);
157 
158  $this->getLogger()->error("Callback '{$handler_string}' for the event '{$name}', '{$type}' caused an exception: {$t->getMessage()}");
159  continue;
160  }
161 
162  throw $t;
163  }
164  }
165 
166  return $value;
167  }
168 
188  public function triggerBefore(string $name, string $type, $object = null, array $options = []): bool {
189  return $this->trigger("{$name}:before", $type, $object, $options);
190  }
191 
210  public function triggerAfter(string $name, string $type, $object = null, array $options = []): void {
211  $options[self::OPTION_STOPPABLE] = false;
212 
213  $this->trigger("{$name}:after", $type, $object, $options);
214  }
215 
230  public function triggerSequence(string $name, string $type, $object = null, ?callable $callable = null, array $options = []): bool {
231  // generate a unique ID to identify this sequence
232  $options['_elgg_sequence_id'] = uniqid("{$name}{$type}", true);
233 
234  if (!$this->triggerBefore($name, $type, $object, $options)) {
235  return false;
236  }
237 
238  $result = $this->trigger($name, $type, $object, $options);
239  if ($result === false) {
240  return false;
241  }
242 
243  if ($callable) {
244  $result = call_user_func($callable, $object);
245  }
246 
247  if ($result !== false) {
248  $this->triggerAfter($name, $type, $object, $options);
249  }
250 
251  return $result;
252  }
253 
268  public function triggerResultsSequence(string $name, string $type, array $params = [], $value = null, ?callable $callable = null, array $options = []) {
269  // generate a unique ID to identify this sequence
270  $unique_id = uniqid("{$name}{$type}results", true);
271  $options['_elgg_sequence_id'] = $unique_id;
272  $params['_elgg_sequence_id'] = $unique_id;
273 
274  if (!$this->triggerBefore($name, $type, $params, $options)) {
275  return false;
276  }
277 
278  $result = $this->triggerResults($name, $type, $params, $value, $options);
279  if ($result === false) {
280  return false;
281  }
282 
283  if ($callable) {
284  $result = call_user_func($callable, $params);
285  }
286 
287  if ($result !== false) {
288  $this->triggerAfter($name, $type, $params, $options);
289  }
290 
291  return $result;
292  }
293 
308  public function triggerDeprecated(string $name, string $type, $object = null, string $message = '', string $version = '', array $options = []): bool {
309  $message = "The '{$name}', '{$type}' event is deprecated. {$message}";
310  $this->checkDeprecation($name, $type, $message, $version);
311 
312  return $this->trigger($name, $type, $object, $options);
313  }
314 
330  public function triggerDeprecatedResults(string $name, string $type, array $params = [], $returnvalue = null, string $message = '', string $version = '', array $options = []) {
331  $message = "The '{$name}', '{$type}' event is deprecated. {$message}";
332  $this->checkDeprecation($name, $type, $message, $version);
333 
334  return $this->triggerResults($name, $type, $params, $returnvalue, $options);
335  }
336 
350  public function registerHandler(string $name, string $type, $callback, int $priority = 500): void {
351  if (empty($name) || empty($type)) {
352  throw new InvalidArgumentException('$name and $type cannot be empty');
353  }
354 
355  if (!is_callable($callback, true)) {
356  throw new InvalidArgumentException('$callback must be a callable');
357  }
358 
359  if (in_array($this->getLogger()->getLevel(false), [LogLevel::WARNING, LogLevel::NOTICE, LogLevel::INFO, LogLevel::DEBUG])) {
360  if (!$this->handlers->isCallable($callback)) {
361  $this->getLogger()->warning('Handler: ' . $this->handlers->describeCallable($callback) . ' is not callable');
362  }
363  }
364 
365  $this->registrations[$name][$type]["{$priority}_{$this->next_index}"] = [
366  self::REG_KEY_PRIORITY => $priority,
367  self::REG_KEY_INDEX => $this->next_index,
368  self::REG_KEY_HANDLER => $callback,
369  ];
370  $this->next_index++;
371 
372  unset($this->ordered_handlers_cache);
373  }
374 
385  public function unregisterHandler(string $name, string $type, $callback): void {
386  if (empty($this->registrations[$name][$type])) {
387  return;
388  }
389 
390  $matcher = $this->getMatcher($callback);
391 
392  foreach ($this->registrations[$name][$type] as $i => $registration) {
393  if ($matcher instanceof MethodMatcher) {
394  if (!$matcher->matches($registration[self::REG_KEY_HANDLER])) {
395  continue;
396  }
397  } elseif ($registration[self::REG_KEY_HANDLER] != $callback) {
398  continue;
399  }
400 
401  unset($this->registrations[$name][$type][$i]);
402  unset($this->ordered_handlers_cache);
403 
404  return;
405  }
406  }
407 
416  public function clearHandlers(string $name, string $type): void {
417  unset($this->registrations[$name][$type]);
418  unset($this->ordered_handlers_cache);
419  }
420 
435  public function getAllHandlers(): array {
436  $ret = [];
437  foreach ($this->registrations as $name => $types) {
438  foreach ($types as $type => $registrations) {
439  foreach ($registrations as $registration) {
440  $priority = $registration[self::REG_KEY_PRIORITY];
441  $ret[$name][$type][$priority][] = $registration[self::REG_KEY_HANDLER];
442  }
443  }
444  }
445 
446  return $ret;
447  }
448 
459  public function hasHandler(string $name, string $type): bool {
460  return !empty($this->registrations[$name][$type]);
461  }
462 
471  public function getOrderedHandlers(string $name, string $type): array {
472  $registrations = [];
473 
474  if (isset($this->ordered_handlers_cache[$name . $type])) {
475  return $this->ordered_handlers_cache[$name . $type];
476  }
477 
478  if (!empty($this->registrations[$name][$type])) {
479  if ($name !== 'all' && $type !== 'all') {
480  $registrations = $this->registrations[$name][$type];
481  }
482  }
483 
484  if (!empty($this->registrations['all'][$type])) {
485  if ($type !== 'all') {
486  $registrations += $this->registrations['all'][$type];
487  }
488  }
489 
490  if (!empty($this->registrations[$name]['all'])) {
491  if ($name !== 'all') {
492  $registrations += $this->registrations[$name]['all'];
493  }
494  }
495 
496  if (!empty($this->registrations['all']['all'])) {
497  $registrations += $this->registrations['all']['all'];
498  }
499 
500  ksort($registrations, SORT_NATURAL);
501 
502  $handlers = [];
503  foreach ($registrations as $registration) {
504  $handlers[] = $registration[self::REG_KEY_HANDLER];
505  }
506 
507  $this->ordered_handlers_cache[$name . $type] = $handlers;
508 
509  return $handlers;
510  }
511 
519  protected function getMatcher($spec): ?MethodMatcher {
520  if (is_string($spec) && str_contains($spec, '::')) {
521  list ($type, $method) = explode('::', $spec, 2);
522  return new MethodMatcher($type, $method);
523  }
524 
525  if (!is_array($spec) || empty($spec[0]) || empty($spec[1]) || !is_string($spec[1])) {
526  return null;
527  }
528 
529  if (is_object($spec[0])) {
530  $spec[0] = get_class($spec[0]);
531  }
532 
533  if (!is_string($spec[0])) {
534  return null;
535  }
536 
537  return new MethodMatcher($spec[0], $spec[1]);
538  }
539 
549  public function backup(): void {
550  $this->backups[] = $this->registrations;
551  $this->registrations = [];
552  unset($this->ordered_handlers_cache);
553  }
554 
560  public function restore(): void {
561  $backup = array_pop($this->backups);
562  if (is_array($backup)) {
563  $this->registrations = $backup;
564  }
565 
566  unset($this->ordered_handlers_cache);
567  }
568 
579  protected function checkDeprecation(string $name, string $type, string $message, string $version): void {
580  $message = trim($message);
581  if (empty($message)) {
582  return;
583  }
584 
585  if (!$this->hasHandler($name, $type)) {
586  return;
587  }
588 
589  $this->logDeprecatedMessage($message, $version);
590  }
591 
600  protected function callHandler($callable, $event, array $args, array $options = []): array {
601  // call a function before the actual callable
602  $begin_callback = elgg_extract(self::OPTION_BEGIN_CALLBACK, $options);
603  if (is_callable($begin_callback)) {
604  call_user_func($begin_callback, [
605  'callable' => $callable,
606  'readable_callable' => $this->handlers->describeCallable($callable),
607  'event' => $event,
608  'arguments' => $args,
609  ]);
610  }
611 
612  // time the callable function
613  $use_timer = (bool) elgg_extract(self::OPTION_USE_TIMER, $options, false);
614  $timer_keys = (array) elgg_extract(self::OPTION_TIMER_KEYS, $options, []);
615  if ($use_timer) {
616  $timer_keys[] = $this->handlers->describeCallable($callable);
617  $this->beginTimer($timer_keys);
618  }
619 
620  // execute the callable function
621  $results = $this->handlers->call($callable, $event, $args);
622 
623  // end the timer
624  if ($use_timer) {
625  $this->endTimer($timer_keys);
626  }
627 
628  // call a function after the actual callable
629  $end_callback = elgg_extract(self::OPTION_END_CALLBACK, $options);
630  if (is_callable($end_callback)) {
631  call_user_func($end_callback, [
632  'callable' => $callable,
633  'readable_callable' => $this->handlers->describeCallable($callable),
634  'event' => $event,
635  'arguments' => $args,
636  'results' => $results,
637  ]);
638  }
639 
640  return $results;
641  }
642 }
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.
Exception thrown if an argument is not of the expected type.
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:246
$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