Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.
   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  namespace core\event;
  18  
  19  defined('MOODLE_INTERNAL') || die();
  20  
  21  /**
  22   * New event manager class.
  23   *
  24   * @package    core
  25   * @since      Moodle 2.6
  26   * @copyright  2013 Petr Skoda {@link http://skodak.org}
  27   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  28   */
  29  
  30  /**
  31   * Class used for event dispatching.
  32   *
  33   * Note: Do NOT use directly in your code, it is intended to be used from
  34   *       base event class only.
  35   */
  36  class manager {
  37      /** @var array buffer of event for dispatching */
  38      protected static $buffer = array();
  39  
  40      /** @var array buffer for events that were not sent to external observers when DB transaction in progress */
  41      protected static $extbuffer = array();
  42  
  43      /** @var bool evert dispatching already in progress - prevents nesting */
  44      protected static $dispatching = false;
  45  
  46      /** @var array cache of all observers */
  47      protected static $allobservers = null;
  48  
  49      /** @var bool should we reload observers after the test? */
  50      protected static $reloadaftertest = false;
  51  
  52      /**
  53       * Trigger new event.
  54       *
  55       * @internal to be used only from \core\event\base::trigger() method.
  56       * @param \core\event\base $event
  57       *
  58       * @throws \coding_Exception if used directly.
  59       */
  60      public static function dispatch(\core\event\base $event) {
  61          if (during_initial_install()) {
  62              return;
  63          }
  64          if (!$event->is_triggered() or $event->is_dispatched()) {
  65              throw new \coding_exception('Illegal event dispatching attempted.');
  66          }
  67  
  68          self::$buffer[] = $event;
  69  
  70          if (self::$dispatching) {
  71              return;
  72          }
  73  
  74          self::$dispatching = true;
  75          self::process_buffers();
  76          self::$dispatching = false;
  77      }
  78  
  79      /**
  80       * Notification from DML layer.
  81       * @internal to be used from DML layer only.
  82       */
  83      public static function database_transaction_commited() {
  84          if (self::$dispatching or empty(self::$extbuffer)) {
  85              return;
  86          }
  87  
  88          self::$dispatching = true;
  89          self::process_buffers();
  90          self::$dispatching = false;
  91      }
  92  
  93      /**
  94       * Notification from DML layer.
  95       * @internal to be used from DML layer only.
  96       */
  97      public static function database_transaction_rolledback() {
  98          self::$extbuffer = array();
  99      }
 100  
 101      protected static function process_buffers() {
 102          global $DB, $CFG;
 103          self::init_all_observers();
 104  
 105          while (self::$buffer or self::$extbuffer) {
 106  
 107              $fromextbuffer = false;
 108              $addedtoextbuffer = false;
 109  
 110              if (self::$extbuffer and !$DB->is_transaction_started()) {
 111                  $fromextbuffer = true;
 112                  $event = reset(self::$extbuffer);
 113                  unset(self::$extbuffer[key(self::$extbuffer)]);
 114  
 115              } else if (self::$buffer) {
 116                  $event = reset(self::$buffer);
 117                  unset(self::$buffer[key(self::$buffer)]);
 118  
 119              } else {
 120                  return;
 121              }
 122  
 123              $observingclasses = self::get_observing_classes($event);
 124              foreach ($observingclasses as $observingclass) {
 125                  if (!isset(self::$allobservers[$observingclass])) {
 126                      continue;
 127                  }
 128                  foreach (self::$allobservers[$observingclass] as $observer) {
 129                      if ($observer->internal) {
 130                          if ($fromextbuffer) {
 131                              // Do not send buffered external events to internal handlers,
 132                              // they processed them already.
 133                              continue;
 134                          }
 135                      } else {
 136                          if ($DB->is_transaction_started()) {
 137                              if ($fromextbuffer) {
 138                                  // Weird!
 139                                  continue;
 140                              }
 141                              // Do not notify external observers while in DB transaction.
 142                              if (!$addedtoextbuffer) {
 143                                  self::$extbuffer[] = $event;
 144                                  $addedtoextbuffer = true;
 145                              }
 146                              continue;
 147                          }
 148                      }
 149  
 150                      if (isset($observer->includefile) and file_exists($observer->includefile)) {
 151                          include_once($observer->includefile);
 152                      }
 153                      if (is_callable($observer->callable)) {
 154                          try {
 155                              call_user_func($observer->callable, $event);
 156                          } catch (\Exception $e) {
 157                              // Observers are notified before installation and upgrade, this may throw errors.
 158                              if (empty($CFG->upgraderunning)) {
 159                                  // Ignore errors during upgrade, otherwise warn developers.
 160                                  debugging("Exception encountered in event observer '$observer->callable': ".$e->getMessage(), DEBUG_DEVELOPER, $e->getTrace());
 161                              }
 162                          }
 163                      } else {
 164                          debugging("Can not execute event observer '$observer->callable'");
 165                      }
 166                  }
 167              }
 168  
 169              // TODO: Invent some infinite loop protection in case events cross-trigger one another.
 170          }
 171      }
 172  
 173      /**
 174       * Returns list of classes related to this event.
 175       * @param \core\event\base $event
 176       * @return array
 177       */
 178      protected static function get_observing_classes(\core\event\base $event) {
 179          $classname = get_class($event);
 180          $observers = array('\\'.$classname);
 181          while ($classname = get_parent_class($classname)) {
 182              $observers[] = '\\'.$classname;
 183          }
 184          $observers = array_reverse($observers, false);
 185  
 186          return $observers;
 187      }
 188  
 189      /**
 190       * Initialise the list of observers.
 191       */
 192      protected static function init_all_observers() {
 193          global $CFG;
 194  
 195          if (is_array(self::$allobservers)) {
 196              return;
 197          }
 198  
 199          if (!PHPUNIT_TEST and !during_initial_install()) {
 200              $cache = \cache::make('core', 'observers');
 201              $cached = $cache->get('all');
 202              $dirroot = $cache->get('dirroot');
 203              if ($dirroot === $CFG->dirroot and is_array($cached)) {
 204                  self::$allobservers = $cached;
 205                  return;
 206              }
 207          }
 208  
 209          self::$allobservers = array();
 210  
 211          $plugintypes = \core_component::get_plugin_types();
 212          $plugintypes = array_merge(array('core' => 'not used'), $plugintypes);
 213          $systemdone = false;
 214          foreach ($plugintypes as $plugintype => $ignored) {
 215              if ($plugintype === 'core') {
 216                  $plugins['core'] = "$CFG->dirroot/lib";
 217              } else {
 218                  $plugins = \core_component::get_plugin_list($plugintype);
 219              }
 220  
 221              foreach ($plugins as $plugin => $fulldir) {
 222                  if (!file_exists("$fulldir/db/events.php")) {
 223                      continue;
 224                  }
 225                  $observers = null;
 226                  include("$fulldir/db/events.php");
 227                  if (!is_array($observers)) {
 228                      continue;
 229                  }
 230                  self::add_observers($observers, "$fulldir/db/events.php", $plugintype, $plugin);
 231              }
 232          }
 233  
 234          self::order_all_observers();
 235  
 236          if (!PHPUNIT_TEST and !during_initial_install()) {
 237              $cache->set('all', self::$allobservers);
 238              $cache->set('dirroot', $CFG->dirroot);
 239          }
 240      }
 241  
 242      /**
 243       * Add observers.
 244       * @param array $observers
 245       * @param string $file
 246       * @param string $plugintype Plugin type of the observer.
 247       * @param string $plugin Plugin of the observer.
 248       */
 249      protected static function add_observers(array $observers, $file, $plugintype = null, $plugin = null) {
 250          global $CFG;
 251  
 252          foreach ($observers as $observer) {
 253              if (empty($observer['eventname']) or !is_string($observer['eventname'])) {
 254                  debugging("Invalid 'eventname' detected in $file observer definition", DEBUG_DEVELOPER);
 255                  continue;
 256              }
 257              if ($observer['eventname'] === '*') {
 258                  $observer['eventname'] = '\core\event\base';
 259              }
 260              if (strpos($observer['eventname'], '\\') !== 0) {
 261                  $observer['eventname'] = '\\'.$observer['eventname'];
 262              }
 263              if (empty($observer['callback'])) {
 264                  debugging("Invalid 'callback' detected in $file observer definition", DEBUG_DEVELOPER);
 265                  continue;
 266              }
 267              $o = new \stdClass();
 268              $o->callable = $observer['callback'];
 269              if (!isset($observer['priority'])) {
 270                  $o->priority = 0;
 271              } else {
 272                  $o->priority = (int)$observer['priority'];
 273              }
 274              if (!isset($observer['internal'])) {
 275                  $o->internal = true;
 276              } else {
 277                  $o->internal = (bool)$observer['internal'];
 278              }
 279              if (empty($observer['includefile'])) {
 280                  $o->includefile = null;
 281              } else {
 282                  if ($CFG->admin !== 'admin' and strpos($observer['includefile'], '/admin/') === 0) {
 283                      $observer['includefile'] = preg_replace('|^/admin/|', '/'.$CFG->admin.'/', $observer['includefile']);
 284                  }
 285                  $observer['includefile'] = $CFG->dirroot . '/' . ltrim($observer['includefile'], '/');
 286                  if (!file_exists($observer['includefile'])) {
 287                      debugging("Invalid 'includefile' detected in $file observer definition", DEBUG_DEVELOPER);
 288                      continue;
 289                  }
 290                  $o->includefile = $observer['includefile'];
 291              }
 292              $o->plugintype = $plugintype;
 293              $o->plugin = $plugin;
 294              self::$allobservers[$observer['eventname']][] = $o;
 295          }
 296      }
 297  
 298      /**
 299       * Reorder observers to allow quick lookup of observer for each event.
 300       */
 301      protected static function order_all_observers() {
 302          foreach (self::$allobservers as $classname => $observers) {
 303              \core_collator::asort_objects_by_property($observers, 'priority', \core_collator::SORT_NUMERIC);
 304              self::$allobservers[$classname] = array_reverse($observers);
 305          }
 306      }
 307  
 308      /**
 309       * Returns all observers in the system. This is only for use for reporting on the list of observers in the system.
 310       *
 311       * @access private
 312       * @return array An array of stdClass with all core observer details.
 313       */
 314      public static function get_all_observers() {
 315          self::init_all_observers();
 316          return self::$allobservers;
 317      }
 318  
 319      /**
 320       * Replace all standard observers.
 321       * @param array $observers
 322       * @return array
 323       *
 324       * @throws \coding_Exception if used outside of unit tests.
 325       */
 326      public static function phpunit_replace_observers(array $observers) {
 327          if (!PHPUNIT_TEST) {
 328              throw new \coding_exception('Cannot override event observers outside of phpunit tests!');
 329          }
 330  
 331          self::phpunit_reset();
 332          self::$allobservers = array();
 333          self::$reloadaftertest = true;
 334  
 335          self::add_observers($observers, 'phpunit');
 336          self::order_all_observers();
 337  
 338          return self::$allobservers;
 339      }
 340  
 341      /**
 342       * Reset everything if necessary.
 343       * @private
 344       *
 345       * @throws \coding_Exception if used outside of unit tests.
 346       */
 347      public static function phpunit_reset() {
 348          if (!PHPUNIT_TEST) {
 349              throw new \coding_exception('Cannot reset event manager outside of phpunit tests!');
 350          }
 351          self::$buffer = array();
 352          self::$extbuffer = array();
 353          self::$dispatching = false;
 354          if (!self::$reloadaftertest) {
 355              self::$allobservers = null;
 356          }
 357          self::$reloadaftertest = false;
 358      }
 359  }