Note:

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

Migrating logging calls in plugins: Difference between revisions

From MoodleDocs
(45 intermediate revisions by 10 users not shown)
Line 1: Line 1:
This document is aimed to assist developers in replacing existing '''add_to_log()''' calls in the plugin with the events. This '''can''' be implemented in Moodle 2.6 and developers will be '''required''' to convert the existing code in 2.7.
This document is aimed to assist developers in replacing existing '''add_to_log()''' and '''events_trigger()''' calls with events. This can be implemented in Moodle 2.6 and will be required in 2.7.


As a quick reminder: [[Event 2]] was introduced in Moodle 2.6, new [[Logging 2]] system is being introduced in Moodle 2.7. Function '''add_to_log()''' becomes deprecated, the existing log table is still present and existing data are intact. This original logging is now called '''legacy logging'''. The new and legacy logging may coexist in the same system, this is not recommended for performance reasons. When replacing the add_to_log() with triggering of an event developer also must ensure that he also generates an entry to the legacy log. It will only be used if legacy log is enabled since it may be enabled on systems that use too many custom reports relying on presence of log table and it would take time to migrate the reports.
As a quick reminder: [[Events API|new events]] were introduced in Moodle 2.6, a new [[Logging 2|logging system]] is being introduced in Moodle 2.7. The '''add_to_log()''' function will be deprecated, but the existing log table will still be present with existing data intact. This original logging is now called ''legacy logging''. The new and legacy logging may coexist in the legacy logging system for purposes of transition, but this is not recommended for performance reasons. When replacing calls to add_to_log() with the triggering of an event, developers must ensure that they also generate an entry for the legacy log. It will only be used if the legacy log is enabled, since it may be enabled on systems that continue to use custom reports relying on presence of the legacy log table and it may take time to migrate such reports.


== Quick guide ==
== Quick guide ==


If you are replacing common add_to_log() calls such as "view" and "view all" in mod/XXX/view.php and mod/XXX/index.php, see below. Otherwise do the following:
If you are replacing common add_to_log() calls such as "view" and "view all" in mod/XXX/view.php and mod/XXX/index.php, see below. Otherwise do the following.


=== Step 1. Choose a name for event ===
=== Step 1. Choose a name for the event ===


