Difference between revisions of "Event 2"

Jump to: navigation, search
m (Why is a new events system needed?)
(core_event_base class)
Line 531: Line 531:
 
   */
 
   */
 
   abstract public static function get_description();
 
   abstract public static function get_description();
 +
 +
 +
  /**
 +
  * Returns legacy event name if existed.
 +
  *
 +
  * @return string
 +
  */
 +
  public function get_legacy_eventname() {
 +
    return $this->legacyeventname;
 +
  }
 +
 +
  /**
 +
  * Override when legacy event contained data.
 +
  *
 +
  * @return mixed
 +
  */
 +
  public function get_legacy_eventdata() {
 +
    return null;
 +
  }
 +
 +
  /**
 +
  * Override when there is a matching add_to_log() in old code.
 +
  *
 +
  * This is constructed using existing the data available in the event,
 +
  * or by doing queries to get more information.
 +
  *
 +
  * @return array|null
 +
  */
 +
  public function get_legacy_logdata() {
 +
    return null;
 +
  }
  
 
   /**
 
   /**
Line 569: Line 600:
 
     $event->triggered = true;
 
     $event->triggered = true;
 
     return $event;
 
     return $event;
  }
 
 
  /**
 
  * Returns legacy event name if existed.
 
  *
 
  * @return string
 
  */
 
  public function get_legacy_eventname() {
 
    return $this->legacyeventname;
 
  }
 
 
  /**
 
  * Override when legacy event contained data.
 
  *
 
  * @return mixed
 
  */
 
  public function get_legacy_eventdata() {
 
    return null;
 
  }
 
 
  /**
 
  * Override when there is a matching add_to_log() in old code.
 
  *
 
  * @retrun array|null
 
  */
 
  public function get_legacy_logdata() {
 
    return null;
 
 
   }
 
   }
  

Revision as of 01:28, 28 May 2013

Note: This page is a work-in-progress. Feedback and suggested improvements are welcome. Please join the discussion on moodle.org or use the page comments.

Events 2
Project state In early specification
Tracker issue MDL-39797
Discussion
Assignee Backend Team


What are events?

Events are atomic pieces of information relating to an action in the system. Such actions are primarily the result of user actions, but could also be the result of the cron process or administration actions undertaken via the command line.

When an action takes place, an event is created by a core API or plugin. The Events system then disseminates this event information to handlers registered as observing events. In this way, the events system acts as a communication backbone throughout the Moodle system.

Event handlers can not modify event data or interrupt the dispatching of events, it is a one way communication channel.

Why is a new events system needed?

The need to improve the Events system was prompted by a need for a richer and more efficient logging system, however the benefits of this improvement will be useful to other parts of Moodle that observe event information.

  • The events need to be more strictly defined. They need to contain a lot more information in a standardised way.
  • It will be possible to subscribe to '*' event, which would allow a system to potentially observe, and selectively deal with, all events.
  • Complex data types were allowed in old events which was causing major problems.
  • Potential duplication of events and logging actions.
  • The logging system will become an event handler, observing events and directing them to logging storage plugins in a controllable way.

Performance

This section needs more work. Needs details on pre and post profiling tests.

How this proposal is going to effect Moodle's performance...

We will test this by ...

(Please complete this section.)

Events API

Each plugin will define the events that it can report (trigger) by extending an abstract base class, once for each possible event. This approach will have several benefits.

Events will be active objects
When they are triggered and possibly after they are reinstantiated (say, when they are retrieved from a log), an event object will be able to provide callback functions for various purposes (such as capability checks).
Automatic inclusion
Event class definitions will be automatically included when needed, without having to maintain lists of known event types. New event definitions can be added without the need to upgrade.
Maintainability
It will be easy to add new events and modify existing events. Plugin authors will be able to report minimal event information or richer information as needed.
Self documenting
The behaviour of events will be combined with the definition of events in one place (file). It will be easy for event handler writers to know what events a plugin can trigger.
Quick, self-validating data structure
As events are instantiated objects, the PHP processor will validate the structure and type of event classes. This does not ensure data value validity, but does give some assurance of consistency.

Backwards compatibility and migration

