Note:

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

Talk:Event 2

From MoodleDocs

DISCUSSION IS OPEN NOW AT: https://moodle.org/mod/forum/discuss.php?d=229425 PLEASE POST YOUR COMMENTS THERE

Question:

Should subplugins trigger events using the name of the parent module (e.g. the assignsubmission_comments subplugin can trigger assign_submissioncommented ?)

Also - why are we assuming there is only a single subject for an event?

e.g. A teacher grades a submission - the subjects should be all of (teacher, student, assignment and submission).

-- Damyon


Damyon, anybody can trigger event defined in another place. For example plugins can trigger the core events and subplugins can trigger events defined in plugin. It's just most common the plugin who defines event triggers it.

In your case the mod_assign will define event: mod_assign_event_submission_completed and subplugins can trigger it specifying their own name as 'component' property of event.

"Teacher grades a submission": teacher is the actor, submission is a subject. Student is not really related here :)

-- Marina Glancy 12:26, 21 May 2013 (WST)

Documenting some lunch table discussions: A valid use case for the event system might be - subscribe to all events affecting this user - this is impossible with this design. Also mentioned was that for bulk actions it would be desirable to fire a bulk user enrolled event with a list of users, rather than lots of individual events - but again you want to be able to subscribe to an event that affects a single user without predetermined knowledge of all the possible event types.

-- Damyon


Big question: Why? This spec really needs an introduction to explain why you are proposing to make non-backwards-compatible changes to a bit of Moodle that currently works fine.--Tim Hunt 00:07, 22 May 2013 (WST)

Use of php autoloading

PHP __autoload is a very nice feature in PHP 5 which allows for

  • Class files to be automatically loaded when needed
  • Only those files that need to be included are included (a little bit of memory savings).
  • Defer file inclusion to last possible moment.
  • Allow registering of more than one function to load class files (Needed while using third party libraries or projects where file structure is different in different modules)
  • Nice error handling can be implemented to suite special needs
  • Enforce structured naming scheme throughout the project
  • Prevents duplicate class name declarations from ever occurring
  • Simple to use and avoid dependency tracking in large projects like moodle.

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

  • Might break plugins not using structure or duplicate class names (Two plugins with same name)
  • If lots of classes, calling the function, translating the names and so on usually takes longer to execute than a group of simple includes (Probably negligible but worth considering)
  • Integrating third party libraries will be an overhead, if class name conflict.
  • Autoloaders cannot work simultaneously, having two libraries with different structure will need the second one to wait till the first one either load the class or decide that it does not recognize the class name.
  • Can cause issues with obfuscater or zend guard.

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

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

spl_autoload_register(function ($classname) {
    global $CFG;
    if ($pluginfullname = stristr($classname, '_event_', true)) {
        $eventfile = $CFG->dirroot.'/'.str_replace('_', '/', $classname).'.php';
        if (is_readable($eventfile)) {
            require_once $eventfile;
        }
    }
});
Are you aware of the existind discussions about auto-loading in Moodle? See MDLSITE-2261 for one summary.--Tim Hunt 19:36, 22 May 2013 (WST)

Current unit test for event and logging

Current Moodle code has unit test for

  • events : lib/tests/eventslib_test.php
  • logs : Stats unit test is making logs. lib/tests/statslib_test.php

cache events

We should consider moving cache events to use new event system.

Cache has it's own event capture mechanism for invalidating cache at particular events like:

  • changesincoursecat
  • changesincourse

To hook existing purge event to new event system, we have to alter cache_helper::purge_by_event() to observe relevant events.

Work shop example

The workshop activity requires a lot of setup and preparation by the teacher.

As far as I can tell, any sort of event is really only ever a one to one event. I can see that perhaps teachers of the activity would like to be notified

  • Current event triggers:*

mod/workshop/submission.php (assessable_content_uploaded) mod/workshop/view.php (workshop_viewed)

  • add_to_log*

mod/workshop/index.php add_to_log($course->id, 'workshop', 'view all', "index.php?id=$course->id", );

mod/workshop/locallib.php add_to_log($this->course->id, 'workshop', $action, $logurl, $info, $this->cm->id); Call in function log() function log called 57 times

"preparing workshop grading strategy handler" "increasing group workload to" "increasing square workload to" "individual workloads in this group are" "update switch phase" "add submission" "update submission" "view submission" "update example" "add example" "add reference assessment"

  • Possible event triggers*

