Note:

If you want to create a new page for developers, you should create it on the Moodle Developer Resource site.

Events API: Difference between revisions

From MoodleDocs
(Removing commitment to 2.6)
Line 160: Line 160:


<code php>
<code php>
abstract class event_base implements cacheable_object, IteratorAggregate {
abstract class event_base implements IteratorAggregate {
   // ... constants
   // ... constants
   // ... all properties as protected variables, magic function __get() for all properties
   // ... all properties as protected variables, magic function __get() for all properties
Line 190: Line 190:
   public function getIterator() {
   public function getIterator() {
     // ...
     // ...
  }
  public function prepare_to_cache() {
    // ... basic implementation caching all properties
    // plugins are recommended to overwrite with caching only non-static properties
  }
  public static function wake_from_cache($a) {
    $event = new self();
    // ... restore properties from $a
    return $event;
   }
   }
}
}

Revision as of 04:40, 22 May 2013

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.

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_eventname. Core events will have prefix 'moodle'.
  • Plugins define each event class in plugindir/events/xxx.php, where xxx is the identifier of the event.
  • The event identifier suffix (eventname in the example above) should be SUBJECT_VERB. There are recommendations for verbs to use in events names [1].

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.

[TODO] Describe how an event object is instantiated and triggered.

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 properties 'legacylogaction' and 'legacylogdata' 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 'legacylogaction' 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

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, I suggest to add a new event property "Associated users IDs" which is an array of User ID associated to this event. Therefore, an observer catching '*' can check if the following fields are related to the user we want:

  • Subject
  • Action
  • Associated object
  • Associated users

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
    • core/moodle
    • mod_assign
    • mod_workshop
    • qtype_match
  • Subject* subject (Defines the object on which the action is performed)
    • user
    • section
    • assignment
    • submission_phase
  • Subject ID* subjectid
  • Subject name subjectname (Human readable identifier of the object: Mark Johnson, Course ABC, ...)
  • Subject URL subjecturl (URL to notice the changes)
  • 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)
  • Actor* actor (user, cli, cron, ...)
  • Actor ID/User ID* actorid (ID assiacted to the actor, typically the user id)
  • Real actor/user ID* realactorid (When logged in as, store the real user ID)
  • Actor IP address* actorip
  • 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 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, probabl 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

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.

Real actor/User ID

This should be the real user when we are logged in as someone. Or the user ID of the person on behalf of whom we are performing an action, when running CLI or Cron.

Properties available through methods

Some properties will not be defined but could be returned using methods implemented in the base class, or in events:

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_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.

event_base class

abstract class event_base implements IteratorAggregate {

 // ... constants
 // ... all properties as protected variables, magic function __get() for all properties
 protected function __construct() {
   // Here plugins can initialise all properties that are static for this event (CRUD, object, subject). No dynamic values here.
 }
 public static function create($args = array()) {
   $event = new self();
   // ... Fill the properties from $args
   // Plugins can overwrite with filling the default dynamic values such as $USER->id, $PAGE->get_course(), etc. No DB queries or expensive operations here.
   return $event;    
 }
 public static final function restore($object) {
   $event = new self();
   // .. restore each property from $object to $event
   return $event;
 }
 public static final function create_and_trigger($args = array()) {
   $event = self::create($args);
   $event->trigger();
 }
 public final function trigger() {
   // ... 
 }
 abstract public static function event_name();
 abstract public static function event_description();
 abstract public function can_view($user = null);
 abstract public function event_message();
 public function getIterator() {
   // ...
 }

}

Logging

[TODO] Shift this to the logging document.

This section is deliberately not called "Logging API" because Moodle does not provide any logging API. All plugins that perform logging and analyse logs to display reports can do it as they wish. Events API defines how to subscribe to events and use them.

  • In places where we previously added information to 'the' log we must generate the new event.
  • The standard Moodle distribution will include simple DB logging plugin storing the data in new format. There will also be an section (optional) legacy logging plugin that will store data in the old format in {log} table. All standard reports will be upgraded to use the new logging system that will use a logging retrieval plugin to access log information. Custom report plugins will be able to query the {log} table, but should ideally shift to the new logging system over time, so that organisations are not forced to continue double logging.

From event to report

There are four steps of how event is converted into report:

  1. Handling of events and filtering what needs to be logged ("What to log")
  2. Storing the events data in the log storage (DB, filesystem, etc.) ("How to log")
  3. Retriving the data from log storage ("How to retrieve")
  4. Displaying the data in form of the report ("How to display")

One plugin can cover one or several steps. It is also possible to create a report that has built-in logging and listens to events (all 4 steps). Also external logging systems do not need to care about steps 3 and 4 at all.

Moodle standard distribution provides a suggestion on logging-report chain that can be followed by 3rd party plugins and may be not.

Logging plugins relation

Standard logging plugins

Standard Moodle 2.6 distribution will include 4 plugins:

Legacy logging plugin (tool_loglegacy)

Covers steps 1-3 from above. Listens to all events, for events that have non-empty 'legacylogaction' property adds the data to {log} table using the 'legacylogdata' object.

Core functions get_logs(), get_logs_usercourse(), get_logs_userday() will be checking if this plugin is installed and redirect to its appropriate functions.

All report plugins that use table {log} should use this plugin.

Event logging handling plugin (tool_eventobserver)

Responsible for step 1 from above. Has functions pluginname_register_log_instance() and pluginname_unregister_log_instance() that allow to add/remove log storage instance and also allows admin to configure what kind of events to store for each log instance.

Event logging DB storage plugin (tool_logdbstorage)

Responsible for step 2 from above. Has function pluginname_get_log_instances(). Allows admin to create storage instances and assign event handling plugin to fill each of them

Event logging driver plugin (tool_logdbdriver)

Responsible for step 3 from above. This plugin can only work with tool_logdbstorage. It provides functions to access data in log.