It should have syntax OBJECT_VERB, for example "entry_added", "work_submitted", etc. It does not need to have plugin name in it because it is obvious from the PHP class namespace. See [[Event 2]] for more details on events names.
Names should follow the syntax OBJECT_VERB, for example "entry_added", "work_submitted", etc. It does not need to include a plugin name because this can be obtained from the PHP class namespace. See [[Events API|the events documentation]] for more details about event name; specifically, there is a restricted [[Events API#Verb list|list of available verbs]].


Define language string for the event name in '''YOURPLUGINDIR/lang/en/FULLPLUGINNAME.php''' :
Define a language string for the event name in '''YOURPLUGINDIR/lang/en/FULLPLUGINNAME.php'''.
<pre style="background: #DFF">
<code php>
$string['eventSOMETHINGHAPPENED] = 'SOMETHING has HAPPENED';
$string['eventEVENTNAME'] = 'Something has happened';
</pre>
</code>


=== Step 2. Create event class ===
=== Step 2. Create event class ===


It must be located in '''YOURPLUGINDIR/classes/event/SOMETHING_HAPPENED.php''' , typical example:
For each event you must create an event class in '''YOURPLUGINDIR/classes/event/EVENTNAME.php''', with the following format.
<pre style="background: #FFA">
<code php>
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
/**
* The EVENTNAME event.
*
* @package    FULLPLUGINNAME
* @copyright  2014 YOUR NAME
* @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace FULLPLUGINNAME\event;
namespace FULLPLUGINNAME\event;
defined('MOODLE_INTERNAL') || die();
defined('MOODLE_INTERNAL') || die();
class SOMETHING_HAPPENED extends \core\event\base {
/**
* The EVENTNAME event class.
*
* @property-read array $other {
*      Extra information about event.
*
*      - PUT INFO HERE
* }
*
* @since    Moodle MOODLEVERSION
* @copyright 2014 YOUR NAME
* @license  http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
**/
class EVENTNAME extends \core\event\base {
     protected function init() {
     protected function init() {
         $this->data['crud'] = 'c'; // c(reate), r(ead), u(pdate), d(elete)
         $this->data['crud'] = 'c'; // c(reate), r(ead), u(pdate), d(elete)
Line 30: Line 66:


     public static function get_name() {
     public static function get_name() {
         return get_string('eventSOMETHINGHAPPENED', 'FULLPLUGINNAME');
         return get_string('eventEVENTNAME', 'FULLPLUGINNAME');
     }
     }


     public function get_description() {
     public function get_description() {
         return "User {$this->userid} has ... ... ... with id {$this->objectid}.";
         return "The user with id {$this->userid} created ... ... ... with id {$this->objectid}.";
     }
     }


     public function get_url() {
     public function get_url() {
         return new \moodle_url('....', array());
         return new \moodle_url('....', array('parameter' => 'value', ...));
     }
     }


     public function get_legacy_log_data() {
     public function get_legacy_logdata() {
        // Override if you are migrating an add_to_log() call.
         return array($this->courseid, 'PLUGINNAME', 'LOGACTION',
         return array($this->courseid, 'PLUGINNAME', 'LOGACTION',
             '...........',
             '...........',
             $this->objectid, $this->contextinstanceid);
             $this->objectid, $this->contextinstanceid);
    }
    public static function get_legacy_eventname() {
        // Override ONLY if you are migrating events_trigger() call.
        return 'MYPLUGIN_OLD_EVENT_NAME';
    }
    protected function get_legacy_eventdata() {
        // Override if you migrating events_trigger() call.
        $data = new \stdClass();
        $data->id = $this->objectid;
        $data->userid = $this->relateduserid;
        return $data;
     }
     }
}
}
</pre>
</code>


=== Step 3. Trigger event instead of add_to_log() ===
=== Step 3. Trigger the event instead of add_to_log() ===


Replace add_to_log() with triggering of event. This is a common example for event inside an activity module:
Replace the add_to_log() with an event trigger. The following is a common example of an event trigger inside an activity module.
<pre>
<code php>
add_to_log($course->id, 'PLUGINNAME', 'LOGACTION', '...........', $objid, $cmid);
add_to_log($course->id, 'PLUGINNAME', 'LOGACTION', '...........', $objid, $cmid);
</pre>
</code>


becomes:
...becomes...


<pre style="background: #FDD">
<code php>
$event = \FULLPLUGINNAME\event\SOMETHING_HAPPENED::create(array(
$event = \FULLPLUGINNAME\event\EVENTNAME::create(array(
     'objectid' => $objid,
     'objectid' => $objid,
     'context' => context_module::instance($cmid)
     'context' => context_module::instance($cmid)
));
));
$event->trigger();
$event->trigger();
</pre>
</code>


== Replacing 'view' event in the modules ==
If you need to trigger event multiple times in the code, or just prefer shorter syntax, you can declare your own static create function in the event class that would populate necessary fields and even add snapshots (see examples in mod_assign). In this case you can triger event in one line:


This is usually found in mod/PLUGINNAME/view.php (or in lib function included from this file) and indicates that user viewed the module.  
<code php>
\FULLPLUGINNAME\event\EVENTNAME::create_from_someobject($someobject)->trigger();
</code>
 
=== Step 4. Increase the version number in version.php===
 
The events that exist are only scanned when a plugin is installed or updated. Therefore when you change events, you need to increase the plugin's version number in its version.php to prompt an upgrade.
 
== Replacing 'view' events in modules ==
 
Calls to add_to_log() to report a 'view' event are usually found in mod/PLUGINNAME/view.php (or in a lib function included by this file) and indicate that a user viewed the module.  


=== Step 1. Choosing the name ===
=== Step 1. Choosing the name ===


The name is already choosen - course_module_viewed - and the language string is defined in core.
Because this is a common event, the name is already chosen: '''course_module_viewed''' and the language string is defined in core.


=== Step 2. Defining class ===
=== Step 2. Defining class ===


'''YOURPLUGINDIR/classes/event/course_module_viewed.php''' :
You must create a class for this event in '''YOURPLUGINDIR/classes/event/course_module_viewed.php''' with the following format.
<pre style="background: #FFA">
<code php>
namespace FULLPLUGINNAME\event;
namespace FULLPLUGINNAME\event;
defined('MOODLE_INTERNAL') || die();
defined('MOODLE_INTERNAL') || die();
Line 85: Line 145:
         parent::init();
         parent::init();
     }
     }
     // You might need to overwrite get_url() and get_legacy_log_data() if view mode needs to be stored as well.
     // You might need to override get_url() and get_legacy_log_data() if view mode needs to be stored as well.
}
}
</pre>
</code>


