Difference between revisions of "Event 2"

From MoodleDocs
Jump to: navigation, search
(Properties)
(Properties rejected)
(3 intermediate revisions by one user not shown)
Line 235: Line 235:
 
! Comment
 
! Comment
 
|-
 
|-
| '''''name'''''
+
| '''''eventname'''''
 
| Event name
 
| Event name
 
| ''static mandatory''
 
| ''static mandatory''
 
| Automatically computed by copying class name
 
| Automatically computed by copying class name
|-
 
| '''''crud'''''
 
| Transaction type
 
| ''static mandatory''
 
| Statically declared in the event class
 
 
|-
 
|-
 
| '''''component'''''
 
| '''''component'''''
Line 254: Line 249:
 
| ''static mandatory''
 
| ''static mandatory''
 
| Can be automatically computed from class name.
 
| Can be automatically computed from class name.
 +
|-
 +
| '''''crud'''''
 +
| Transaction type
 +
| ''static mandatory''
 +
| Statically declared in the event class
 
|-
 
|-
 
| '''''level'''''
 
| '''''level'''''
Line 310: Line 310:
 
| Is this action related to some user? This could be used for some personal timeline view.
 
| Is this action related to some user? This could be used for some personal timeline view.
 
|-
 
|-
| extradata
+
| extra
 
| Extra data
 
| Extra data
 
|
 
|
Line 408: Line 408:
 
| Course module name
 
| Course module name
 
| Might be costly to retrieve for little gain
 
| Might be costly to retrieve for little gain
 +
|-
 +
| categoryid
 +
| Course category id
 +
| Categories are a tree structure, we can not identify them by one integer. It would have to be a path.
 +
|-
 +
| cmid
 +
| Course module id
 +
| Can be derived from contextlevel and contextinstanceid
 
|}
 
|}
  

Revision as of 17:13, 29 June 2013

Note: This article is a work in progress. Please use the page comments or an appropriate moodle.org forum for any recommendations/suggestions for improvement.

Events 2
Project state In early specification
Tracker issue MDL-39797 , MDL-39952
Discussion https://moodle.org/mod/forum/discuss.php?d=229425
Assignee Backend Team


Contents

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 if we want to use them for new logging and other advanced use cases. They need to contain a lot more information in a standardised way (such as most fields from current log table and log_actions table).
  • Complex data types were allowed in old events which was causing major problems when serialising/storing/unserializing the data.
  • The logging and events contain similar information and are tiggered at the same place, new events would remove this code duplication. All events should be loggable and all current log info should be triggered as events.
  • The logging system will become an event handler, observing events and directing them to logging storage plugins in a controllable way.
  • It will be possible to subscribe to '*' event, which would allow a system to potentially observe, and selectively deal with, all events. Current handlers do not get event name which makes this problematic.
  • Current event handlers may trigger exceptions during site upgrade which would lead to fatal upgrade problems. The new design eliminates this.
  • Failure in handlers blocked dispatching of subsequent events. Instead problems in new observers would be only logged and execution would continue normally.
  • Current execution of external handlers during DB transactions blocks other handlers. This would be eliminated by in-memory buffer for external events.
  • It would good to have observer priority.
  • Nested events are not dispatched sequentially, it would change the order of events received in lower priority handlers.

Performance

Some basic profiling has been conducted.

There is a general plan to complete pre- and post-implementation testing as development happens. The new system will be imemented in parallel with the old one which should help with comparison of new and old logging performance on each page.

Our aim is to trigger more events and log more information, which is going to impact on performance. We hope to offset that impact by improving log storage, simplifying event dispatching and adding other core performance improvements. The proposed class structure of the base event should allow some new advanced techniques, which may also improve performance in some scenarios.

More details will be added to this section soon.

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 calls to the events_trigger() function 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'.
  • The function events_trigger() will continue working as before, but it will be called automatically after a 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

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

Event observers

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

  • eventclass - event class name or "*" indicating all events.
  • callback - 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 a successful commit of the transaction.
$observers = array(
 
array(
'eventclass' => 'core_event_sample_executed',
'callback' => 'core_event_sample_observer::observe_one',
),
 
array(
'eventclass' => 'core_event_sample_executed',
'callback' => 'core_event_sample_observer::external_observer',
'priority' => 200,
'internal' => false,
),
 
array(
'eventclass' => '*',
'callback' => 'core_event_sample_observer::observe_all',
'includefile' => null,
'internal' => true,
'priority' => 9999,
),
 
);

Event dispatching

A 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. There is no risk of performance regression because the list is already cached in MUC and we could even improve it by caching it in pure php file in dataroot. Observers get events before installation or any upgrade, however observers are not notified during the initial installation of moodle core tables.

