Event 2

Revision as of 08:20, 24 May 2013 by Frédéric Massart (talk | contribs) (Events naming convention)

Jump to: navigation, search

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.

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 first step is to make Events more specific.
  • It will be possible to subscribe to '*' event, which means to potentially observe, and selectively deal with, all events.
  • 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 overloading 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.

Triggering events

  • All event descriptions are objects extending event_base (which is defined in core)
  • Each event class name is a unique identifier of the event
  • Class names will follow the identifier scheme pluginfullname_event_something_happened. Core events will have prefix 'core'.
  • Plugins define each event class in separate file. File name 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: moodle_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.

Examples of triggering an event:

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

or

$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();

Backward compatibility

  • moodle core and standard plugins will replace all usages of events_trigger() and add_to_log() with proper events
  • 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')
  • For events that substitute add_to_log() calls the additional property 'legacylog' will be specified.
  • Function events_trigger() will create and trigger an instance of event_legacy class with non-empty 'legacyeventname' property and optional 'legacyeventdata'
  • Function add_to_log() will create and trigger an instance of event_legacy class with non-empty 'legacylog' property

Handling (observing) events

  • Event handlers can be described as it is done now in plugindir/db/events.php, this file is parsed during install/upgrade of plugin and all handlers are removed on uninstall
  • It is possible to subscribe to all events (*)
  • If event handler refers to the old (2.5) name of event, it will be used only for events that have corresponding 'legacyeventname' property. If it refers to 2.6 event class name it will be used with full data
  • It is also possible to dynamically register/unregister handlers but they are not recommended (same as it is with dynamic caches definitions). Ideally they should only be used in unit and/or behat tests

Handlers (observers) sequence

  • Event handlers can also have an attribute 'sortorder' (positive or negative, default 0).
  • User with appropriate capability can overwrite the sequence of handlers for each event type

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
  • Legacy log data legacylog Array of the former add_to_log() paremeters

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.

event_base class

/**
 * Base event abstract class.
 */
abstract class 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 $legacyname;
  protected $legacydata;
  protected $legacylog;
 
  /**
   * 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'];
    // }
    // if (isset($args['legacyname'])) {
    //   $event->legacyname = $args['legacyname'];
    // }
    // if (isset($args['legacydata'])) {
    //   $event->legacydata = $args['legacydata'];
    // }
    // if (isset($args['legacylog'])) {
    //   $event->legacylog = $args['legacylog'];
    // }
 
    $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();
 
  /**
   * 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.
    }
    if (!empty($this->legacydata) && (!is_array($this->legacydata) || !is_object($this->legacydata))) {
      // Error.
    }
    if (!empty($this->legacylog) && (!is_array($this->legacylog) || !is_object($this->legacylog))) {
      // 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;
    // Now send to the observers.
    // ...
  }
 
  /**
   * 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.

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 core_event_workshop_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_course_moved

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 uploadded assessablecontent)
INCORRECT: core_event_assessable_content_uploaded
           (Someone uploaded content of assessable)

Event dispatching system

Use of php autoloading

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 (Two plugins with same name)
  • 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)
  • Integrating third party libraries will be an overhead, if class name conflict.
  • 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.
  • Can cause issues with obfuscater or zend guard.

Personally for events, it might be nice to use autoload functionality, as it will avoid tracking dependency and load class when needed.

We can use PSR-0 standard or code below to achieve this.

spl_autoload_register(function ($classname) {
    global $CFG;
    if ($pluginfullname = stristr($classname, '_event_', true)) {
        $eventfile = $CFG->dirroot.'/'.str_replace('_', '/', $classname).'.php';
        if (is_readable($eventfile)) {
            require_once $eventfile;
        }
    }
});

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 event intiating a generic event 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
  • Automatically lists events without being installed
  • Inlcuded only when needed using autoloading
  • Self-validating data structure (by PHP)

Cons

  • Big learning curve, hard to copy-and-paste, may reduce uptake of system
  • Potentially can create invalid events structures, no value validation (are we setting false expectations?)

Each plugin defines events in a list based on generic object Pros

  • Simpler for regular developers
  • Complete control of event structure in core
  • Definition of all plugin events in one file, processed at install
  • Easy to call an event, consistent throughout all of Moodle

Cons

  • Requires upgrade to register an event
  • Would need to be maintained through DB tables
  • Can’t have programmatic control over events at plugin level

Related links

MDLSITE-2261

Unit testing

Shared events

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.

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. Student '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. Student '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

// 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
    core_event_assignment_created::create_and_trigger($value);
 
...
 
class core_event_assignment_created extends event_base {
    // ... constants
    /**
     * @var string $type.
     */
    protected $type = 'User action';
    /**
     * @var string $component.
     */
    protected $component = 'mod_assign';
     /**
     * @var string $action.
     */
    protected $action = 'created';
    /**
     * @var string $transactiontype
     */
    protected $transactiontype = 'create'
    /**
     * @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');
    }
}

Development stages

Stage 1

  • Describe classes event_base and event_legacy
  • Current events triggering:
    • Rename function events_trigger() to events_trigger_old()
    • Create function events_trigger() that will create the instance of event_legacy with filled properties 'legacyeventname' and 'legacyeventdata'
    • In function event_base::trigger() check in case the event has property 'legacyeventname' execute events_trigger_old($this->legacyeventname, $this->legacyeventdata)
  • Current adding to logs:
    • Rename function add_to_log() to add_to_log_old()
    • Create function add_to_log() that will create the instance of event_legacy with filled property 'legacylog'
    • In function event_base::trigger() check in case the event has property 'legacylog' execute add_to_log_old($this->legacylog['foo'], ...)
  • Make sure all unit tests continue to work

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 add_to_log() with proper event triggering (but still specifying properties 'legacy*')
  • Add more events throughtout the standard Moodle package

This is actually the most amount of work in this project

Stage 3 (requires completion of Stage 1)

  • Remove events_trigger_old() and re-write the existing event triggering system to have better performance and not query database so often
  • Allow handling of '*' (any) event

Stage 4 (requires completion of Stage 3)

  • Create plugin tool_loglegacy and move there all function querying / inserting into {log} table
  • The existing functions accessing table {log} (such as get_logs(), get_logs_usercourse(), etc.) should check if plugin is installed and call appropriate function from it
  • Make sure nobody else in core reads or writes from table {log}
  • Remove function add_to_log_old() and it's usage in event_base::trigger(), instead plugin should listen to '*' events and log events with non-empty legacylog
  • If any of standard plugins work with {log} table they should define the dependency on this plugin

Stage 5 (requires completion of Stage 4)

  • Create 3 plugins that listen to events, store the new events objects in new log table, query the new log table and return data
  • Rewrite existing reports and other standard plugins to use the new events system, remove dependency from tool_loglegacy