=== Step 3. Triggering event ===
=== Step 3. Triggering the event ===


This example takes data from $PAGE object but you may as well substite with ids and objects that you fetched:
This example takes data from $PAGE object but you may substitute this with ids and objects that you have fetched.
<pre style="background: #FDD">
<code php>
$event = \FULLPLUGINNAME\event\course_module_viewed::create(array(
$event = \FULLPLUGINNAME\event\course_module_viewed::create(array(
     'objectid' => $PAGE->cm->instance,
     'objectid' => $PAGE->cm->instance,
Line 98: Line 158:
));
));
$event->add_record_snapshot('course', $PAGE->course);
$event->add_record_snapshot('course', $PAGE->course);
$event->add_record_snapshot($PAGE->cm->modname, $activityrecord); // You can use $PAGE->activityrecord if you have set it or skip this line if you don't have a record.
// In the next line you can use $PAGE->activityrecord if you have set it, or skip this line if you don't have a record.
$event->add_record_snapshot($PAGE->cm->modname, $activityrecord);
$event->trigger();
$event->trigger();
</pre>
</code>


== Replacing 'view all' event in the modules ==
===Step 4. Update version.php===
Don't forget to update the plugin version number to prompt an upgrade.


This is usually found in mod/PLUGINNAME/index.php (or in lib function included from this file) and indicates that user viewed list of modules of this type in the course.  
== Replacing 'view all' events in modules ==
 
Calls to add_to_log using 'view_all' are usually found in mod/PLUGINNAME/index.php (or in a lib function included by this file). These invents indicate that a user viewed the list of all instances of this module within the course.  


=== Step 1. Choosing the name ===
=== Step 1. Choosing the name ===


The name is already choosen - course_module_instance_list_viewed - and the language string is defined in core.
Because this is a common event, the name is already chosen: '''course_module_instance_list_viewed''' and the language string is defined in core.


=== Step 2. Defining class ===
=== Step 2. Defining class ===


'''YOURPLUGINDIR/classes/event/course_module_instance_list_viewed.php''' :
You must create a class for this event in '''YOURPLUGINDIR/classes/event/course_module_instance_list_viewed.php''' with the following structure.
<pre style="background: #FFA">
<code php>
namespace FULLPLUGINNAME\event;
namespace FULLPLUGINNAME\event;
defined('MOODLE_INTERNAL') || die();
defined('MOODLE_INTERNAL') || die();
class course_module_instance_list_viewed extends \core\event\course_module_instance_list_viewed {
class course_module_instance_list_viewed extends \core\event\course_module_instance_list_viewed {
}
}
</pre>
</code>


=== Step 3. Triggering event ===
=== Step 3. Triggering the event ===


<pre style="background: #FDD">
<code php>
$event = \FULLPLUGINNAME\event\course_module_instance_list_viewed::create(array(
$event = \FULLPLUGINNAME\event\course_module_instance_list_viewed::create(array(
     'context' => context_course::instance($course->id)
     'context' => context_course::instance($course->id)
));
));
$event->trigger();
$event->trigger();
</pre>
</code>


== What to include in the event ==
== What to include in the event ==
Line 133: Line 197:
=== init() and create() ===
=== init() and create() ===


As you noticed in the examples above you can specify additional fields either by overwriting init() method of event or when calling create(). The first way is for properties that are always the same for this event, the second is for dynamic properties.
Ideally all information needed when initialising and triggering events should already be available without having to run additional queries. Queries run to fill event objects with data will cause additional performance load, which should be avoided. Information that needs to be gathered from the database should be provided by other event methods, which can be called selectively when needed.


Usually you need to include fields:
As you noticed in the examples above you can specify additional properties either by the overriding the init() method of the event or when calling create(). The first way is used for properties that are always the same for this event, the second is for dynamic properties that may differ when the event is triggered.
* '''context''' or '''contextid''': required. If you want to hardcode system context, do so in init();
* '''crud''', '''edulevel''': required, most often are specified in init();
* '''objecttable''' and '''objectid''': used by events that show the change in one record of one table, which will be the case for the most of the events. Since 'objecttable' is always the same it is usually specified in init();
* '''relateduserid''': only if it is easy to select a single user who is affected by this operation. For example, user who is being graded, user who receives the message, user who is being enrolled, etc. This is NOT a user who performs the action;
* '''other''': everything else that you may think is important about this event. This field will be serialised and stored by logger. Developers should put here only necessary information for functions get_description(), get_url() and get_legacy_logdata(). This should only contain array or scalar value, '''can not use objects'''