Example setup

  • Submission with multiple files to be uploaded.
  • Three students review each students submission
  • Teacher can give feedback to the reviewer and apply a weight to the assessment mark given by the reviewer.


  1. (Teacher) creates a workshop activity
  2. Workshop description is set
  3. Instructions for submission are provided
  4. assessment form is created
  5. example submission is submitted


  • Workshop moved to submission phase


  1. instructions for assessment are provided
  2. example assessment is provided
  3. StudentX allocated to studentY's submission
  4. StudentX submitted a submission


  • Workshop moved to assessment phase
  1. StudentX provides an assessment of studentY's submission
    • StudentX assessment can be broken down into feedback and grade


  • Workshop moved to grading evaluation phase
  1. Submission grade for studentX is calculated
  2. Assessment grade for studentY is calculated
  3. Teacher provides feedback to studentY's assessment
  4. Teacher provides weighting to studentY's assessment grade


  • Workshop is moved to Closed status

event_base class (2nd suggestion)

<?php

// define('CLI_SCRIPT', true);
require 'config.php';

/**
 * Base event abstract class.
 */
abstract class event_base implements IteratorAggregate {

  const LEVEL_MINOR = 'minor';
  const LEVEL_NORMAL = 'normal';
  const LEVEL_MAJOR = 'major';

  // Constants.
  private $version = 1;
  private $type = 'action';
  private $restored = false;
  private $triggered = false;

  // Those variables should be constants for child classes.
  protected $name;
  protected $component;
  protected $action;
  protected $object;
  protected $associatedobject;
  protected $crud;
  protected $level;

  // Event variables.
  protected $time;
  protected $contextid;
  protected $courseid;
  protected $cmid;
  protected $userid;
  protected $realuserid;
  protected $useripaddress;
  protected $objectid;
  protected $associatedobjectid;
  protected $data;
  protected $currenturl;
  // The list is incomplete...

  /**
   * Final constructor.
   */
  protected final function __construct() {
    // Automatic values.
    $this->name = get_class($this);
    $this->component = strstr($this->name, '_event_', true);
    $this->level = self::LEVEL_NORMAL;
  }

  /**
   * Magic getter to make the properties publically readable.
   *
   * @param string $name of the property.
   * @return mixed value of the property or null if not found.
   */
  public function __get($name) {
    if (isset($this->{$name})) {
      return $this->{$name};
    }
    return null;
  }

  /**
   * Define whether a user can view the event details or not.
   *
   * @param int $userid ID of the user.
   * @return is_bool True if the user can.
   */
  abstract public function can_view($userid = null);

