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 
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 }
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