Migrating logging calls in plugins

Jump to: navigation, search

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.