Elgg  Version master
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 
30  protected int $next_index = 0;
31 
32  protected array $ordered_handlers_cache = [];
33 
37  protected array $registrations = [];
38 
39  protected array $backups = [];
40 
46  public function __construct(protected HandlersService $handlers) {
47  }
48 
63  public function trigger(string $name, string $type, $object = null, array $options = []): bool {
64  $options = array_merge([
65  self::OPTION_STOPPABLE => true,
66  ], $options);
67 
68  // allow for the profiling of system events (when enabled)
69  if ($this->hasTimer() && $type === 'system' && $name !== 'shutdown') {
70  $options[self::OPTION_USE_TIMER] = true;
71  $options[self::OPTION_TIMER_KEYS] = ["[{$name},{$type}]"];
72  }
73 
74  // get registered handlers
75  $handlers = $this->getOrderedHandlers($name, $type);
76 
77  // This starts as a string, but if a handler type-hints an object we convert it on-demand inside
78  // \Elgg\HandlersService::call and keep it alive during all handler calls. We do this because
79  // creating objects for every triggering is expensive.
80  /* @var $event Event|string */
81  $event = 'event';
82  $event_args = [
83  $name,
84  $type,
85  null,
86  [
87  'object' => $object,
88  '_elgg_sequence_id' => elgg_extract('_elgg_sequence_id', $options),
89  ],
90  ];
91  foreach ($handlers as $handler) {
92  list($success, $return, $event) = $this->callHandler($handler, $event, $event_args, $options);
93 
94  if (!$success) {
95  continue;
96  }
97 
98  if (!empty($options[self::OPTION_STOPPABLE]) && ($return === false)) {
99  return false;
100  }
101  }
102 
103  return true;
104  }
105 
119  public function triggerResults(string $name, string $type, array $params = [], $value = null, array $options = []) {
120  // This starts as a string, but if a handler type-hints an object we convert it on-demand inside
121  // \Elgg\HandlersService::call and keep it alive during all handler calls. We do this because
122  // creating objects for every triggering is expensive.
123  /* @var $event Event|string */
124  $event = 'event';
125  foreach ($this->getOrderedHandlers($name, $type) as $handler) {
126  $event_args = [$name, $type, $value, $params];
127 
128  list($success, $return, $event) = $this->callHandler($handler, $event, $event_args, $options);
129 
130  if (!$success) {
131  continue;
132  }
133 
134  if ($return !== null) {
135  $value = $return;
136  $event->setValue($value);
137  }
138  }
139 
140  return $value;
141  }
142 
162  public function triggerBefore(string $name, string $type, $object = null, array $options = []): bool {
163  return $this->trigger("{$name}:before", $type, $object, $options);
164  }
165 
184  public function triggerAfter(string $name, string $type, $object = null, array $options = []): void {
185  $options[self::OPTION_STOPPABLE] = false;
186 
187  $this->trigger("{$name}:after", $type, $object, $options);
188  }
189 
204  public function triggerSequence(string $name, string $type, $object = null, ?callable $callable = null, array $options = []): bool {
205  // generate a unique ID to identify this sequence
206  $options['_elgg_sequence_id'] = uniqid("{$name}{$type}", true);
207 
208  if (!$this->triggerBefore($name, $type, $object, $options)) {
209  return false;
210  }
211 
212  $result = $this->trigger($name, $type, $object, $options);
213  if ($result === false) {
214  return false;
215  }
216 
217  if ($callable) {
218  $result = call_user_func($callable, $object);
219  }
220 
221  if ($result !== false) {
222  $this->triggerAfter($name, $type, $object, $options);
223  }
224 
225  return $result;
226  }
227 
242  public function triggerResultsSequence(string $name, string $type, array $params = [], $value = null, ?callable $callable = null, array $options = []) {
243  // generate a unique ID to identify this sequence
244  $unique_id = uniqid("{$name}{$type}results", true);
245  $options['_elgg_sequence_id'] = $unique_id;
246  $params['_elgg_sequence_id'] = $unique_id;
247 
248  if (!$this->triggerBefore($name, $type, $params, $options)) {
249  return false;
250  }
251 
252  $result = $this->triggerResults($name, $type, $params, $value, $options);
253  if ($result === false) {
254  return false;
255  }
256 
257  if ($callable) {
258  $result = call_user_func($callable, $params);
259  }
260 
261  if ($result !== false) {
262  $this->triggerAfter($name, $type, $params, $options);
263  }
264 
265  return $result;
266  }
267 
282  public function triggerDeprecated(string $name, string $type, $object = null, string $message = '', string $version = '', array $options = []): bool {
283  $message = "The '{$name}', '{$type}' event is deprecated. {$message}";
284  $this->checkDeprecation($name, $type, $message, $version);
285 
286  return $this->trigger($name, $type, $object, $options);
287  }
288 
304  public function triggerDeprecatedResults(string $name, string $type, array $params = [], $returnvalue = null, string $message = '', string $version = '', array $options = []) {
305  $message = "The '{$name}', '{$type}' event is deprecated. {$message}";
306  $this->checkDeprecation($name, $type, $message, $version);
307 
308  return $this->triggerResults($name, $type, $params, $returnvalue, $options);
309  }
310 
324  public function registerHandler(string $name, string $type, $callback, int $priority = 500): bool {
325  if (empty($name) || empty($type) || !is_callable($callback, true)) {
326  return false;
327  }
328 
329  if (in_array($this->getLogger()->getLevel(false), [LogLevel::WARNING, LogLevel::NOTICE, LogLevel::INFO, LogLevel::DEBUG])) {
330  if (!$this->handlers->isCallable($callback)) {
331  $this->getLogger()->warning('Handler: ' . $this->handlers->describeCallable($callback) . ' is not callable');
332  }
333  }
334 
335  $this->registrations[$name][$type]["{$priority}_{$this->next_index}"] = [
336  self::REG_KEY_PRIORITY => $priority,
337  self::REG_KEY_INDEX => $this->next_index,
338  self::REG_KEY_HANDLER => $callback,
339  ];
340  $this->next_index++;
341 
342  unset($this->ordered_handlers_cache);
343 
344  return true;
345  }
346 
357  public function unregisterHandler(string $name, string $type, $callback): void {
358  if (empty($this->registrations[$name][$type])) {
359  return;
360  }
361 
362  $matcher = $this->getMatcher($callback);
363 
364  foreach ($this->registrations[$name][$type] as $i => $registration) {
365  if ($matcher instanceof MethodMatcher) {
366  if (!$matcher->matches($registration[self::REG_KEY_HANDLER])) {
367  continue;
368  }
369  } elseif ($registration[self::REG_KEY_HANDLER] != $callback) {
370  continue;
371  }
372 
373  unset($this->registrations[$name][$type][$i]);
374  unset($this->ordered_handlers_cache);
375 
376  return;
377  }
378  }
379 
388  public function clearHandlers(string $name, string $type): void {
389  unset($this->registrations[$name][$type]);
390  unset($this->ordered_handlers_cache);
391  }
392 
407  public function getAllHandlers(): array {
408  $ret = [];
409  foreach ($this->registrations as $name => $types) {
410  foreach ($types as $type => $registrations) {
411  foreach ($registrations as $registration) {
412  $priority = $registration[self::REG_KEY_PRIORITY];
413  $ret[$name][$type][$priority][] = $registration[self::REG_KEY_HANDLER];
414  }
415  }
416  }
417 
418  return $ret;
419  }
420 
431  public function hasHandler(string $name, string $type): bool {
432  return !empty($this->registrations[$name][$type]);
433  }
434 
443  public function getOrderedHandlers(string $name, string $type): array {
444  $registrations = [];
445 
446  if (isset($this->ordered_handlers_cache[$name . $type])) {
447  return $this->ordered_handlers_cache[$name . $type];
448  }
449 
450  if (!empty($this->registrations[$name][$type])) {
451  if ($name !== 'all' && $type !== 'all') {
452  $registrations = $this->registrations[$name][$type];
453  }
454  }
455 
456  if (!empty($this->registrations['all'][$type])) {
457  if ($type !== 'all') {
458  $registrations += $this->registrations['all'][$type];
459  }
460  }
461 
462  if (!empty($this->registrations[$name]['all'])) {
463  if ($name !== 'all') {
464  $registrations += $this->registrations[$name]['all'];
465  }
466  }
467 
468  if (!empty($this->registrations['all']['all'])) {
469  $registrations += $this->registrations['all']['all'];
470  }
471 
472  ksort($registrations, SORT_NATURAL);
473 
474  $handlers = [];
475  foreach ($registrations as $registration) {
476  $handlers[] = $registration[self::REG_KEY_HANDLER];
477  }
478 
479  $this->ordered_handlers_cache[$name . $type] = $handlers;
480 
481  return $handlers;
482  }
483 
491  protected function getMatcher($spec): ?MethodMatcher {
492  if (is_string($spec) && str_contains($spec, '::')) {
493  list ($type, $method) = explode('::', $spec, 2);
494  return new MethodMatcher($type, $method);
495  }
496 
497  if (!is_array($spec) || empty($spec[0]) || empty($spec[1]) || !is_string($spec[1])) {
498  return null;
499  }
500 
501  if (is_object($spec[0])) {
502  $spec[0] = get_class($spec[0]);
503  }
504 
505  if (!is_string($spec[0])) {
506  return null;
507  }
508 
509  return new MethodMatcher($spec[0], $spec[1]);
510  }
511 
521  public function backup(): void {
522  $this->backups[] = $this->registrations;
523  $this->registrations = [];
524  unset($this->ordered_handlers_cache);
525  }
526 
532  public function restore(): void {
533  $backup = array_pop($this->backups);
534  if (is_array($backup)) {
535  $this->registrations = $backup;
536  }
537 
538  unset($this->ordered_handlers_cache);
539  }
540 
551  protected function checkDeprecation(string $name, string $type, string $message, string $version): void {
552  $message = trim($message);
553  if (empty($message)) {
554  return;
555  }
556 
557  if (!$this->hasHandler($name, $type)) {
558  return;
559  }
560 
561  $this->logDeprecatedMessage($message, $version);
562  }
563 
572  protected function callHandler($callable, $event, array $args, array $options = []): array {
573  // call a function before the actual callable
574  $begin_callback = elgg_extract(self::OPTION_BEGIN_CALLBACK, $options);
575  if (is_callable($begin_callback)) {
576  call_user_func($begin_callback, [
577  'callable' => $callable,
578  'readable_callable' => $this->handlers->describeCallable($callable),
579  'event' => $event,
580  'arguments' => $args,
581  ]);
582  }
583 
584  // time the callable function
585  $use_timer = (bool) elgg_extract(self::OPTION_USE_TIMER, $options, false);
586  $timer_keys = (array) elgg_extract(self::OPTION_TIMER_KEYS, $options, []);
587  if ($use_timer) {
588  $timer_keys[] = $this->handlers->describeCallable($callable);
589  $this->beginTimer($timer_keys);
590  }
591 
592  // execute the callable function
593  $results = $this->handlers->call($callable, $event, $args);
594 
595  // end the timer
596  if ($use_timer) {
597  $this->endTimer($timer_keys);
598  }
599 
600  // call a function after the actual callable
601  $end_callback = elgg_extract(self::OPTION_END_CALLBACK, $options);
602  if (is_callable($end_callback)) {
603  call_user_func($end_callback, [
604  'callable' => $callable,
605  'readable_callable' => $this->handlers->describeCallable($callable),
606  'event' => $event,
607  'arguments' => $args,
608  'results' => $results,
609  ]);
610  }
611 
612  return $results;
613  }
614 }
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=[])
Triggers a event that is 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=[])
Triggers 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:256
$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