Usually you need to include the following properties.
{| class="nicetable"
|-
| '''context''' or '''contextid'''
| required
| Describes the context where the event took place.
| If you want to hardcode the system context, do so in init().
|-
| '''crud'''
| required
| Describes whether the event reflects creation (c), reading (r), updating (u) or deleting (d). This should be a single character string.
| Most often are specified in init().
|-
| '''edulevel'''
| required
| The level of educational value of the event. Can be LEVEL_TEACHING, LEVEL_PARTICIPATING or LEVEL_OTHER.
| Most often are specified in init().
|-
| '''objecttable''' and '''objectid'''
|
| Objecttable is the table that best represents the event object. Usually it is the object where the "CRUD" action was performed. This table is used by events that show the change in one record of one table, which will be the case for the most events.
| Since 'objecttable' is always the same it is usually specified in init().
|-
| '''relateduserid'''
|
| The id of the user affected by the event.
| Only used if it is easy to identify a single user who is affected by this operation. For example a user who is being graded, a user who receives the message, a user being enrolled, etc. '''This is NOT the user who performs the action''' (who is identified in the userid field).
|-
| '''other'''
|
| Everything else that you may think is important about this event. This property will be serialised and stored by loggers.
| Include only necessary information. It can be used in get_description(), get_url() and get_legacy_logdata(). This property should only contain an array or scalar value, it '''can not use objects'''.
Example of information stored in 'other' can be found in event course_module_deleted:
Example of information stored in 'other' can be found in event course_module_deleted:
* Definition: https://github.com/moodle/moodle/blob/master/lib/classes/event/course_module_deleted.php#L69
* Definition: https://github.com/moodle/moodle/blob/MOODLE_27_STABLE/lib/classes/event/course_module_deleted.php#L71
* Triggering: https://github.com/moodle/moodle/blob/master/course/lib.php#L1716..L1727
* Triggering:https://github.com/moodle/moodle/blob/MOODLE_27_STABLE/course/lib.php#L1737..L1747
|}


Usually you don't need to include fields:
Usually you don't need to include the following properties as they are deduced by the base class.
* 'userid': user who performs the action, taken from $USER
* 'userid': user who performs the action, taken from $USER
* 'courseid': course affected in the operation, it will be taken from context. It may not be specified at all for the events that are not related to a particular course.
* 'courseid': course affected in the operation, which will be taken from context. It need not be specified at all for events that are not related to a particular course.