Events:

  • Moodle core and standard plugins will replace all events_trigger() with new events classes.
  • For events that already exist in Moodle 2.5 the additional legacy information should be added to the event data (in properties 'legacyeventname' and 'legacyeventdata'.
  • Function events_trigger() will continue working as before, it will be called automatically after new event is processed using the 'legacyeventname' and 'legacyeventdata'.
  • The legacy events handling code will be maintained separately and will continue being supported in Moodle 2.x, new legacy events will not be added.
  • Existing legacy event handlers will be migrated to new event handlers accepting new event class instances.
  • More subsystems may be migrated to events-handlers, ex.: gradebook history

Logging:

  • Function add_to_log() and all logging internals will continue working as before.
  • Existing add_to_log() parameters will be migrated inside new events method get_legacy_log_data() and core_event_base::trigger() will call add_to_log() automatically (this may depend on some setting for performance reasons)

Event dispatching and observers

New event dispatching is completely separate from old events code. Original event handlers are now called observers, the description is stored in the same db/events.php file, but as a new array with different format.

Event observers

The observers are described in db/events.php in array $observers, the array is not indexed and contains list of observers defined as array with following properties;

  • eventclass - event class name or "*" indicating all events
  • callable - string with PHP callable type
  • includefile - optional, file to be included before calling the observer, path relative to dirroot
  • priority - optional, defaults to 0, observers with higher priority are notified first
  • internal - optional, defaults to true, non-internal observers are not called during database transactions, but instead after successful commit of the transaction
$observers = array(
 
    array(
        'eventclass'  => 'core_event_sample_executed',
        'callable'    => 'core_event_sample_observer::observe_one',
    ),
 
    array(
        'eventclass'  => 'core_event_sample_executed',
        'callable'    => 'core_event_sample_observer::external_observer',
        'priority'    => 200,
        'internal'    => 0,
    ),
 
    array(
        'eventclass'  => '*',
        'callable'    => 'core_event_sample_observer::observe_all',
        'includefile' => null,
        'internal'    => 1,
        'priority'    => 9999,
    ),
 
);

Event dispatching

List of available observers is constructed on the fly directly from all available events.php files, it is no longer parsed during upgrade or install. Observers get events before installation or any upgrade, however observers are not notified during initial installation.

Developers of observers must make sure that execution does not end with fatal error under any condition, exceptions are automatically captured, logged in PHP error log and notification of other observers continues.

Observers are notified sequentially in the same order in which events were triggered, it means that events triggered in observers are buffered temporarily and are processed after all observers are notified.

Differences from old event handling

  1. New events contain a lot more structured information.
  2. New event data must not contain any PHP classes except stdClass.
  3. No database access in new event dispatching code.
  4. There is no support for cron execution - this eliminates performance problems, simplifies events implementation and prevents abuse of cron events.
  5. Events triggered in observers are procesed in different order.
  6. External events are buffered when transaction in progress instead of being sent to cron queue.
  7. It is possible to define multiple observers for one event in one events.php file.
  8. It is possible to subscribe observer to all events.
  9. New event manager is using frankenstyle autoloading - smaller memory footprint when events not used on current page.

Demonstration of event dispatching algorithms

See https://github.com/skodak/moodle/compare/master...wip_MDL-39846_m26_eventdispatcher

Triggering events

  • All event descriptions are objects extending core_event_base class.
  • Events are triggered by creating new instance of class event and executing $event->trigger().
  • Each event class name is a unique identifier of the event.
  • Class names follow the identifier scheme pluginfullname_event_something_happened. Core events have prefix 'core_'.
  • Plugins define each event class in separate file. File name and location must match class name, for example: plugindir/classes/event/something/happened.php
  • The event identifier suffix (something_happened in the example above) should follow the naming convention. There are recommendations for verbs to use in events names Suggested verb list

Examples: core_event_course_completed, mod_assign_event_submission_commented, mod_forum_event_post_shared, mod_forum_event_post_responded etc.

[TODO] We need to agree on "teacher verbs" to use when entity (course, module, etc.) was created, updated and/or deleted

  • Ideally, it should be possible to trigger an event without gathering additional information, just for the event. To reduce cost of data gathering, specifically the cost of database reads, at least the minimal values needed to trigger an event should be already available in variables.

Example of triggering an event:

$event = mod_myplugin_event_something_happened::create(array('courseid' => XXX, 'userid' => YYY, 'data' => ZZZ));
// ... more code that may delete or modify the entities used when creating an event object
$event->trigger();

or optionally if implemented:

mod_myplugin_event_something_happened::create_and_trigger(array('courseid' => XXX, 'userid' => YYY, 'data' => ZZZ));


Use of php autoloading

See Automatic Class Loading Proposal


PHP __autoload is a very nice feature in PHP 5 which allows for

  • Class files to be automatically loaded when needed
  • Only those files that need to be included are included (a little bit of memory savings).
  • Defer file inclusion to last possible moment.
  • Allow registering of more than one function to load class files (Needed while using third party libraries or projects where file structure is different in different modules)
  • Nice error handling can be implemented to suite special needs
  • Enforce structured naming scheme throughout the project
  • Prevents duplicate class name declarations from ever occurring
  • Simple to use and avoid dependency tracking in large projects like moodle.

On the other hand there are few cons for using __autoload:

  • Might break plugins not using structure or duplicate class names (not a problem if Frankenstyle rules used).
  • If lots of classes, calling the function, translating the names and so on usually takes longer to execute than a group of simple includes (Probably negligible but worth considering).
  • Autoloaders cannot work simultaneously, having two libraries with different structure will need the second one to wait till the first one either load the class or decide that it does not recognize the class name. (Moodle classloader should be registered as the last one because it is the slowest)
  • Can cause issues with obfuscater or zend guard (Moodle does not support obfuscators already).

Why separate classes?

There were two alternatives proposed, on how to define the event structure. First having a separate class for each event (extending a base class), the other being each a generic event instance of the base class.

Decision: Use separate class for each event.

Each plugin creates own event class for each event

Pros

  • Maintainability - It is much easier to review, debug, integrate.
  • Self documenting, behaviour combined with definition
  • It is extremely flexible for plugin developers and core devs too.
  • Automatically lists events without being installed - PHPDocs as events documentation
  • Inlcuded only when needed using autoloading
  • Self-validating data structure (by PHP)
  • Some developers find it easier to copy whole class files as templates.

Cons

  • Big learning curve for developers without OOP skills
  • Some developers may find it harder to copy-and-paste examples


Each plugin defines events in a list based on generic object

Pros

  • Easier for some developers
  • More control of event structure in core (?)
  • Easy to call an event, consistent throughout all of Moodle (?)

Cons

  • It is not flexible enough, PHP code gives developers more freedom.
  • It would not be possible to implement any performance hacks in custom methods, all data would have to be calculated even if not used.
  • It would be necessary to define access control callbacks in other code.
  • It would be harder and slower to integrate legacy logging.
  • It would be harder and slower to implement support for legacy events.
  • Event observers could not use event class name as reliable identifier.
  • Requires upgrade/install to register an event, events could not be triggered earlier.
  • Would need to be maintained through DB tables.
  • The implementation of events infrastructure would be significantly more complex and error prone.

Event properties

Those are the fields that an event could define. Depending on the type of event, some can be optional and some can be mandatory. Not all of the mandatory fields have to be set when triggering the event as some logic can help defining them in the constructor. For example, we can use $USER to find out what user is currently logged in.

There are suggested list of properties, with some mandatory fields (*) defined for the base class.

  • Event specification version number* version
  • Event name* name (Automatically created using the class name)
  • Type* type
    • Error
    • User action
      • Procedural action
      • Manual action
    • System log
  • Datetime* time (milliseconds? DateTime object? Let's keep in mind that timestamps are unreliable as they don't include timezone!)
  • Context ID* contextid
  • Category ID coursecatid
  • Category name coursecatname
  • Course ID* courseid
  • Course name coursename
  • Course module ID* cmid
  • Course module name cmname
  • Component* component (Component declaring the event, not the one triggering it)
    • core/moodle
    • mod_assign
    • mod_workshop
    • qtype_match
  • Actor* actor (user, cli, cron, ...) (We cannot always identify what is the source of the action, so this would either be 'User' or 'System' and so the property userid is enough)
  • User ID* userid (User ID, or 0 when not logged in, or -1 when it's CLI, Cron, or the System)
  • Real user ID* realuserid (When logged in as, store the real user ID)
  • User IP address* userip
  • Action* action (The exact action that is performed on the object)
    • created
    • moved (for a course, a section, a module, a user between groups)
    • submitted (for an assignment, or a message)
    • ended (for a submission phase for instance)
  • Object* object (Defines the object on which the action is performed)
    • user
    • section
    • assignment
    • submission_phase
  • Object ID* objectid
  • Object name objectname (Human readable identifier of the object: Mark Johnson, Course ABC, ...)
  • Object URL objecturl (URL to notice the changes)
  • Associated object associatedobject (Object associated to the subject. Ie: category of origin when a course is moved. User to whom a message is sent.)
    • section
    • category
    • user (to whom you sent a message)
  • Associated object ID associatedobjectid
  • Associated object URL associatedobjecturl (URL to notice the changes)
  • Associated users IDs associatedusersids (see get_affected_users())
    • Typically used when the action has multiple users involved. For instance, bulk uploading of users, or marking a group assignment.
  • Transaction type* crud (CRUD)
    • create
    • read
    • update
    • delete
  • Level* level (Reserved for internal use, probably no more than 3 types, default to normal)
    • major
    • normal
    • minor
  • Severity severity (In the case of error logging, we would need to set the severity according to http://tools.ietf.org/html/rfc5424#section-6.2.1, defaults to info)
  • Data data (any extra field specific to this event)
  • Current URL currenturl
  • Legacy event name legacyname Former event name
  • Legacy event data legacydata Former event data

Data

The field data should be a placeholder for any extra information which is specific to the event. This array/object of values could describe in more details an entry which has been deleted by storing the data, or an entry updated by storing the differences, etc.... This data could ideally be converted to JSON (by a logger) to easily be stored, which means that PHP Objects cannot be included in there. We would not rely on serialized data as they have been proven not reliable.

Filtering events

Observers should be able to filter events based on information that they are interested in. Damyon pointed out that if you want to filter all the events that happen to a specific user, you would have problems when grading a group assignment, because there is no track of who was in that group. Though, we could store this information in $data, but this is not filterable without knowing what specific data each event is defining.

In order to be able to filter by users, we could add a method get_all_affected_users() which combines subject, object and associated object when relevant. But more importantly, this method can add extra heavy logic, specific to the event, to retrieve more users, for example the users in a group.

core_event_base class

/**
 * Base event abstract class.
 */
abstract class core_event_base implements IteratorAggregate {
 
  const LEVEL_MINOR = 'minor';
  const LEVEL_NORMAL = 'normal';
  const LEVEL_MAJOR = 'major';
 
  // Constants.
  private $version = 1;
  private $type = 'action';
  private $restored = false;
  private $triggered = false;
 
  // Those variables should be constants for child classes.
  protected $name;
  protected $component;
  protected $action;
  protected $object;
  protected $associatedobject;
  protected $crud;
  protected $level;
 
  // Event variables.
  protected $time;
  protected $contextid;
  protected $categoryid;
  protected $categoryname;
  protected $courseid;
  protected $coursename;
  protected $cmid;
  protected $cmname;
  protected $userid;
  protected $realuserid;
  protected $useripaddress;
  protected $objectid;
  protected $objecturl;
  protected $associatedobjectid;
  protected $associatedobjecturl;
  protected $currenturl;
  protected $data;
 
  // Legacy variables.
  protected $legacyeventname;
 
 
  /**
   * Final constructor.
   */
  protected final function __construct() {
    // Automatic values.
    $this->name = get_class($this);
    $this->component = strstr($this->name, '_event_', true);
    $this->level = self::LEVEL_NORMAL;
  }
 
  /**
   * Magic getter to make the properties publically readable.
   *
   * @param string $name of the property.
   * @return mixed value of the property or null if not found.
   */
  public function __get($name) {
    if (isset($this->{$name})) {
      return $this->{$name};
    }
    return null;
  }
 
  /**
   * Define whether a user can view the event details or not.
   *
   * @param int $userid ID of the user.
   * @return is_bool True if the user can.
   */
  abstract public function can_view($userid = null);
 
  /**
   * Create and return an instance of the event.
   *
   * @param array $args array of properties and their values.
   * @return object event obbject.
   */
  public static final function create($args = array()) {
    $args = (array) $args;
    $event = new static();
 
    // This validation is not necessary, but this is nice way of preventing typos or overwrite of 'constants'
    // values such as 'action', 'object', etc...
    $validprops = array('time' => 0, 'contextid' => 0, 'courseid' => 0, 'cmid' => 0, 'userid' => 0, 'realuserid' => 0,
      'useripaddress' => 0, 'objectid', 'associatedobjectid' => 0, 'data' => 0, 'currenturl' => 0, 'legacyname' => 0,
      'legacydata' => 0, 'legacylog' => 0, 'categoryname' => 0, 'coursename' => 0, 'cmname' => 0, 'objecturl' => 0,
      'associatedobjecturl' => 0);
    $allowed = array_intersect_key($args, $validprops);
    if (count($allowed) !== count($args)) {
      // Errors, some properties were filtered.
    }
    foreach ($allowed as $key => $val) {
      $event->{$key} = $val;
    }
 
    // Or this, without validation.
    //
    // if (isset($args['time'])) {
    //   $event->time = $args['time'];
    // }
    // if (isset($args['contextid'])) {
    //   $event->contextid = $args['contextid'];
    // }
    // if (isset($args['courseid'])) {
    //   $event->courseid = $args['courseid'];
    // }
    // if (isset($args['cmid'])) {
    //   $event->cmid = $args['cmid'];
    // }
    // if (isset($args['userid'])) {
    //   $event->userid = $args['userid'];
    // }
    // if (isset($args['realuserid'])) {
    //   $event->realuserid = $args['realuserid'];
    // }
    // if (isset($args['useripaddress'])) {
    //   $event->useripaddress = $args['useripaddress'];
    // }
    // if (isset($args['objectid'])) {
    //   $event->objectid = $args['objectid'];
    // }
    // if (isset($args['associatedobjectid'])) {
    //   $event->associatedobjectid = $args['associatedobjectid'];
    // }
    // if (isset($args['data'])) {
    //   $event->data = $args['data'];
    // }
    // if (isset($args['currenturl'])) {
    //   $event->currenturl = $args['currenturl'];
    // }
    // if (isset($args['categoryname'])) {
    //     $event->categoryname = $args['categoryname'];
    // }
    // if (isset($args['coursename'])) {
    //     $event->coursename = $args['coursename'];
    // }
    // if (isset($args['cmname'])) {
    //     $event->cmname = $args['cmname'];
    // }
    // if (isset($args['objecturl'])) {
    //     $event->objecturl = $args['objecturl'];
    // }
    // if (isset($args['associatedobjecturl'])) {
    //     $event->associatedobjecturl = $args['associatedobjecturl'];
    // }
 
    $event->init();
    return $event;
  }
 
  /**
   * Initialise the event.
   *
   * This is used to fill up the default values of the event upon creation.
   *
   * @return void
   */
  protected function init() {
    global $PAGE, $USER;
 
    // Default values.
    if (empty($this->time)) {
      $this->time = time();
    }
 
    if (empty($this->contextid)) {
      $this->contextid = $PAGE->context->id;
    }
 
    if (empty($this->courseid)) {
      $this->courseid = $PAGE->course->id;
    }
 
    if (empty($this->cmid) && !empty($PAGE->cm)) {
      $this->cmid = $PAGE->cm->id;
    }
 
    if (empty($this->userid) && !is_numeric($this->userid)) {
      if (CLI_SCRIPT) {
        $this->userid = -1;
      } else {
        $this->userid = isloggedin() ? $USER->id: 0;
      }
    }
 
    if ($this->userid === $USER->id) {
      $this->realuserid = session_is_loggedinas() ? session_get_realuser()->id : 0;
    }
 
    if (empty($this->useripaddress)) {
      $this->useripaddress = getremoteaddr();
    }
 
    if (empty($this->currenturl)) {
      $this->currenturl = $PAGE->url->out(false);
    }
  }
 
  /**
   * Get all the users affected by the event.
   *
   * @return array list of user IDs.
   */
  public function get_all_affected_users() {
    if (!$this->triggered || !$this->restored) {
      // Error.
    }
    $users = array();
    if ($this->userid > 0) {
      $users[] = $this->userid;
    }
    if ((!empty($this->objectid) || is_numeric($this->objectid)) && ($this->object == 'user' || $this->object == 'member')) {
      $users[] = $this->objectid;
    }
    if ((!empty($this->associatedobjectid) || is_numeric($this->associatedobjectid)) &&
        ($this->associatedobject == 'user' || $this->associatedobject == 'member')) {
      $users[] = $this->associatedobjectid;
    }
    return array_unique($users);
  }
 
  /**
   * Human readable localised description of the event.
   *
   * Example: "A user created a new topic in a forum".
   *
   * @return string event description.
   */
  abstract public static function get_description();
 
 
  /**
   * Returns legacy event name if existed.
   *
   * @return string
   */
  public function get_legacy_eventname() {
    return $this->legacyeventname;
  }
 
  /**
   * Override when legacy event contained data.
   *
   * @return mixed
   */
  public function get_legacy_eventdata() {
    return null;
  }
 
  /**
   * Override when there is a matching add_to_log() in old code.
   *
   * This is constructed using existing the data available in the event,
   * or by doing queries to get more information.
   *
   * @return array|null
   */
  public function get_legacy_logdata() {
    return null;
  }
 
  /**
   * Human readable localised message about the event that happened.
   *
   * Example: User 'John' created a topic called 'Awesome!'.
   *
   * @return string event message.
   */
  abstract public function get_message();
 
  /**
   * Human readable localised name of the event.
   *
   * Example: 'Forum topic created'
   *
   * @return string name of the event.
   */
  abstract public static function get_name();
 
  /**
   * Restore an event by passing its original data.
   *
   * @param array|object $args event properties to restore.
   * @return object event object.
   */
  public static final function restore($args) {
    $args = (array) $args;
    $event = new static();
 
    // Let's restore each possible property. It is possible that an event definition has changed since
    // we logged it, and so it's better to restore it as it was at the time we saved it.
    foreach ($args as $key => $val) {
      $event->{$key} = $val;
    }
 
    $event->restored = true;
    $event->triggered = true;
    return $event;
  }
 
  /**
   * Validation of mandatory fields
   *
   * This cannot be overriden by the child classes.
   *
   * @throws Exception
   * @return void
   */
  private final function strict_validation() {
    // Errors.
    if (empty($this->action)) {
      // Error.
    }
    if (empty($this->crud)) {
      // Error.
    }
 
    if ((!empty($this->objectid) || is_numeric($this->objectid)) && empty($this->object)) {
      // Error, the object should have been defined in the child level.
    }
 
    if ((!empty($this->associatedobjectid) || is_numeric($this->associatedobjectid)) && empty($this->associatedobject)) {
      // Error, the object should have been defined in the child level.
    }
 
    if (!empty($this->data) && (!is_array($this->data) || !is_object($this->data))) {
      // Error.
    }
  }
 
  /**
   * Trigger the event.
   *
   * This is not possible on restored events.
   *
   * @return [type] [description]
   */
  public final function trigger() {
    if ($this->restored || $this->triggered) {
      // Error.
    }
    $this->strict_validation();
    $this->validate();
    $this->triggered = true;
 
    if (!empty($CFG->loglifetime)) {
      if ($data = $this->get_legacy_logdata()) {
        call_user_func_array('add_to_log', $data);
      }
    }
 
    core_event_manager::dispatch($this);
 
    if ($legacyeventname = $this->get_legacy_eventname()) {
      events_trigger($legacyeventname, $this->get_legacy_eventdata());
    }
  }
 
  /**
   * Extra validation to add to the event.
   *
   * This should throw an exception.
   *
   * @return void
   */
  protected function validate() {
  }
 
  /**
   * Get iterator.
   *
   * @return ArrayIterator
   */
  public function getIterator() {
    $properties = array();
    foreach (get_object_vars($this) as $key => $value) {
      $properties[$key] = $value;
    }
    return new ArrayIterator($properties);
  }
}

Note that constructor can not be overriden. This was we can be sure that when restoring object from log we do not accidentally fill the properties with any values from current environment ($USER, $PAGE, etc.).

Properties that must be set on a class level

Those properties cannot be passed while triggering the event because they are considered as constants

  • action
  • object
  • associatedobject
  • crud
  • level

If need be, those properties can be defined in the init() method, for example level needs to refer to a base class constant LEVEL_xxx.

Methods that must or may be overwritten

init()

Method called when the event is created. Plugins can this to populate some properties as they need to upon creation. They should still call the parent method which already includes some logic to guess properties from $USER->id, $PAGE->get_course(), etc. No DB queries or expensive operations should be used here for performance reasons.

get_name() [static]

Static function returining human-readable localised event name. It will usually be called by reports displaying the logs or plugins that allow to configure logged event types. Recommended code:

return new lang_string('event_eventname', 'pluginname');

get_description() [static]

Static function returning human-readable localised event description. Recommended code:

return new lang_string('event_eventname_desc', 'pluginname');

can_view($user = null)

Allows to check if the user has capability to view this event (usually executed on event object restored from log).

get_message()

Method returning a human readable and localised string about what happened, using the event properties. Example: Fred deleted the blog post named 'Blah'.

get_all_affected_users()

Some events might affect multiple users (users in group, in course, bulk operation), but this information could be hard to compute. This method could be used by observers to retrieve additional information knowing that this can have a heavy cost! But this could also just return some information from the $data parameter, as only the event knows its structure. If overridden, it should call the parent method which includes the users when they are subject, object or associated object.

validate()

Used to add some extra validation. For example to add some required fields to certain event.

get_legacy_log_data()

Returns legacy log information if exists. To be used only if legacy logging enabled.

Events naming convention

Clear event names help developers reading what events are triggered and defining the events properties when defining the event class.

Decision: <component>_event_<object2>_<object>_<verb>

Structure of existing events

List of existing events called in 2.5, along with their future name and the decomposition of the new name into action, object and associated object.

2.5 name New name Subject Action Object <relationship> Object 2 Comment
activity_completion_changed core_event_activity_completion_updated Someone changed completion of activity
assessable_content_uploaded mod_*_event_assessablecontent_uploaded Someone uploaded assessablecontent
assessable_files_done mod_*_event_assessablecontent_processed Someone processed assessable content
assessable_file_uploaded Someone uploaded assessable_file To be deprecated MDL-35197
assessable_submitted mod_*_event_assessablecontent_submitted Someone submitted assessable content
blog_entry_added mod_blog_event_entry_created Someone added entry
blog_entry_deleted mod_blog_event_entry_deleted Someone deleted entry
blog_entry_edited mod_blog_event_entry_updated Someone updated entry
cohort_added core_event_cohort_created Someone created cohort
cohort_deleted core_event_cohort_deleted Someone deleted cohort
cohort_updated core_event_cohort_updated Someone updated cohort
cohort_member_added core_event_cohort_member_added Someone added member to cohort
cohort_member_removed core_event_cohort_member_deleted Someone deleted member from cohort
course_category_deleted core_event_coursecat_deleted Someone deleted coursecat
course_completed core_event_course_completed Someone completed course
course_content_removed core_event_course_purged Someone purged course
course_created core_event_course_created Someone created course
course_deleted core_event_course_deleted Someone deleted course
course_restored core_event_course_restored Someone restored course
course_updated core_event_course_updated Someone updated course
groups_group_created core_event_group_created Someone created group
groups_group_deleted core_event_group_deleted Someone deleted group
groups_grouping_created core_event_grouping_created Someone created grouping
groups_grouping_deleted core_event_grouping_deleted Someone deleted grouping
groups_groupings_deleted Someone deleted groupings To deprecate
groups_groupings_groups_removed Someone removed groups from groupings To deprecate
groups_grouping_updated core_event_grouping_updated Someone updated grouping
groups_groups_deleted Someone deleted groups To deprecate
groups_group_updated core_event_group_updated Someone updated group
groups_member_added core_event_group_member_added Someone added member to group
groups_member_removed core_event_group_member_deleted Someone deleted member from group
groups_members_removed Someone removed members from group To deprecate
lti_unknown_service_api_call mod_lti_event_unknownservice_called Someone called unknown service
mod_created core_event_module_created Someone created module
mod_deleted core_event_module_deleted Someone deleted module
portfolio_send This is a hack...
role_assigned core_event_user_role_added Someone added role to user Or ..._role_assigned
role_unassigned core_event_user_role_removed Someone removed role from user Or ..._role_unassigned
quiz_attempt_started mod_quiz_event_attempt_started Someone started attempt of quiz
test_cron
test_instant
user_created core_event_user_created Someone created user
user_deleted core_event_user_deleted Someone deleted user
user_enrolled core_event_user_enrolment_created Someone added enrolment of user
user_enrol_modified core_event_user_enrolment_updated Someone updated enrolment of user
user_unenrolled core_event_user_enrolment_deleted Someone removed enrolment of user
user_logout core_event_loggedout Someone loggedout
user_updated core_event_user_updated Someone updated user
workshop_viewed mod_workshop_event_viewed Someone viewed workshop

Naming suggestion

<component>_event_<object2>_<object>_<verb>
  • component_event_user_created
  • component_event_activity_completion_changed
  • component_event_user_enrolment_created or user_enrolled
  • component_event_user_enrolment_removed or user_unenrolled
  • component_event_user_enrolment_modified
  • component_event_cohort_member_added
  • component_event_group_added
  • component_event_grouping_group_added
  • component_event_user_message_sent
  • component_event_users_imported

So it becomes readable like this:

<subject> <action> [a/the] <object> [in/of/from/to] <object2>
  • System created a user
  • System changed the completion of activity
  • System created an enrolment for user
  • System modified the enrolment of user
  • System added a member to cohort
  • System added group
  • System added group to grouping
  • System sent message to user
  • System imported users

Rules

No subject/Use of user

The subject is never part of the event name. We never add user to the event name, except when the action is taken on the user:

A user logged in: core_event_loggedin
A user is created: core_event_user_created
A role is assigned to a user: core_event_user_role_assigned
A user updated is profile: core_event_profile_updated
A user accessed his My Home: core_event_myhome_viewed

No course as associated object

It is not often necessary to add the course as the associated object. It does not add value to the event, and the event property $courseid will be enough to retrieve the information.

INCORRECT: core_event_course_section_moved
INCORRECT: core_event_course_user_enrolled
CORRECT: core_event_section_moved
CORRECT: core_event_user_enrolled

Use singular

Plural must be used on objects when it's a One to Many relationship. Ex: bulk import, mass deletion, ... In any other case, use singular.

No underscores

In order to clearly identify what is what in the event name, underscores should only be used to separate the event name elements.

CORRECT: core_event_assessablecontent_uploaded
         (Someone uploaded assessablecontent)
INCORRECT: core_event_assessable_content_uploaded
           (Someone uploaded content of assessable)

One to many

Decision: Each event should have a one to one relationship. We can reconsider this at a later stage, if the performance hit is exteremely high.

In 2.5, some events are triggered when an action happens on multiple objects. We have to decide whether we want to keep supporting One to Many events or not.

Keeping list of all changes for multiple actions may be problematic because you would have to keep them all in memory untill all things are processed. This might also result in incorrect order of events. The only correct solution seems to be to trigger each item individually and the many thing at the end. Performance needs to be improved elsewhere...

Accuracy

When uploading a bunch of users using the CSV upload feature, if only one event is triggered, it means that the observers of user_created won't be triggered. And so some functionalities can be lost as, as a plugin developer, I expect this user_created to be triggered regardless of the way they have been uploaded. Of course, the developer could observe the event bulk_user_imported, but that means that he could miss some relevant observers.

This applies to existing events.

Performance

Triggering one event is cheaper then repeating the same events x times...

Information tracking

A bulk event, might not be verbose enough to allow for proper logging afterwards. Though this is the responsability of the logger, we probably want to make it easy to store relevant information.

Double event

In the case of a bulk user import, if we were to trigger an event per user created, we probably want to trigger one event 'user_bulk_upload_started' when the action starts.

Example events

Assignment

Assumption: Course contains groups with students in each group.

1. Teacher creates an assignment with group mode set to 'Separate groups' and Feedback type set to comments and files.

  • Event: User 'Teacher' has created assignment 'B' in course 'C101'.

2. A student views the assignment.

  • Event: User 'Student' has viewed assignment 'B' in course 'C101'.

3. A member from one of the groups submits an assignment

  • Event: User 'Student' has added a submission for assignment 'B' for group 'C' in course 'C101'.
  • Event: Email sent to the teacher and all students in that group.

4. User 'Adrian' adds some changes to the assignment and updates it.

  • Event: User 'Adrian' has updated the submission for assignment 'B' for group 'C' in course 'C101'.
  • Event: Email sent to the teacher and all students in that group.

5. Teacher views the assignment.

  • Event: User 'Teacher' has viewed assignment 'B' in course 'C101'.

6. Teacher clicks on 'View/grade all submissions'

  • Event: User 'Teacher' has viewed the assignment 'B' grade area in course 'C101'.

7. Teacher clicks to grade the student's submission.

  • Event: User 'Teacher' has viewed the submission for user 'student' for assignment 'B' in course 'C101'.

8. Teacher marks the assignment with the setting 'Apply grades and feedback to entire group' set to 'Yes' leaving a comment and a file.

  • Event: User 'Teacher' has marked assignment 'B' for group 'C' in course 'C101'.
  • Event: User 'Teacher' has left a comment for assignment 'B' for group 'C' in course 'C101'.
  • Event: User 'Teacher' has uploaded a feedback file for assignment 'B' for group 'C' in course 'C101'.
  • Event: User 'Teacher' has uploaded a file to the course 'C101'.
  • Event: Email sent to user 'Student' notifying them their submission for assignment 'B' has been marked. - This is done for all users in the group.

9. User 'Adrian' views the feedback.

  • Event: User 'Adrian' has viewed assignment 'B' in course 'C101'.

10. User 'Adrian' opens the feedback file.

  • Event: User 'Adrian' has viewed the file 'A' for assignment 'B' in course 'C101'.

11. User 'Adrian' adds some changes to the assignment insulting the teachers marking and updates it.

  • Event: User 'Adrian' has updated the submission for assignment 'B' for group 'C' in course 'C101'.
  • Event: Email sent to user 'Teacher' notifying them that user 'Adrian' has updated the submission for assignment 'B'.
  • Event: Email sent to user 'Student' notifying them their submission for assignment 'B' has been updated. - This is done for all users in the group.

12. The teacher clicks directly on the link in the email to be taken to the grading page.

  • Event: User 'Teacher' has viewed the submission for user 'student' for assignment 'B' in course 'C101'.

13. The teacher is upset due to the harsh comments and decides to mark Adrian down, but not the rest of the group by setting 'Apply grades and feedback to entire group' set to 'No'.

  • Event: User 'Teacher' has marked assignment 'B' for user 'Adrian' in course 'C101'.
  • Event: User 'Teacher' has left a comment for assignment 'B' for user 'Adrian' in course 'C101'.
  • Event: User 'Teacher' has uploaded a feedback file for assignment 'B' for user 'Adrian' in course 'C101'.
  • Event: Email sent to user 'Adrian' notifying them their submission for assignment 'B' has been marked.

Code example

mod/assign/lib.php

// Creating an event handler class for the following event:
// Event: User 'Teacher' has created assignment 'B' in course 'C101'. 
 
...
 
    $realuser = session_get_realuser();
    $value = new stdClass();
    $value->contextid = $context->id;
    $value->courseid = $courseid;
    $value->cmid = $cm;
    $value->userid = $USER->id;
    $value->realuserid = $realuser->id;
    $value->objectid = $assign->id; // Assignment ID
    $event = mod_assign_event_assignment_added::create($value);
    $event->trigger()
 
...

mod/assign/classes/event/assignment/added.php

class mod_assign_event_assignment_added extends core_event_base {
    /**
     * @var string $component.
     */
    protected $component = 'mod_assign';
     /**
     * @var string $action.
     */
    protected $action = 'created';
    /**
     * @var string $crud.
     */
    protected $crud = 'c'
    /**
     * @var string $object What is the object being affected in this action?
     */
    protected $object = 'assignment'
 
    public static function get_name() {
        return get_string('event_assignment_created', 'assign');
    }
 
    public static function get_description() {
        return get_string('event_assignment_created_desc', 'assign');
    }
 
    public static function can_view($userid = null) {
	$context = context_user::instance($userid);
	return has_capability('mod/assign:view', $context);
    }
 
    public static function get_message() {
	return get_string('event_assignment_created_message', 'assign');
    }
 
}

Development stages

Stage 1

  • Finish class loader spec and implement basic Frankenstyle class loader
  • Describe new handler definition and new db table fields (just few new flags in current db/events.php)
  • Describe new event dispatcher
  • Describe core_event_base class
  • Current (= legacy) events triggering:
    • Refactor current event handling code to new self-contained class - do not change functionality, keep events_trigger()
    • Create new event handler management class that deals with installation and upgrades of both legacy and new handlers.
  • New events:
    • Create new core_event_base class
    • Create new self-contained event dispatcher class with '*' handler support
    • In function core_event_base::trigger() check if the event has property 'legacyeventname' execute events_trigger($this->legacyeventname, $this->legacyeventdata) after triggering new event.
    • Write unit tests for all new events code
  • Do not change anything in current logging yet.

After completing this stage everything should continue to work as it did before and we can start parallel work on further stages.

Stage 2 (requires completion of Stage 1)

  • Create event classes and replace existing calls to events_trigger() and with new event classes containing legacy information properties
  • Add more events throughtout the standard Moodle package in places where we have add_to_log(). Implement some_event::get_legacy_log_data() which reuirns original parameters of add_to_log() and remove it. Old add_to_log()t is called in core_event_base::trigger() automatically with original parameters.
  • Add even more new events all over the place.

The difficult part is defining the new event classes properly because we must not change them after 2.6 release.

Stage 3 (requires partial completion of Stage 2)

  • Migrate current legacy event handlers to new handlers with one event class instace parameter, ex.: enrol plugins.

Stage 4 (requires partial completion of Stage 2)

  • Implement event logging handler
  • Implement logging storages
  • Define logging apis
  • Create new reports
  • Switch to new logging everywhere after Stage 2 completed and new reports are usable

See Logging 2

Stage 5

  • Decide how much backwards compatibility we want for old log table. Most probably they get only legacy log data.
  • Implement some BC solution for old code that reads log tables directly.

See Logging 2

Stage 6 (requires completion of Stage 4 and 5)

Moodle 2.8dev? This is the ultimate end of old logging via log table.

  • Deprecate add_to_log() function with debug message and do nothing inside.
  • Remove all legacy logging from event classes.

See Logging 2