Developers of observers must make sure that execution does not end with a fatal error under any condition (before install, before upgrade or normal operation). Exceptions are automatically captured, logged in the PHP error log, and notification of other observers continues. Current handlers must not throw any exceptions at any time.

Observers are notified sequentially in the same order in which events were triggered. This 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 processed in a different order.
  6. External events are buffered when a transaction is in progress, instead of being sent to the cron queue.
  7. It is possible to define multiple observers for one event in one events.php file.
  8. It is possible to subscribe an observer to all events.
  9. The new event manager is using frankenstyle autoloading - smaller memory footprint when events are not used on the 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 the core_event_base class.
  • Events are triggered by creating a new instance of the 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 a separate file. File name and location must match the 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.

Decision:Recommended verb list

Examples: core_event_course_completed, mod_assign_event_submission_commented, mod_forum_event_post_shared, mod_forum_event_post_responded etc.


  • Ideally, it should be possible to trigger an event without gathering additional information for the event. To reduce the 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.

An 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 saving).
  • Defer file inclusion to the 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 avoids dependency tracking in large projects like moodle.

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

  • It might break plugins not using the appropriate structure, or duplicate class names (not a problem if Frankenstyle rules used).
  • If we have lots of classes, then 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 structures will need the second one to wait till the first one either loads the class or decides that it does not recognize the class name. (Moodle classloader should be registered as the last one because it is the slowest).
  • It can cause issues with obfuscater or zend guard (Moodle does not support obfuscators).

Why separate classes?

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

Decision: Use separate class for each event.

Each plugin creates its own event class for each event

Pros

  • Maintainability - It is much easier to review, debug, integrate.
  • Self documenting, behaviour is combined with definition.
  • It is extremely flexible for plugin developers and core devs too.
  • Automatically lists events without being installed - PHPDocs as events documentation.
  • It is included only when needed using autoloading.
  • Self-validating data structure (by PHP).
  • Some developers will 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 a generic object

Pros

  • Easier for some developers.
  • More control of event structure in core (?).
  • Easy to call an event and is 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 it is 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 names as reliable identifiers.
  • Requires upgrade/install to register an event in DB table with MUC cache. Events could not be triggered earlier. The list of event definitions would be used for validation and prefilling of information when triggering event in multiple places, it would also contain extra info such as description strings.
  • The implementation of an events infrastructure would be significantly more complex and error prone.

Information contained in events

Events have to contain as many information as they can, but this should not affect the performances. That's why part of the information is available in properties, and the rest via methods. This allows for delaying the computation of the data at the time it is really needed, if it ever is.

Properties

List of properties that the developer has to pass to the event upon creation, or automatically generated when possible and cost free. Some of those properties not mandatory.

Property name Title Type Comment
eventname Event name static mandatory Automatically computed by copying class name
component Component static mandatory Component declaring the event, automatically computed from class name.
action Action static mandatory Can be automatically computed from class name.
crud Transaction type static mandatory Statically declared in the event class
level Level static mandatory Levels to be defined (eg: minor, normal, major)
object Object static Object on which the action is taken, can be automatically computed from class name. (There might not always be one, example: core_event_loggedin)
objectid Object ID Identifier of the object
associatedobject Associated object static Object associated to the main object. Ie: The user to whom a message is sent.
associatedobjectid Associated object ID Identifier of the associated object
contextid Context ID mandatory
contextlevel Context level automatic from context This tells you if it was a course, activity, course category, etc.
contextinstanceid Context instanceid automatic from context Based on context level this may be course id , course module id, course category, etc.
userid User ID mandatory User ID, or 0 when not logged in, or -1 when other (System, CLI, Cron, ...)
courseid Affected course automatic from context This is used only for contexts at and bellow course level - this can be used to filter events by course (includes all course activities)
affecteduserid Affected user Is this action related to some user? This could be used for some personal timeline view.
extra Extra data Any extra fields specific to the event - scalars or arrays, must be serialisable using json_encode()
realuserid Real User ID automatic User who "logged in as", stores the real user ID
origin Origin of the event automatic User, CLI, cron, Webservice, ... (optionally with IP address)
timecreated Time of the event automatic
  • static: Cannot be set with the information passed to the event when instantiated.
  • mandatory: Is required in order to trigger the event.

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 detail 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 to be unreliable.

Methods

The computation of this data is not required by default, but can be accessed by any event observer if need be.

Method Comment
get_all_affected_users() Returns all the users affected by this event
get_objecturl() Returns the URL to view the object
get_associatedobjecturl() Returns the URL to view the associated object
get_currenturl() Returns the current URL, uses $PAGE.
get_useripaddress() Returns the User IP address
get_legacyeventdata() Returns the data of the legacy event
get_legacylogdata() Returns the data of the legacy log

