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 
33  protected $handlers;
34 
38  protected $next_index = 0;
39 
43  protected $registrations = [];
44 
48  protected $backups = [];
49 
55  public function __construct(HandlersService $handlers) {
56  $this->handlers = $handlers;
57  }
58 
73  public function trigger(string $name, string $type, $object = null, array $options = []): bool {
74  $options = array_merge([
75  self::OPTION_STOPPABLE => true,
76  ], $options);
77 
78  // allow for the profiling of system events (when enabled)
79  if ($this->hasTimer() && $type === 'system' && $name !== 'shutdown') {
80  $options[self::OPTION_USE_TIMER] = true;
81  $options[self::OPTION_TIMER_KEYS] = ["[{$name},{$type}]"];
82  }
83 
84  // get registered handlers
85  $handlers = $this->getOrderedHandlers($name, $type);
86 
87  // This starts as a string, but if a handler type-hints an object we convert it on-demand inside
88  // \Elgg\HandlersService::call and keep it alive during all handler calls. We do this because
89  // creating objects for every triggering is expensive.
90  /* @var $event Event|string */
91  $event = 'event';
92  $event_args = [
93  $name,
94  $type,
95  null,
96  [
97  'object' => $object,
98  '_elgg_sequence_id' => elgg_extract('_elgg_sequence_id', $options),
99  ],
100  ];
101  foreach ($handlers as $handler) {
102  list($success, $return, $event) = $this->callHandler($handler, $event, $event_args, $options);
103 
104  if (!$success) {
105  continue;
106  }
107 
108  if (!empty($options[self::OPTION_STOPPABLE]) && ($return === false)) {
109  return false;
110  }
111  }
112 
113  return true;
114  }
115 
129  public function triggerResults(string $name, string $type, array $params = [], $value = null, array $options = []) {
130  // This starts as a string, but if a handler type-hints an object we convert it on-demand inside
131  // \Elgg\HandlersService::call and keep it alive during all handler calls. We do this because
132  // creating objects for every triggering is expensive.
133  /* @var $event Event|string */
134  $event = 'event';
135  foreach ($this->getOrderedHandlers($name, $type) as $handler) {
136  $event_args = [$name, $type, $value, $params];
137 
138  list($success, $return, $event) = $this->callHandler($handler, $event, $event_args, $options);
139 
140  if (!$success) {
141  continue;
142  }
143 
144  if ($return !== null) {
145  $value = $return;
146  $event->setValue($value);
147  }
148  }
149 
150  return $value;
151  }
152 
172  public function triggerBefore(string $name, string $type, $object = null, array $options = []): bool {
173  return $this->trigger("{$name}:before", $type, $object, $options);
174  }
175 
194  public function triggerAfter(string $name, string $type, $object = null, array $options = []): void {
195  $options[self::OPTION_STOPPABLE] = false;
196 
197  $this->trigger("{$name}:after", $type, $object, $options);
198  }
199 
214  public function triggerSequence(string $name, string $type, $object = null, callable $callable = null, array $options = []): bool {
215  // generate a unique ID to identify this sequence
216  $options['_elgg_sequence_id'] = uniqid("{$name}{$type}", true);
217 
218  if (!$this->triggerBefore($name, $type, $object, $options)) {
219  return false;
220  }
221 
222  $result = $this->trigger($name, $type, $object, $options);
223  if ($result === false) {
224  return false;
225  }
226 
227  if ($callable) {
228  $result = call_user_func($callable, $object);
229  }
230 
231  $this->triggerAfter($name, $type, $object, $options);
232 
233  return $result;
234  }
235 
250  public function triggerResultsSequence(string $name, string $type, array $params = [], $value = null, callable $callable = null, array $options = []) {
251  // generate a unique ID to identify this sequence
252  $unique_id = uniqid("{$name}{$type}results", true);
253  $options['_elgg_sequence_id'] = $unique_id;
254  $params['_elgg_sequence_id'] = $unique_id;
255 
256  if (!$this->triggerBefore($name, $type, $params, $options)) {
257  return false;
258  }
259 
260  $result = $this->triggerResults($name, $type, $params, $value, $options);
261  if ($result === false) {
262  return false;
263  }
264 
265  if ($callable) {
266  $result = call_user_func($callable, $params);
267  }
268 
269  $this->triggerAfter($name, $type, $params, $options);
270 
271  return $result;
272  }
273 
288  public function triggerDeprecated(string $name, string $type, $object = null, string $message = '', string $version = '', array $options = []): bool {
289  $message = "The '{$name}', '{$type}' event is deprecated. {$message}";
290  $this->checkDeprecation($name, $type, $message, $version);
291 
292  return $this->trigger($name, $type, $object, $options);
293  }
294 
310  public function triggerDeprecatedResults(string $name, string $type, array $params = [], $returnvalue = null, string $message = '', string $version = '', array $options = []) {
311  $message = "The '{$name}', '{$type}' event is deprecated. {$message}";
312  $this->checkDeprecation($name, $type, $message, $version);
313 
314  return $this->triggerResults($name, $type, $params, $returnvalue, $options);
315  }
316 
330  public function registerHandler(string $name, string $type, $callback, int $priority = 500): bool {
331  if (empty($name) || empty($type) || !is_callable($callback, true)) {
332  return false;
333  }
334 
335  if (($name == 'view' || $name == 'view_vars') && $type !== 'all') {
336  $type = ViewsService::canonicalizeViewName($type);
337  }
338 
339  $services = _elgg_services();
340  if (in_array($this->getLogger()->getLevel(false), [LogLevel::WARNING, LogLevel::NOTICE, LogLevel::INFO, LogLevel::DEBUG])) {
341  if (!$services->handlers->isCallable($callback)) {
342  $this->getLogger()->warning('Handler: ' . $services->handlers->describeCallable($callback) . ' is not callable');
343  }
344  }
345 
346  $this->registrations[$name][$type][] = [
347  self::REG_KEY_PRIORITY => $priority,
348  self::REG_KEY_INDEX => $this->next_index,
349  self::REG_KEY_HANDLER => $callback,
350  ];
351  $this->next_index++;
352 
353  return true;
354  }
355 
366  public function unregisterHandler(string $name, string $type, $callback): void {
367  if (($name === 'view' || $name === 'view_vars') && $type !== 'all') {
368  $type = ViewsService::canonicalizeViewName($type);
369  }
370 
371  if (empty($this->registrations[$name][$type])) {
372  return;
373  }
374 
375  $matcher = $this->getMatcher($callback);
376 
377  foreach ($this->registrations[$name][$type] as $i => $registration) {
378  if ($matcher instanceof MethodMatcher) {
379  if (!$matcher->matches($registration[self::REG_KEY_HANDLER])) {
380  continue;
381  }
382  } elseif ($registration[self::REG_KEY_HANDLER] != $callback) {
383  continue;
384  }
385 
386  unset($this->registrations[$name][$type][$i]);
387  return;
388  }
389  }
390 
399  public function clearHandlers(string $name, string $type): void {
400  unset($this->registrations[$name][$type]);
401  }
402 
417  public function getAllHandlers(): array {
418  $ret = [];
419  foreach ($this->registrations as $name => $types) {
420  foreach ($types as $type => $registrations) {
421  foreach ($registrations as $registration) {
422  $priority = $registration[self::REG_KEY_PRIORITY];
423  $ret[$name][$type][$priority][] = $registration[self::REG_KEY_HANDLER];
424  }
425  }
426  }
427 
428  return $ret;
429  }
430 
441  public function hasHandler(string $name, string $type): bool {
442  return !empty($this->registrations[$name][$type]);
443  }
444 
453  public function getOrderedHandlers(string $name, string $type): array {
454  $registrations = [];
455 
456  if (!empty($this->registrations[$name][$type])) {
457  if ($name !== 'all' && $type !== 'all') {
458  array_splice($registrations, count($registrations), 0, $this->registrations[$name][$type]);
459  }
460  }
461 
462  if (!empty($this->registrations['all'][$type])) {
463  if ($type !== 'all') {
464  array_splice($registrations, count($registrations), 0, $this->registrations['all'][$type]);
465  }
466  }
467 
468  if (!empty($this->registrations[$name]['all'])) {
469  if ($name !== 'all') {
470  array_splice($registrations, count($registrations), 0, $this->registrations[$name]['all']);
471  }
472  }
473 
474  if (!empty($this->registrations['all']['all'])) {
475  array_splice($registrations, count($registrations), 0, $this->registrations['all']['all']);
476  }
477 
478  usort($registrations, function ($a, $b) {
479  // priority first
480  if ($a[self::REG_KEY_PRIORITY] < $b[self::REG_KEY_PRIORITY]) {
481  return -1;
482  }
483 
484  if ($a[self::REG_KEY_PRIORITY] > $b[self::REG_KEY_PRIORITY]) {
485  return 1;
486  }
487 
488  // then insertion order
489  return ($a[self::REG_KEY_INDEX] < $b[self::REG_KEY_INDEX]) ? -1 : 1;
490  });
491 
492  $handlers = [];
493  foreach ($registrations as $registration) {
494  $handlers[] = $registration[self::REG_KEY_HANDLER];
495  }
496 
497  return $handlers;
498  }
499 
507  protected function getMatcher($spec): ?MethodMatcher {
508  if (is_string($spec) && str_contains($spec, '::')) {
509  list ($type, $method) = explode('::', $spec, 2);
510  return new MethodMatcher($type, $method);
511  }
512 
513  if (!is_array($spec) || empty($spec[0]) || empty($spec[1]) || !is_string($spec[1])) {
514  return null;
515  }
516 
517  if (is_object($spec[0])) {
518  $spec[0] = get_class($spec[0]);
519  }
520 
521  if (!is_string($spec[0])) {
522  return null;
523  }
524 
525  return new MethodMatcher($spec[0], $spec[1]);
526  }
527 
537  public function backup(): void {
538  $this->backups[] = $this->registrations;
539  $this->registrations = [];
540  }
541 
547  public function restore(): void {
548  $backup = array_pop($this->backups);
549  if (is_array($backup)) {
550  $this->registrations = $backup;
551  }
552  }
553 
564  protected function checkDeprecation(string $name, string $type, string $message, string $version): void {
565  $message = trim($message);
566  if (empty($message)) {
567  return;
568  }
569 
570  if (!$this->hasHandler($name, $type)) {
571  return;
572  }
573 
574  $this->logDeprecatedMessage($message, $version);
575  }
576 
585  protected function callHandler($callable, $event, array $args, array $options = []): array {
586  // call a function before the actual callable
587  $begin_callback = elgg_extract(self::OPTION_BEGIN_CALLBACK, $options);
588  if (is_callable($begin_callback)) {
589  call_user_func($begin_callback, [
590  'callable' => $callable,
591  'readable_callable' => $this->handlers->describeCallable($callable),
592  'event' => $event,
593  'arguments' => $args,
594  ]);
595  }
596 
597  // time the callable function
598  $use_timer = (bool) elgg_extract(self::OPTION_USE_TIMER, $options, false);
599  $timer_keys = (array) elgg_extract(self::OPTION_TIMER_KEYS, $options, []);
600  if ($use_timer) {
601  $timer_keys[] = $this->handlers->describeCallable($callable);
602  $this->beginTimer($timer_keys);
603  }
604 
605  // execute the callable function
606  $results = $this->handlers->call($callable, $event, $args);
607 
608  // end the timer
609  if ($use_timer) {
610  $this->endTimer($timer_keys);
611  }
612 
613  // call a function after the actual callable
614  $end_callback = elgg_extract(self::OPTION_END_CALLBACK, $options);
615  if (is_callable($end_callback)) {
616  call_user_func($end_callback, [
617  'callable' => $callable,
618  'readable_callable' => $this->handlers->describeCallable($callable),
619  'event' => $event,
620  'arguments' => $args,
621  'results' => $results,
622  ]);
623  }
624 
625  return $results;
626  }
627 }
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:22
$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
__construct(HandlersService $handlers)
Constructor.
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
$options
Elgg admin footer.
Definition: footer.php:6
$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
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.
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.
_elgg_services()
Get the global service provider.
Definition: elgglib.php:346
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:80