See the full list of fields in [[Event_2#Information_contained_in_events]]
See the [[Events API#Information_contained_in_events|full list of properties]] for more information.


=== get_legacy_logdata() ===
=== get_legacy_logdata() ===


Since this document is a transition guide from add_to_log() to the events, you will most likely need to overwrite this method. Return here a 5-element array which imitates the 5 arguments you used to pass to add_to_log() function. Since 2.7 the get_legacy_logdata() method will only be called if the legacy logging is enabled in legacy logging plugin.
This method is used to add log data to the legacy log. You need only override this method when replacing an add_to_log() call. Since this document is a transition guide from add_to_log() to events, you will most likely need to override this method. This method needs to return an array (with 3-7 elements) that imitates the arguments that used to be passed to the add_to_log() function. From Moodle 2.7, the get_legacy_logdata() method will only be called if legacy logging is enabled through the legacy logging plugin.


=== get_description() and get_url() ===
=== get_description() and get_url() ===


Most reporting tools will display aggregated logged information so those methods are not likely to be called often. They will only be used by detailed reports such as loglive. At the moment treat get_description() as internal very brief description of the performed action that is used for error recovery like any other system log. It is hardcoded in English but it is possible that in future versions of Moodle (2.8 or even later) it can start using language strings. Those methods can not make DB queries, access global variables, etc. They should return exactly the same result whenever they are called regardless of the environment. For example: course may be renamed, user may be deleted - '''do not retrieve course or user name but instead use their ids'''.
Most reporting tools will display aggregated event information (for example the count of student logins) so those methods, which describe individual events, are not likely to be called often; they will only be used by detailed reports such as loglive. At the moment, use get_description() to provide a very brief internal description of the action performed, so that it can be used for error recovery like any other system log. The description is hard-coded in English but it may be possible that future versions of Moodle (2.8 or later) will allow the use of translatable language strings. These methods should not make DB queries, access global variables, etc. For example, when a course is renamed or when a user is deleted, do not retrieve the course name or user name, instead simply use their ids. These functions should return exactly the same result whenever they are called, regardless of the environment or state, even after they have been restored from logs.
 
=== get_legacy_eventname() and get_legacy_eventdata() ===


=== can_view() ===
You will need to override these two functions if you are upgrading events_trigger() calls. These will allow legacy plugins to continue to listen to your new events without upgrading their listeners.


Future of this function is not yet decided. It was intended as a callback allowing each event to determine whether the current user can see the logged event. But in reality checking various capabilities on the big amount of records is a very expensive process. At the moment '''events do not overwrite this function and it is not used'''. If you are interested in method future, watch/vote/comment on MDL-44107
If you need to provide more detailed information to observers, you can choose to:
* add more information to 'other', but remember that this will be logged and it's better to keep logs as small as possible;
* use record snapshots, which are especially useful for delete actions (you can call get_record_snapshot() inside get_legacy_eventdata() and observers are encouraged to get data from snapshots as well);
* add new properties to your event class and define getter/setter functions, for example set_custom_data() and get_custom_data().


=== add_record_snapshot() ===
=== add_record_snapshot() ===


Record snapshot can be added for any DB table related to the event. If it is added it must be exact record from the DB, with correct field types and without additional fields. Also developer must add a record snapshot when he deletes something from database. '''Record snapshots cannot be used from reports, it is intended for event observers only.'''
A record snapshot can be added for any DB table related to the event. If it is added, it must be an instance of stdClass containing all fields that are present in the corresponding DB table. You must add a record snapshot when you delete something from database. '''Record snapshots cannot be used from reports, it is intended for event observers only.''' Usually observers expect a record snapshot identified by 'objecttable' and 'objectid' but developers may also add snapshots of related tables, i.e. when book chapter is updated the developer may decide to add snapshots of related records in tables book_chapters, book, course_modules and course.


Record snapshots should be added only when you already have an object do not need to perform any additional DB queries to retrieve it. Otherwise omit it, as the record will be retrieved by get_record_snapshot() automatically and only if needed. For performance reasons the snapshots are not guaranteed to contain the exact state at the time of event triggering, it may be actually fetched at any time between the triggering of event and its observation.
Record snapshots should be added only when you already have an object and do not need to perform any additional DB queries to retrieve it. Otherwise omit it, as the record will be retrieved by get_record_snapshot() automatically, and only if needed. For performance reasons the snapshots are not guaranteed to contain an exact state at the time of event triggering, it may be fetched at any time between the triggering of event and its observation.


== Events DONTs ==
== Events DON'Ts ==


Do not put more information in 'other' than it is needed, for example full DB record for delete/create operation or list of all changed fields in edit operation. If observer is interested in those fields, it can request them by calling get_record_snapshot(). Never include large text fields in event data. '''Keep the log size reasonable.'''
Do not put more information in 'other' than is needed. For example, do not include a full DB record for delete/create operations or a list of all changed properties in edit operations. If observers are interested in this information, it can requested by calling get_record_snapshot(). Never include large text fields in event data. '''Please help to keep the log size reasonable.'''


Do NOT use $USER, $COURSE, $PAGE, and other global variables when overwriting get_* methods.
Do NOT use $USER, $COURSE, $PAGE or other global variables when overriding get_* methods (with the exception of get_legacy_eventdata).


Do NOT call $this->get_record_snapshot() inside event class, if you need additional information for internal functions, add it to 'other'.
Do NOT call $this->get_record_snapshot() inside the event class (again with the exception of get_legacy_eventdata). If you need additional information for internal functions that cannot be added to existing properties, add it to the 'other' property.


Do NOT use $this->context inside an event class. '''Remember that methods get_description() and get_url() may be called on events restored from log and the context can be already deleted.''' It is possible that the original context does not exist any more. Instead use $this->contextid, $this->contextlevel, $this->contextinstanceid.
Do NOT use $this->context inside an event class. '''Remember that methods get_description() and get_url() may be called on events after they have been restored from logs.''' It is possible that the original context no longer exists when these functions are called. Instead use $this->contextid, $this->contextlevel, $this->contextinstanceid.


== Validation and testing ==
== Validation and testing ==


You may notice that the most of existing events in Moodle also have function validate_data() . You can also add it for your own safety to ensure that you don't forget to define all required data when triggering event. Also we highly recommend to cover your events with unittests. Search for files with the names events_test.php in Moodle for examples.
You may notice that the most of events in Moodle also have function validate_data() . You can add this function for your own safety to ensure that you don't forget to define all required data when triggering event.
 
Always use developer debugging mode when doing development. A lot of useful build-in validation will only work in development mode.
 
We highly recommend to cover your events with unit tests. Search in standard plugins for files with the names events_test.php to see examples.
 
In order to manually test the event you can perform the action and check how it appears in '''Course Administration > Reports > Logs'''. Also look up your event in the '''Site Administration > Reports > Events list'''.
 
== "Installation" of events ==
 
Remember that you have to bump the plugin version in order to prompt an upgrade so that new events will be "installed".
 
==See also==
The [https://github.com/markn86/moodle-mod_certificate/pull/34 '''add_to_log deprecated, replace with events'''] issue on Github illustrates how the deprecated '''add_to_log()''' calls in the Certificate Module were upgraded to the new events method.

Revision as of 11:40, 6 April 2020

This document is aimed to assist developers in replacing existing add_to_log() and events_trigger() calls with events. This can be implemented in Moodle 2.6 and will be required in 2.7.

As a quick reminder: new events were introduced in Moodle 2.6, a new logging system is being introduced in Moodle 2.7. The add_to_log() function will be deprecated, but the existing log table will still be present with existing data intact. This original logging is now called legacy logging. The new and legacy logging may coexist in the legacy logging system for purposes of transition, but this is not recommended for performance reasons. When replacing calls to add_to_log() with the triggering of an event, developers must ensure that they also generate an entry for the legacy log. It will only be used if the legacy log is enabled, since it may be enabled on systems that continue to use custom reports relying on presence of the legacy log table and it may take time to migrate such reports.

Quick guide

If you are replacing common add_to_log() calls such as "view" and "view all" in mod/XXX/view.php and mod/XXX/index.php, see below. Otherwise do the following.

Step 1. Choose a name for the event

Names should follow the syntax OBJECT_VERB, for example "entry_added", "work_submitted", etc. It does not need to include a plugin name because this can be obtained from the PHP class namespace. See the events documentation for more details about event name; specifically, there is a restricted list of available verbs.

Define a language string for the event name in YOURPLUGINDIR/lang/en/FULLPLUGINNAME.php. $string['eventEVENTNAME'] = 'Something has happened';

Step 2. Create event class

For each event you must create an event class in YOURPLUGINDIR/classes/event/EVENTNAME.php, with the following format. <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>.

/**

* The EVENTNAME event.
*
* @package    FULLPLUGINNAME
* @copyright  2014 YOUR NAME
* @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/

namespace FULLPLUGINNAME\event; defined('MOODLE_INTERNAL') || die(); /**

* The EVENTNAME event class.
*
* @property-read array $other {
*      Extra information about event.
*
*      - PUT INFO HERE
* }
*
* @since     Moodle MOODLEVERSION
* @copyright 2014 YOUR NAME
* @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
**/

class EVENTNAME extends \core\event\base {

   protected function init() {
       $this->data['crud'] = 'c'; // c(reate), r(ead), u(pdate), d(elete)
       $this->data['edulevel'] = self::LEVEL_PARTICIPATING;
       $this->data['objecttable'] = '...';
   }
   public static function get_name() {
       return get_string('eventEVENTNAME', 'FULLPLUGINNAME');
   }
   public function get_description() {
       return "The user with id {$this->userid} created ... ... ... with id {$this->objectid}.";
   }
   public function get_url() {
       return new \moodle_url('....', array('parameter' => 'value', ...));
   }
   public function get_legacy_logdata() {
       // Override if you are migrating an add_to_log() call.
       return array($this->courseid, 'PLUGINNAME', 'LOGACTION',
           '...........',
           $this->objectid, $this->contextinstanceid);
   }
   public static function get_legacy_eventname() {
       // Override ONLY if you are migrating events_trigger() call.
       return 'MYPLUGIN_OLD_EVENT_NAME';
   }
   protected function get_legacy_eventdata() {
       // Override if you migrating events_trigger() call.
       $data = new \stdClass();
       $data->id = $this->objectid;
       $data->userid = $this->relateduserid;
       return $data;
   }

}

Step 3. Trigger the event instead of add_to_log()

Replace the add_to_log() with an event trigger. The following is a common example of an event trigger inside an activity module. add_to_log($course->id, 'PLUGINNAME', 'LOGACTION', '...........', $objid, $cmid);

...becomes...

$event = \FULLPLUGINNAME\event\EVENTNAME::create(array(

   'objectid' => $objid,
   'context' => context_module::instance($cmid)

)); $event->trigger();

If you need to trigger event multiple times in the code, or just prefer shorter syntax, you can declare your own static create function in the event class that would populate necessary fields and even add snapshots (see examples in mod_assign). In this case you can triger event in one line:

\FULLPLUGINNAME\event\EVENTNAME::create_from_someobject($someobject)->trigger();

Step 4. Increase the version number in version.php

The events that exist are only scanned when a plugin is installed or updated. Therefore when you change events, you need to increase the plugin's version number in its version.php to prompt an upgrade.

Replacing 'view' events in modules

Calls to add_to_log() to report a 'view' event are usually found in mod/PLUGINNAME/view.php (or in a lib function included by this file) and indicate that a user viewed the module.

Step 1. Choosing the name

Because this is a common event, the name is already chosen: course_module_viewed and the language string is defined in core.

Step 2. Defining class

You must create a class for this event in YOURPLUGINDIR/classes/event/course_module_viewed.php with the following format. namespace FULLPLUGINNAME\event; defined('MOODLE_INTERNAL') || die(); class course_module_viewed extends \core\event\course_module_viewed {

   protected function init() {
       $this->data['objecttable'] = 'PLUGINNAME';
       parent::init();
   }
   // You might need to override get_url() and get_legacy_log_data() if view mode needs to be stored as well.

}

Step 3. Triggering the event

This example takes data from $PAGE object but you may substitute this with ids and objects that you have fetched. $event = \FULLPLUGINNAME\event\course_module_viewed::create(array(

   'objectid' => $PAGE->cm->instance,
   'context' => $PAGE->context,

)); $event->add_record_snapshot('course', $PAGE->course); // In the next line you can use $PAGE->activityrecord if you have set it, or skip this line if you don't have a record. $event->add_record_snapshot($PAGE->cm->modname, $activityrecord); $event->trigger();

Step 4. Update version.php

Don't forget to update the plugin version number to prompt an upgrade.

Replacing 'view all' events in modules

Calls to add_to_log using 'view_all' are usually found in mod/PLUGINNAME/index.php (or in a lib function included by this file). These invents indicate that a user viewed the list of all instances of this module within the course.

Step 1. Choosing the name

Because this is a common event, the name is already chosen: course_module_instance_list_viewed and the language string is defined in core.

Step 2. Defining class

You must create a class for this event in YOURPLUGINDIR/classes/event/course_module_instance_list_viewed.php with the following structure. namespace FULLPLUGINNAME\event; defined('MOODLE_INTERNAL') || die(); class course_module_instance_list_viewed extends \core\event\course_module_instance_list_viewed { }

Step 3. Triggering the event

$event = \FULLPLUGINNAME\event\course_module_instance_list_viewed::create(array(

   'context' => context_course::instance($course->id)

)); $event->trigger();

What to include in the event

init() and create()

Ideally all information needed when initialising and triggering events should already be available without having to run additional queries. Queries run to fill event objects with data will cause additional performance load, which should be avoided. Information that needs to be gathered from the database should be provided by other event methods, which can be called selectively when needed.

As you noticed in the examples above you can specify additional properties either by the overriding the init() method of the event or when calling create(). The first way is used for properties that are always the same for this event, the second is for dynamic properties that may differ when the event is triggered.

Usually you need to include the following properties.

context or contextid required Describes the context where the event took place. If you want to hardcode the system context, do so in init().
crud required Describes whether the event reflects creation (c), reading (r), updating (u) or deleting (d). This should be a single character string. Most often are specified in init().
edulevel required The level of educational value of the event. Can be LEVEL_TEACHING, LEVEL_PARTICIPATING or LEVEL_OTHER. Most often are specified in init().
objecttable and objectid Objecttable is the table that best represents the event object. Usually it is the object where the "CRUD" action was performed. This table is used by events that show the change in one record of one table, which will be the case for the most events. Since 'objecttable' is always the same it is usually specified in init().
relateduserid The id of the user affected by the event. Only used if it is easy to identify a single user who is affected by this operation. For example a user who is being graded, a user who receives the message, a user being enrolled, etc. This is NOT the user who performs the action (who is identified in the userid field).
other Everything else that you may think is important about this event. This property will be serialised and stored by loggers. Include only necessary information. It can be used in get_description(), get_url() and get_legacy_logdata(). This property should only contain an array or scalar value, it can not use objects.

Example of information stored in 'other' can be found in event course_module_deleted:

Usually you don't need to include the following properties as they are deduced by the base class.

  • 'userid': user who performs the action, taken from $USER
  • 'courseid': course affected in the operation, which will be taken from context. It need not be specified at all for events that are not related to a particular course.

See the full list of properties for more information.

get_legacy_logdata()

This method is used to add log data to the legacy log. You need only override this method when replacing an add_to_log() call. Since this document is a transition guide from add_to_log() to events, you will most likely need to override this method. This method needs to return an array (with 3-7 elements) that imitates the arguments that used to be passed to the add_to_log() function. From Moodle 2.7, the get_legacy_logdata() method will only be called if legacy logging is enabled through the legacy logging plugin.

get_description() and get_url()

Most reporting tools will display aggregated event information (for example the count of student logins) so those methods, which describe individual events, are not likely to be called often; they will only be used by detailed reports such as loglive. At the moment, use get_description() to provide a very brief internal description of the action performed, so that it can be used for error recovery like any other system log. The description is hard-coded in English but it may be possible that future versions of Moodle (2.8 or later) will allow the use of translatable language strings. These methods should not make DB queries, access global variables, etc. For example, when a course is renamed or when a user is deleted, do not retrieve the course name or user name, instead simply use their ids. These functions should return exactly the same result whenever they are called, regardless of the environment or state, even after they have been restored from logs.

get_legacy_eventname() and get_legacy_eventdata()

You will need to override these two functions if you are upgrading events_trigger() calls. These will allow legacy plugins to continue to listen to your new events without upgrading their listeners.

If you need to provide more detailed information to observers, you can choose to:

  • add more information to 'other', but remember that this will be logged and it's better to keep logs as small as possible;
  • use record snapshots, which are especially useful for delete actions (you can call get_record_snapshot() inside get_legacy_eventdata() and observers are encouraged to get data from snapshots as well);
  • add new properties to your event class and define getter/setter functions, for example set_custom_data() and get_custom_data().

add_record_snapshot()

A record snapshot can be added for any DB table related to the event. If it is added, it must be an instance of stdClass containing all fields that are present in the corresponding DB table. You must add a record snapshot when you delete something from database. Record snapshots cannot be used from reports, it is intended for event observers only. Usually observers expect a record snapshot identified by 'objecttable' and 'objectid' but developers may also add snapshots of related tables, i.e. when book chapter is updated the developer may decide to add snapshots of related records in tables book_chapters, book, course_modules and course.

Record snapshots should be added only when you already have an object and do not need to perform any additional DB queries to retrieve it. Otherwise omit it, as the record will be retrieved by get_record_snapshot() automatically, and only if needed. For performance reasons the snapshots are not guaranteed to contain an exact state at the time of event triggering, it may be fetched at any time between the triggering of event and its observation.

Events DON'Ts

Do not put more information in 'other' than is needed. For example, do not include a full DB record for delete/create operations or a list of all changed properties in edit operations. If observers are interested in this information, it can requested by calling get_record_snapshot(). Never include large text fields in event data. Please help to keep the log size reasonable.

Do NOT use $USER, $COURSE, $PAGE or other global variables when overriding get_* methods (with the exception of get_legacy_eventdata).

Do NOT call $this->get_record_snapshot() inside the event class (again with the exception of get_legacy_eventdata). If you need additional information for internal functions that cannot be added to existing properties, add it to the 'other' property.

Do NOT use $this->context inside an event class. Remember that methods get_description() and get_url() may be called on events after they have been restored from logs. It is possible that the original context no longer exists when these functions are called. Instead use $this->contextid, $this->contextlevel, $this->contextinstanceid.

Validation and testing

You may notice that the most of events in Moodle also have function validate_data() . You can add this function for your own safety to ensure that you don't forget to define all required data when triggering event.

Always use developer debugging mode when doing development. A lot of useful build-in validation will only work in development mode.

We highly recommend to cover your events with unit tests. Search in standard plugins for files with the names events_test.php to see examples.

In order to manually test the event you can perform the action and check how it appears in Course Administration > Reports > Logs. Also look up your event in the Site Administration > Reports > Events list.

"Installation" of events

Remember that you have to bump the plugin version in order to prompt an upgrade so that new events will be "installed".

See also

The add_to_log deprecated, replace with events issue on Github illustrates how the deprecated add_to_log() calls in the Certificate Module were upgraded to the new events method.