Properties rejected

Name Title Why
version Event specification number The event specification should never change
type Type of event (action, error, ...) Only 'action' is supported as this stage, so not required
actor Whether current execution is cron, cli, user, ... Impossible to track down at a low level
version Event specification number The event specification should never change
severity Severity following logging standards Our logging does not match this, as we will not (at present) log errors
coursecatname Category name Might be costly to retrieve for little gain
coursename Course name Might be costly to retrieve for little gain
cmname Course module name Might be costly to retrieve for little gain
categoryid Course category id Categories are a tree structure, we can not identify them by one integer. It would have to be a path.
cmid Course module id Can be derived from contextlevel and contextinstanceid

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;
 
// These 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 publicly 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 a 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, 'legacyeventname' => 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'];
// }
// ...
 
$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 it exists.
*
* @return string
*/

public final function get_legacy_eventname() {
return $this->legacyeventname;
}
 
/**
* Override when legacy event contains 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 the data available in the event,
* or by doing extra queries to get more information. So calling this
* method can have a cost!
*
* @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 the constructor can not be overriden. This is so we can be sure that when restoring an object from a log we do not accidentally fill the properties with any values from the current environment ($USER, $PAGE, etc.).

Properties that must be set on a class level

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

  • action
  • object
  • associatedobject
  • crud
  • level

If need be, these 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()

This is the method called when the event is created. Plugins can use 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]

This is a static function returning a human-readable localised event name. It will usually be called by reports displaying the logs or plugins that allow configuration of 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 checking to see if the user has the capability to view this event (usually executed on event object restored from log).

get_message()

A Method for 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 an associated object.

validate()

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

get_legacy_log_data()

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

Events naming convention

Clear event names help developers when 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

Plurals must be used on objects when it's a One to Many relationship. Ex: bulk import, mass deletion, ... In any other case, use the 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)

Shared events

Decision: Not supported at this stage.

In Moodle 2.5 we have a good example of a shared event: 'assessable_content_uploaded' which is triggered in forum, 'assignment and workshop.

The problem with shared events is that we cannot easily track what component triggered them. Of course we could add a new property to the event to keep track of that, but we would soon need more information and more properties. Also, in the case of a logger, the event received would be unique, where in fact it should be considered different depending on the component firing it.

In our first implementation, we will create one specific event per module. This flexibility does not prevent any observer from capturing them, but still makes sure that the consistency and specificity of each event is maintained.

It could happen that some events are defined in core and shared, but this should not really happen as low-level APIs should trigger the event, and a module should call that low API instead of doing the job itself.

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 extremely 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 a list of all changes for multiple actions may be problematic because you would have to keep them all in memory until all things are processed. This might also result in the order of events being incorrect. The only correct solution seems to be to trigger each item individually and then many things 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 functionality 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 number of times...

Information tracking

A bulk event, might not be verbose enough to allow for proper logging afterwards. Though this is the responsibility 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.

Unit Testing

With unit testing for this system we want to assert the following:

  • That event strict validation and custom validation works.
  • Missing event data is auto filled with accurate data.
  • Typos in properties passed to ::create() are captured (if we decide to validate).
  • The legacy methods return the expected values.
  • The class properties are correctly overridden (crud, level, action, object, ...).
  • The properties automatically generated (component, name, ...) are correct.
  • Events are dispatched to the corresponding observers.
  • Events are dispatched to the corresponding legacy handlers.
  • Events are dispatched to the * observers.
  • Events perform an add_to_log() if it has legacy log data.
  • 'Events restore' restored the whole event data, and does not miss any information.
  • 'Events restore' does not generate any extra information.

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 function get_message() {
return get_string('event_assignment_created_message', 'assign', $this->objectid);
}
 
}

Development stages

Stage 1

  • Finish class loader spec and implement basic Frankenstyle class loader.
  • Describe new observer definition - just few new flags in current db/events.php
  • Describe new event dispatcher.
  • Describe core_event_base class.
  • Current (= legacy) events triggering:
    • Re-factor 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.
  • No changes to be made to the current logging system 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 throughout the standard Moodle package in places where we have add_to_log(). Implement some_event::get_legacy_log_data() which returns original parameters of add_to_log() and remove it. Old add_to_log() function 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 the 2.6 release.

Stage 3 (requires partial completion of Stage 2)

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

Stage 4 (requires partial completion of Stage 2)

  • Implement an event logging handler.
  • Implement logging storage plugins.
  • Define logging apis.
  • Create new reports.
  • Switch to new logging everywhere after Stage 2 has been completed and new reports are usable.

See Logging 2

Stage 5

  • Decide how much backwards compatibility we want for old log tables. Most probably they will 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 the log table.

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

See Logging 2

Personal tools
User docs (English)