  /**
   * Create and return an instance of the event.
   *
   * @param array $args array of properties and their values.
   * @return object event obbject.
   */
  public static final function create($args = array()) {
    $args = (array) $args;
    $event = new static();
    $event->set($args);
    $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)) {
      if (CLI_SCRIPT) {
        $this->userid = -1;
      } else {
        $this->userid = isloggedin() ? $USER->id: 0;
      }
    }

    if ($this->userid === $USER->id) {
      $this->realuserid = session_is_loggedinas() ? session_get_realuser()->id : 0;
    }

    if (empty($this->useripaddress)) {
      $this->useripaddress = getremoteaddr();
    }

    if (empty($this->currenturl)) {
      $this->currenturl = $PAGE->url->out(false);
    }
  }

  /**
   * Get all the users affected by the event.
   *
   * @return array list of user IDs.
   */
  public function get_all_affected_users() {
    if (!$this->triggered || !$this->restored) {
      // Error.
    }
    $users = array();
    if ($this->userid > 0) {
      $users[] = $this->userid;
    }
    if ((!empty($this->objectid) || is_numeric($this->objectid)) && ($this->object == 'user' || $this->object == 'member')) {
      $users[] = $this->objectid;
    }
    if ((!empty($this->associatedobjectid) || is_numeric($this->associatedobjectid)) &&
        ($this->associatedobject == 'user' || $this->associatedobject == 'member')) {
      $users[] = $this->associatedobjectid;
    }
    return array_unique($users);
  }

  /**
   * Human readable localised description of the event.
   *
   * Example: "A user created a new topic in a forum".
   *
   * @return string event description.
   */
  abstract public static function get_description();

  /**
   * Human readable localised message about the event that happened.
   *
   * Example: User 'John' created a topic called 'Awesome!'.
   *
   * @return string event message.
   */
  abstract public function get_message();

  /**
   * Human readable localised name of the event.
   *
   * Example: 'Forum topic created'
   *
   * @return string name of the event.
   */
  abstract public static function get_name();

  /**
   * Restore an event by passing its original data.
   *
   * @param array|object $args event properties to restore.
   * @return object event object.
   */
  public static final function restore($args) {
    $args = (array) $args;
    $event = new static();
    $event->set($args);
    $event->restored = true;
    $event->triggered = true;
    return $event;
  }

  /**
   * Private method used to set the allowed properties.
   *
   * @param array $args properties and their values.
   * @return void
   */
  private final function set($args = array()) {
    $args = (array) $args;

    // With validation.
    $validprops = array('time' => 0, 'contextid' => 0, 'courseid' => 0, 'cmid' => 0, 'userid' => 0, 'realuserid' => 0,
      'useripaddress' => 0, 'objectid', 'associatedobjectid' => 0, 'data' => 0, 'currenturl' => 0);
    $allowed = array_intersect_key($args, $validprops);
    if (count($allowed) !== count($args)) {
      // Errors, some properties were filtered.
    }
    foreach ($allowed as $key => $val) {
      $event->{$key} = $val;
    }

    // Or this, without validation.
    //
    // if (isset($args['time'])) {
    //   $event->time = $args['time'];
    // }
    // if (isset($args['contextid'])) {
    //   $event->contextid = $args['contextid'];
    // }
    // if (isset($args['courseid'])) {
    //   $event->courseid = $args['courseid'];
    // }
    // if (isset($args['cmid'])) {
    //   $event->cmid = $args['cmid'];
    // }
    // if (isset($args['userid'])) {
    //   $event->userid = $args['userid'];
    // }
    // if (isset($args['realuserid'])) {
    //   $event->realuserid = $args['realuserid'];
    // }
    // if (isset($args['useripaddress'])) {
    //   $event->useripaddress = $args['useripaddress'];
    // }
    // if (isset($args['objectid'])) {
    //   $event->objectid = $args['objectid'];
    // }
    // if (isset($args['associatedobjectid'])) {
    //   $event->associatedobjectid = $args['associatedobjectid'];
    // }
    // if (isset($args['data'])) {
    //   $event->data = $args['data'];
    // }
    // if (isset($args['currenturl'])) {
    //   $event->currenturl = $args['currenturl'];
    // }
  }

  /**
   * 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.
    }
  }

  /**
   * Trigger the event.
   *
   * This is not possible on restored events.
   *
   * @return [type] [description]
   */
  public final function trigger() {
    if ($this->restored || $this->triggered) {
      // Error.
    }
    $this->strict_validation();
    $this->validate();
    $this->triggered = true;
    // Now send to the observers.
    // ...
    print_object($this);
  }

  /**
   * 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);
  }
}

# Example of child class.
class core_event_user_created extends event_base {
  protected $action = 'created';
  protected $object = 'user';
  protected $crud = 'c';

  public static function get_name() {
    return get_string('bla', 'foo');
  }

  public static function get_description() {
    return get_string('bla_desc', 'foo');
  }

  public function get_message() {
    return get_string('bla_msg', 'foo', $this->objectid);
  }

  public function can_view($user = null) {
    return false;
  }
}

$PAGE->set_context(context_system::instance());
$PAGE->set_url('/');

# Example to trigger it.
$newuserid = 12345;
$a = core_event_user_created::create(array('objectid' => $newuserid));

echo '<pre>';
foreach ($a as $key => $value) {
  echo "$key: $value" . PHP_EOL;
}
echo '</pre>';

$a->trigger();

Frédéric Massart 12:59, 24 May 2013 (WST)

Early performance tests

I did some raw iterations over the example code and the old event trigger system. Here is the result:-

for($i = 0; $i < 10000; $i++) {
    $newuserid = 12345;
    $a = core_event_user_created::create(array('objectid' => $newuserid));
}

Total time cost :- 2669 milliseconds

for ($i = 0; $i < 10000; $i++) {
    $eventdata = new stdClass();
    events_trigger("event_whatever", $eventdata);
}

Total time cost:- 327 milliseconds

Difference = 0.2342 milliseconds per call.

Ankit Agarwal 15:41, 27 May 2013 (WST)

Observer documentation

The observer documentation (which a lot of plugin developers will be interested in) is very sparse. It doesn't even give an example of what the callback method should look like. It also rather assumes core classes and ignores the format for plugins. As a lot of this stuff will be new for many devs a bit more hand-holding would be nice.--Howard Miller (talk) 20:56, 6 August 2014 (WST)

It would also be useful to provide some documentation (at least an example) of using the $event object that is past to the callback method. This class is non-trivial so it's far from obvious. --Howard Miller (talk) 19:11, 31 July 2015 (AWST)

I agree with Howard Miller, sample local plugin covering custom event trigger, custom event observer and core observer will be great help.-- Rt S