Merge branch 'w30_MDL-39846_m26_event2' of https://github.com/skodak/moodle

This commit is contained in:
Damyon Wiese 2013-07-22 15:05:53 +08:00
commit c31909cb69
22 changed files with 2440 additions and 166 deletions

View File

@ -0,0 +1,153 @@
<?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/>.
/**
* Local stuff for category enrolment plugin.
*
* @package enrol_category
* @copyright 2010 Petr Skoda {@link http://skodak.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Event handler for category enrolment plugin.
*
* We try to keep everything in sync via listening to events,
* it may fail sometimes, so we always do a full sync in cron too.
*/
class enrol_category_observer {
/**
* Triggered when user is assigned a new role.
*
* @param \core\event\role_assigned $event
*/
public static function role_assigned(\core\event\role_assigned $event) {
global $DB;
if (!enrol_is_enabled('category')) {
return;
}
$ra = new stdClass();
$ra->roleid = $event->objectid;
$ra->userid = $event->relateduserid;
$ra->contextid = $event->contextid;
//only category level roles are interesting
$parentcontext = context::instance_by_id($ra->contextid);
if ($parentcontext->contextlevel != CONTEXT_COURSECAT) {
return;
}
// Make sure the role is to be actually synchronised,
// please note we are ignoring overrides of the synchronised capability (for performance reasons in full sync).
$syscontext = context_system::instance();
if (!$DB->record_exists('role_capabilities', array('contextid'=>$syscontext->id, 'roleid'=>$ra->roleid, 'capability'=>'enrol/category:synchronised', 'permission'=>CAP_ALLOW))) {
return;
}
// Add necessary enrol instances.
$plugin = enrol_get_plugin('category');
$sql = "SELECT c.*
FROM {course} c
JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :courselevel AND ctx.path LIKE :match)
LEFT JOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'category')
WHERE e.id IS NULL";
$params = array('courselevel'=>CONTEXT_COURSE, 'match'=>$parentcontext->path.'/%');
$rs = $DB->get_recordset_sql($sql, $params);
foreach ($rs as $course) {
$plugin->add_instance($course);
}
$rs->close();
// Now look for missing enrolments.
$sql = "SELECT e.*
FROM {course} c
JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :courselevel AND ctx.path LIKE :match)
JOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'category')
LEFT JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = :userid)
WHERE ue.id IS NULL";
$params = array('courselevel'=>CONTEXT_COURSE, 'match'=>$parentcontext->path.'/%', 'userid'=>$ra->userid);
$rs = $DB->get_recordset_sql($sql, $params);
foreach ($rs as $instance) {
$plugin->enrol_user($instance, $ra->userid, null, time());
}
$rs->close();
}
/**
* Triggered when user role is unassigned.
*
* @param \core\event\role_unassigned $event
*/
public static function role_unassigned(\core\event\role_unassigned $event) {
global $DB;
if (!enrol_is_enabled('category')) {
return;
}
$ra = new stdClass();
$ra->userid = $event->relateduserid;
$ra->contextid = $event->contextid;
// only category level roles are interesting
$parentcontext = context::instance_by_id($ra->contextid);
if ($parentcontext->contextlevel != CONTEXT_COURSECAT) {
return;
}
// Now this is going to be a bit slow, take all enrolments in child courses and verify each separately.
$syscontext = context_system::instance();
if (!$roles = get_roles_with_capability('enrol/category:synchronised', CAP_ALLOW, $syscontext)) {
return;
}
$plugin = enrol_get_plugin('category');
$sql = "SELECT e.*
FROM {course} c
JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :courselevel AND ctx.path LIKE :match)
JOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'category')
JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = :userid)";
$params = array('courselevel'=>CONTEXT_COURSE, 'match'=>$parentcontext->path.'/%', 'userid'=>$ra->userid);
$rs = $DB->get_recordset_sql($sql, $params);
list($roleids, $params) = $DB->get_in_or_equal(array_keys($roles), SQL_PARAMS_NAMED, 'r');
$params['userid'] = $ra->userid;
foreach ($rs as $instance) {
$coursecontext = context_course::instance($instance->courseid);
$contextids = $coursecontext->get_parent_context_ids();
array_pop($contextids); // Remove system context, we are interested in categories only.
list($contextids, $contextparams) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED, 'c');
$params = array_merge($params, $contextparams);
$sql = "SELECT ra.id
FROM {role_assignments} ra
WHERE ra.userid = :userid AND ra.contextid $contextids AND ra.roleid $roleids";
if (!$DB->record_exists_sql($sql, $params)) {
// User does not have any interesting role in any parent context, let's unenrol.
$plugin->unenrol_user($instance, $ra->userid);
}
}
$rs->close();
}
}

View File

@ -25,20 +25,16 @@
defined('MOODLE_INTERNAL') || die();
/* List of handlers */
$handlers = array (
'role_assigned' => array (
'handlerfile' => '/enrol/category/locallib.php',
'handlerfunction' => array('enrol_category_handler', 'role_assigned'),
'schedule' => 'instant',
'internal' => 1,
$observers = array (
array (
'eventname' => '\core\event\role_assigned',
'callback' => 'enrol_category_observer::role_assigned',
),
'role_unassigned' => array (
'handlerfile' => '/enrol/category/locallib.php',
'handlerfunction' => array('enrol_category_handler', 'role_unassigned'),
'schedule' => 'instant',
'internal' => 1,
array (
'eventname' => '\core\event\role_unassigned',
'callback' => 'enrol_category_observer::role_unassigned',
),
);
);

View File

@ -24,131 +24,6 @@
defined('MOODLE_INTERNAL') || die();
/**
* Event handler for category enrolment plugin.
*
* We try to keep everything in sync via listening to events,
* it may fail sometimes, so we always do a full sync in cron too.
*/
class enrol_category_handler {
/**
* Triggered when user is assigned a new role.
* @static
* @param stdClass $ra
* @return bool
*/
public static function role_assigned($ra) {
global $DB;
if (!enrol_is_enabled('category')) {
return true;
}
//only category level roles are interesting
$parentcontext = context::instance_by_id($ra->contextid);
if ($parentcontext->contextlevel != CONTEXT_COURSECAT) {
return true;
}
// Make sure the role is to be actually synchronised,
// please note we are ignoring overrides of the synchronised capability (for performance reasons in full sync).
$syscontext = context_system::instance();
if (!$DB->record_exists('role_capabilities', array('contextid'=>$syscontext->id, 'roleid'=>$ra->roleid, 'capability'=>'enrol/category:synchronised', 'permission'=>CAP_ALLOW))) {
return true;
}
// Add necessary enrol instances.
$plugin = enrol_get_plugin('category');
$sql = "SELECT c.*
FROM {course} c
JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :courselevel AND ctx.path LIKE :match)
LEFT JOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'category')
WHERE e.id IS NULL";
$params = array('courselevel'=>CONTEXT_COURSE, 'match'=>$parentcontext->path.'/%');
$rs = $DB->get_recordset_sql($sql, $params);
foreach ($rs as $course) {
$plugin->add_instance($course);
}
$rs->close();
// Now look for missing enrolments.
$sql = "SELECT e.*
FROM {course} c
JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :courselevel AND ctx.path LIKE :match)
JOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'category')
LEFT JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = :userid)
WHERE ue.id IS NULL";
$params = array('courselevel'=>CONTEXT_COURSE, 'match'=>$parentcontext->path.'/%', 'userid'=>$ra->userid);
$rs = $DB->get_recordset_sql($sql, $params);
foreach ($rs as $instance) {
$plugin->enrol_user($instance, $ra->userid, null, $ra->timemodified);
}
$rs->close();
return true;
}
/**
* Triggered when user role is unassigned.
* @static
* @param stdClass $ra
* @return bool
*/
public static function role_unassigned($ra) {
global $DB;
if (!enrol_is_enabled('category')) {
return true;
}
// only category level roles are interesting
$parentcontext = context::instance_by_id($ra->contextid);
if ($parentcontext->contextlevel != CONTEXT_COURSECAT) {
return true;
}
// Now this is going to be a bit slow, take all enrolments in child courses and verify each separately.
$syscontext = context_system::instance();
if (!$roles = get_roles_with_capability('enrol/category:synchronised', CAP_ALLOW, $syscontext)) {
return true;
}
$plugin = enrol_get_plugin('category');
$sql = "SELECT e.*
FROM {course} c
JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :courselevel AND ctx.path LIKE :match)
JOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'category')
JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = :userid)";
$params = array('courselevel'=>CONTEXT_COURSE, 'match'=>$parentcontext->path.'/%', 'userid'=>$ra->userid);
$rs = $DB->get_recordset_sql($sql, $params);
list($roleids, $params) = $DB->get_in_or_equal(array_keys($roles), SQL_PARAMS_NAMED, 'r');
$params['userid'] = $ra->userid;
foreach ($rs as $instance) {
$coursecontext = context_course::instance($instance->courseid);
$contextids = $coursecontext->get_parent_context_ids();
array_pop($contextids); // Remove system context, we are interested in categories only.
list($contextids, $contextparams) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED, 'c');
$params = array_merge($params, $contextparams);
$sql = "SELECT ra.id
FROM {role_assignments} ra
WHERE ra.userid = :userid AND ra.contextid $contextids AND ra.roleid $roleids";
if (!$DB->record_exists_sql($sql, $params)) {
// User does not have any interesting role in any parent context, let's unenrol.
$plugin->unenrol_user($instance, $ra->userid);
}
}
$rs->close();
return true;
}
}
/**
* Sync all category enrolments in one course
* @param stdClass $course

View File

@ -25,10 +25,7 @@
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot.'/enrol/category/locallib.php');
class enrol_category_testcase extends advanced_testcase {
class enrol_category_plugin_testcase extends advanced_testcase {
protected function enable_plugin() {
$enabled = enrol_get_plugins(true);
@ -107,7 +104,8 @@ class enrol_category_testcase extends advanced_testcase {
}
public function test_handler_sync() {
global $DB;
global $DB, $CFG;
require_once($CFG->dirroot.'/enrol/category/locallib.php');
$this->resetAfterTest();
@ -179,7 +177,8 @@ class enrol_category_testcase extends advanced_testcase {
}
public function test_sync_course() {
global $DB;
global $DB, $CFG;
require_once($CFG->dirroot.'/enrol/category/locallib.php');
$this->resetAfterTest();
@ -272,7 +271,8 @@ class enrol_category_testcase extends advanced_testcase {
}
public function test_sync_full() {
global $DB;
global $DB, $CFG;
require_once($CFG->dirroot.'/enrol/category/locallib.php');
$this->resetAfterTest();

View File

@ -47,6 +47,7 @@ $string['cachedef_eventinvalidation'] = 'Event invalidation';
$string['cachedef_groupdata'] = 'Course group information';
$string['cachedef_htmlpurifier'] = 'HTML Purifier - cleaned content';
$string['cachedef_locking'] = 'Locking';
$string['cachedef_observers'] = 'Event observers';
$string['cachedef_plugininfo_base'] = 'Plugin info - base';
$string['cachedef_plugininfo_block'] = 'Plugin info - blocks';
$string['cachedef_plugininfo_filter'] = 'Plugin info - filters';

View File

@ -1685,7 +1685,11 @@ function role_assign($roleid, $userid, $contextid, $component = '', $itemid = 0,
reload_all_capabilities();
}
events_trigger('role_assigned', $ra);
$event = \core\event\role_assigned::create(
array('context'=>$context, 'objectid'=>$ra->roleid, 'relateduserid'=>$ra->userid,
'other'=>array('id'=>$ra->id, 'component'=>$ra->component, 'itemid'=>$ra->itemid)));
$event->add_record_snapshot('role_assignments', $ra);
$event->trigger();
return $ra->id;
}
@ -1769,8 +1773,12 @@ function role_unassign_all(array $params, $subcontexts = false, $includemanual =
if (!empty($USER->id) && $USER->id == $ra->userid) {
reload_all_capabilities();
}
$event = \core\event\role_unassigned::create(
array('context'=>$context, 'objectid'=>$ra->roleid, 'relateduserid'=>$ra->userid,
'other'=>array('id'=>$ra->id, 'component'=>$ra->component, 'itemid'=>$ra->itemid)));
$event->add_record_snapshot('role_assignments', $ra);
$event->trigger();
}
events_trigger('role_unassigned', $ra);
}
unset($ras);
@ -1796,7 +1804,11 @@ function role_unassign_all(array $params, $subcontexts = false, $includemanual =
if (!empty($USER->id) && $USER->id == $ra->userid) {
reload_all_capabilities();
}
events_trigger('role_unassigned', $ra);
$event = \core\event\role_unassigned::create(
array('context'=>$context, 'objectid'=>$ra->roleid, 'relateduserid'=>$ra->userid,
'other'=>array('id'=>$ra->id, 'component'=>$ra->component, 'itemid'=>$ra->itemid)));
$event->add_record_snapshot('role_assignments', $ra);
$event->trigger();
}
}
}
@ -6901,7 +6913,7 @@ class context_module extends context {
* Is this context part of any course? If yes return course context.
*
* @param bool $strict true means throw exception if not found, false means return false if not found
* @return course_context context of the enclosing course, null if not found or exception
* @return context_course context of the enclosing course, null if not found or exception
*/
public function get_course_context($strict = true) {
return $this->get_parent_context();

617
lib/classes/event/base.php Normal file
View File

@ -0,0 +1,617 @@
<?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/>.
namespace core\event;
/**
* Base event class.
*
* @package core
* @copyright 2013 Petr Skoda {@link http://skodak.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* All other event classes must extend this class.
*
* @package core
* @copyright 2013 Petr Skoda {@link http://skodak.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*
* @property-read string $eventname Name of the event (=== class name with leading \)
* @property-read string $component Full frankenstyle component name
* @property-read string $action what happened
* @property-read string $target what/who was target of the action
* @property-read string $objecttable name of database table where is object record stored
* @property-read int $objectid optional id of the object
* @property-read string $crud letter indicating event type
* @property-read int $level log level (number between 1 and 100)
* @property-read int $contextid
* @property-read int $contextlevel
* @property-read int $contextinstanceid
* @property-read int $userid who did this?
* @property-read int $courseid
* @property-read int $relateduserid
* @property-read mixed $other array or scalar, can not contain objects
* @property-read int $timecreated
*/
abstract class base implements \IteratorAggregate {
/** @var array event data */
protected $data;
/** @var array the format is standardised by logging API */
protected $logextra;
/** @var \context of this event */
protected $context;
/**
* @var bool indicates if event was already triggered,
* this prevents second attempt to trigger event.
*/
private $triggered;
/**
* @var bool indicates if event was already dispatched,
* this prevents direct calling of manager::dispatch($event).
*/
private $dispatched;
/**
* @var bool indicates if event was restored from storage,
* this prevents triggering of restored events.
*/
private $restored;
/** @var array list of event properties */
private static $fields = array(
'eventname', 'component', 'action', 'target', 'objecttable', 'objectid', 'crud', 'level', 'contextid',
'contextlevel', 'contextinstanceid', 'userid', 'courseid', 'relateduserid', 'other',
'timecreated');
/** @var array simple record cache */
private $recordsnapshots = array();
/**
* Private constructor, use create() or restore() methods instead.
*/
private final function __construct() {
$this->data = array_fill_keys(self::$fields, null);
}
/**
* Create new event.
*
* The optional data keys as:
* 1/ objectid - the id of the object specified in class name
* 2/ context - the context of this event
* 3/ other - the other data describing the event, can not contain objects
* 4/ relateduserid - the id of user which is somehow related to this event
*
* @param array $data
* @return \core\event\base returns instance of new event
*
* @throws \coding_exception
*/
public static final function create(array $data = null) {
global $PAGE, $USER;
$data = (array)$data;
/** @var \core\event\base $event */
$event = new static();
$event->triggered = false;
$event->restored = false;
$event->dispatched = false;
// Set static event data specific for child class.
$event->init();
// Set automatic data.
$event->data['timecreated'] = time();
$classname = get_class($event);
$parts = explode('\\', $classname);
if (count($parts) !== 3 or $parts[1] !== 'event') {
throw new \coding_exception("Invalid event class name '$classname', it must be defined in component\\event\\ namespace");
}
$event->data['eventname'] = '\\'.$classname;
$event->data['component'] = $parts[0];
$pos = strrpos($parts[2], '_');
if ($pos === false) {
throw new \coding_exception("Invalid event class name '$classname', there must be at least one underscore separating object and action words");
}
$event->data['target'] = substr($parts[2], 0, $pos);
$event->data['action'] = substr($parts[2], $pos+1);
// Set optional data or use defaults.
$event->data['objectid'] = isset($data['objectid']) ? $data['objectid'] : null;
$event->data['courseid'] = isset($data['courseid']) ? $data['courseid'] : null;
$event->data['userid'] = isset($data['userid']) ? $data['userid'] : $USER->id;
$event->data['other'] = isset($data['other']) ? $data['other'] : null;
$event->data['relateduserid'] = isset($data['relateduserid']) ? $data['relateduserid'] : null;
if (isset($event->context)) {
if (isset($data['context'])) {
debugging('Context was already set in init() method, ignoring context parameter', DEBUG_DEVELOPER);
}
} else if (!empty($data['context'])) {
$event->context = $data['context'];
} else if (!empty($data['contextid'])) {
$event->context = \context::instance_by_id($data['contextid'], MUST_EXIST);
} else {
throw new \coding_exception('context (or contextid) is a required event property, system context may be hardcoded in init() method.');
}
$event->data['contextid'] = $event->context->id;
$event->data['contextlevel'] = $event->context->contextlevel;
$event->data['contextinstanceid'] = $event->context->instanceid;
if (!isset($event->data['courseid'])) {
if ($coursecontext = $event->context->get_course_context(false)) {
$event->data['courseid'] = $coursecontext->id;
} else {
$event->data['courseid'] = 0;
}
}
if (!array_key_exists('relateduserid', $data) and $event->context->contextlevel == CONTEXT_USER) {
$event->data['relateduserid'] = $event->context->instanceid;
}
// Warn developers if they do something wrong.
if (debugging('', DEBUG_DEVELOPER)) { // This should be replaced by new $CFG->slowdebug flag if introduced.
static $automatickeys = array('eventname', 'component', 'action', 'target', 'contextlevel', 'contextinstanceid', 'timecreated');
static $initkeys = array('crud', 'level', 'objecttable');
foreach ($data as $key => $ignored) {
if ($key === 'context') {
continue;
} else if (in_array($key, $automatickeys)) {
debugging("Data key '$key' is not allowed in \\core\\event\\base::create() method, it is set automatically");
} else if (in_array($key, $initkeys)) {
debugging("Data key '$key' is not allowed in \\core\\event\\base::create() method, you need to set it in init() method");
} else if (!in_array($key, self::$fields)) {
debugging("Data key '$key' does not exist in \\core\\event\\base");
}
}
}
// Let developers validate their custom data (such as $this->data['other'], contextlevel, etc.).
$event->validate_data();
return $event;
}
/**
* Override in subclass.
*
* Set all required data properties:
* 1/ crud - letter [crud] TODO: MDL-37658
* 2/ level - number 1...100 TODO: MDL-37658
* 3/ objecttable - name of database table if objectid specified
*
* Optionally it can set:
* a/ fixed system context
*
* @return void
*/
protected abstract function init();
/**
* Let developers validate their custom data (such as $this->data['other'], contextlevel, etc.).
*
* Throw \coding_exception or debugging() notice in case of any problems.
*/
protected function validate_data() {
// Override if you want to validate event properties when
// creating new events.
}
/**
* Returns localised general event name.
*
* Override in subclass, we can not make it static and abstract at the same time.
*
* TODO: MDL-37658
*
* @return string|\lang_string
*/
public static function get_name() {
// Override in subclass with real lang string.
$parts = explode('\\', __CLASS__);
if (count($parts) !== 3) {
return 'unknown event';
}
return $parts[0].': '.str_replace('_', ' ', $parts[2]);
}
/**
* Returns localised description of what happened.
*
* TODO: MDL-37658
*
* @return string|\lang_string
*/
public function get_description() {
return null;
}
/**
* Define whether a user can view the event or not.
*
* @param int|\stdClass $user_or_id ID of the user.
* @return bool True if the user can view the event, false otherwise.
*/
public function can_view($user_or_id = null) {
return is_siteadmin($user_or_id);
}
/**
* Restore event from existing historic data.
*
* @param array $data
* @param array $logextra the format is standardised by logging API
* @return bool|\core\event\base
*/
public static final function restore(array $data, array $logextra) {
$classname = $data['eventname'];
$component = $data['component'];
$action = $data['action'];
$target = $data['target'];
// Security: make 100% sure this really is an event class.
if ($classname !== "\\{$component}\\event\\{$target}_{$action}") {
return false;
}
if (!class_exists($classname)) {
return false;
}
$event = new $classname();
if (!($event instanceof \core\event\base)) {
return false;
}
$event->restored = true;
$event->triggered = true;
$event->dispatched = true;
$event->logextra = $logextra;
foreach (self::$fields as $key) {
if (!array_key_exists($key, $data)) {
debugging("Event restore data must contain key $key");
$data[$key] = null;
}
}
if (count($data) != count(self::$fields)) {
foreach ($data as $key => $value) {
if (!in_array($key, self::$fields)) {
debugging("Event restore data cannot contain key $key");
unset($data[$key]);
}
}
}
$event->data = $data;
return $event;
}
/**
* Returns event context.
* @return \context
*/
public function get_context() {
if (isset($this->context)) {
return $this->context;
}
$this->context = \context::instance_by_id($this->data['contextid'], false);
return $this->context;
}
/**
* Returns relevant URL, override in subclasses.
* @return \moodle_url
*/
public function get_url() {
return null;
}
/**
* Return standardised event data as array.
*
* @return array
*/
public function get_data() {
return $this->data;
}
/**
* Return auxiliary data that was stored in logs.
*
* TODO: MDL-37658
*
* @return array the format is standardised by logging API
*/
public function get_logextra() {
return $this->logextra;
}
/**
* Does this event replace legacy event?
*
* Note: do not use directly!
*
* @return null|string legacy event name
*/
protected function get_legacy_eventname() {
return null;
}
/**
* Legacy event data if get_legacy_eventname() is not empty.
*
* Note: do not use directly!
*
* @return mixed
*/
protected function get_legacy_eventdata() {
return null;
}
/**
* Doest this event replace add_to_log() statement?
*
* Note: do not use directly!
*
* @return null|array of parameters to be passed to legacy add_to_log() function.
*/
protected function get_legacy_logdata() {
return null;
}
/**
* Validate all properties right before triggering the event.
*
* This throws coding exceptions for fatal problems and debugging for minor problems.
*
* @throws \coding_exception
*/
protected final function validate_before_trigger() {
global $DB;
if (empty($this->data['crud'])) {
throw new \coding_exception('crud must be specified in init() method of each method');
}
if (empty($this->data['level'])) {
throw new \coding_exception('level must be specified in init() method of each method');
}
if (!empty($this->data['objectid']) and empty($this->data['objecttable'])) {
throw new \coding_exception('objecttable must be specified in init() method if objectid present');
}
if (debugging('', DEBUG_DEVELOPER)) { // This should be replaced by new $CFG->slowdebug flag if introduced.
// Ideally these should be coding exceptions, but we need to skip these for performance reasons
// on production servers.
if (!in_array($this->data['crud'], array('c', 'r', 'u', 'd'), true)) {
debugging("Invalid event crud value specified.");
}
if (!is_number($this->data['level'])) {
debugging('Event property level must be a number');
}
if (self::$fields !== array_keys($this->data)) {
debugging('Number of event data fields must not be changed in event classes');
}
$encoded = json_encode($this->data['other']);
if ($encoded === false or $this->data['other'] !== json_decode($encoded, true)) {
debugging('other event data must be compatible with json encoding');
}
if ($this->data['userid'] and !is_number($this->data['userid'])) {
debugging('Event property userid must be a number');
}
if ($this->data['courseid'] and !is_number($this->data['courseid'])) {
debugging('Event property courseid must be a number');
}
if ($this->data['objectid'] and !is_number($this->data['objectid'])) {
debugging('Event property objectid must be a number');
}
if ($this->data['relateduserid'] and !is_number($this->data['relateduserid'])) {
debugging('Event property relateduserid must be a number');
}
if ($this->data['objecttable']) {
if (!$DB->get_manager()->table_exists($this->data['objecttable'])) {
debugging('Unknown table specified in objecttable field');
}
}
}
}
/**
* Trigger event.
*/
public final function trigger() {
global $CFG;
if ($this->restored) {
throw new \coding_exception('Can not trigger restored event');
}
if ($this->triggered or $this->dispatched) {
throw new \coding_exception('Can not trigger event twice');
}
$this->validate_before_trigger();
$this->triggered = true;
if (isset($CFG->loglifetime) and $CFG->loglifetime != -1) {
if ($data = $this->get_legacy_logdata()) {
call_user_func_array('add_to_log', $data);
}
}
if (PHPUNIT_TEST and \phpunit_util::is_redirecting_events()) {
$this->dispatched = true;
\phpunit_util::event_triggered($this);
return;
}
\core\event\manager::dispatch($this);
$this->dispatched = true;
if ($legacyeventname = $this->get_legacy_eventname()) {
events_trigger($legacyeventname, $this->get_legacy_eventdata());
}
}
/**
* Was this event already triggered?
*
* @return bool
*/
public final function is_triggered() {
return $this->triggered;
}
/**
* Used from event manager to prevent direct access.
*
* @return bool
*/
public final function is_dispatched() {
return $this->dispatched;
}
/**
* Was this event restored?
*
* @return bool
*/
public final function is_restored() {
return $this->restored;
}
/**
* Add cached data that will be most probably used in event observers.
*
* This is used to improve performance, but it is required for data
* that was just deleted.
*
* @param string $tablename
* @param \stdClass $record
*
* @throws \coding_exception if used after ::trigger()
*/
public final function add_record_snapshot($tablename, $record) {
global $DB;
if ($this->triggered) {
throw new \coding_exception('It is not possible to add snapshots after triggering of events');
}
// NOTE: this might use some kind of MUC cache,
// hopefully we will not run out of memory here...
if (debugging('', DEBUG_DEVELOPER)) { // This should be replaced by new $CFG->slowdebug flag if introduced.
if (!$DB->get_manager()->table_exists($tablename)) {
debugging("Invalid table name '$tablename' specified, database table does not exist.");
}
}
$this->recordsnapshots[$tablename][$record->id] = $record;
}
/**
* Returns cached record or fetches data from database if not cached.
*
* @param string $tablename
* @param int $id
* @return \stdClass
*
* @throws \coding_exception if used after ::restore()
*/
public final function get_record_snapshot($tablename, $id) {
global $DB;
if ($this->restored) {
throw new \coding_exception('It is not possible to get snapshots from restored events');
}
if (isset($this->recordsnapshots[$tablename][$id])) {
return $this->recordsnapshots[$tablename][$id];
}
$record = $DB->get_record($tablename, array('id'=>$id));
$this->recordsnapshots[$tablename][$id] = $record;
return $record;
}
/**
* Magic getter for read only access.
*
* @param string $name
* @return mixed
*/
public function __get($name) {
if (array_key_exists($name, $this->data)) {
return $this->data[$name];
}
debugging("Accessing non-existent event property '$name'");
}
/**
* Magic setter.
*
* Note: we must not allow modification of data from outside,
* after trigger() the data MUST NOT CHANGE!!!
*
* @param string $name
* @param mixed $value
*
* @throws \coding_exception
*/
public function __set($name, $value) {
throw new \coding_exception('Event properties must not be modified.');
}
/**
* Is data property set?
*
* @param string $name
* @return bool
*/
public function __isset($name) {
return isset($this->data[$name]);
}
/**
* Create an iterator because magic vars can't be seen by 'foreach'.
*
* @return \ArrayIterator
*/
public function getIterator() {
return new \ArrayIterator($this->data);
}
}

View File

@ -0,0 +1,349 @@
<?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/>.
namespace core\event;
/**
* New event manager class.
*
* @package core
* @copyright 2013 Petr Skoda {@link http://skodak.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Class used for event dispatching.
*
* Note: Do NOT use directly in your code, it is intended to be used from
* base event class only.
*/
class manager {
/** @var array buffer of event for dispatching */
protected static $buffer = array();
/** @var array buffer for events that were not sent to external observers when DB transaction in progress */
protected static $extbuffer = array();
/** @var bool evert dispatching already in progress - prevents nesting */
protected static $dispatching = false;
/** @var array cache of all observers */
protected static $allobservers = null;
/** @var bool should we reload observers after the test? */
protected static $reloadaftertest = false;
/**
* Trigger new event.
*
* @internal to be used only from \core\event\base::trigger() method.
* @param \core\event\base $event
*
* @throws \coding_Exception if used directly.
*/
public static function dispatch(\core\event\base $event) {
if (during_initial_install()) {
return;
}
if (!$event->is_triggered() or $event->is_dispatched()) {
throw new \coding_exception('Illegal event dispatching attempted.');
}
self::$buffer[] = $event;
if (self::$dispatching) {
return;
}
self::$dispatching = true;
self::process_buffers();
self::$dispatching = false;
}
/**
* Notification from DML layer.
* @internal to be used from DML layer only.
*/
public static function database_transaction_commited() {
if (self::$dispatching or empty(self::$extbuffer)) {
return;
}
self::$dispatching = true;
self::process_buffers();
self::$dispatching = false;
}
/**
* Notification from DML layer.
* @internal to be used from DML layer only.
*/
public static function database_transaction_rolledback() {
self::$extbuffer = array();
}
protected static function process_buffers() {
global $DB, $CFG;
while (self::$buffer or self::$extbuffer) {
$fromextbuffer = false;
$addedtoextbuffer = false;
if (self::$extbuffer and !$DB->is_transaction_started()) {
$fromextbuffer = true;
$event = reset(self::$extbuffer);
unset(self::$extbuffer[key(self::$extbuffer)]);
} else if (self::$buffer) {
$event = reset(self::$buffer);
unset(self::$buffer[key(self::$buffer)]);
} else {
return;
}
$observers = self::get_event_observers('\\'.get_class($event));
foreach ($observers as $observer) {
if ($observer->internal) {
if ($fromextbuffer) {
// Do not send buffered external events to internal handlers,
// they processed them already.
continue;
}
} else {
if ($DB->is_transaction_started()) {
if ($fromextbuffer) {
// Weird!
continue;
}
// Do not notify external observers while in DB transaction.
if (!$addedtoextbuffer) {
self::$extbuffer[] = $event;
$addedtoextbuffer = true;
}
continue;
}
}
if (isset($observer->includefile) and file_exists($observer->includefile)) {
include_once($observer->includefile);
}
if (is_callable($observer->callable)) {
try {
call_user_func($observer->callable, $event);
} catch (\Exception $e) {
// Observers are notified before installation and upgrade, this may throw errors.
if (empty($CFG->upgraderunning)) {
// Ignore errors during upgrade, otherwise warn developers.
debugging("Exception encountered in event observer '$observer->callable': ".$e->getMessage(), DEBUG_DEVELOPER, $e->getTrace());
}
}
} else {
debugging("Can not execute event observer '$observer->callable'");
}
}
// TODO: Invent some infinite loop protection in case events cross-trigger one another.
}
}
/**
* Returns list of event observers.
* @param string $classname
* @return array
*/
protected static function get_event_observers($classname) {
self::init_all_observers();
if (isset(self::$allobservers[$classname])) {
return self::$allobservers[$classname];
}
if (isset(self::$allobservers['*'])) {
return self::$allobservers['*'];
}
return array();
}
/**
* Initialise the list of observers.
*/
protected static function init_all_observers() {
global $CFG;
if (is_array(self::$allobservers)) {
return;
}
if (!PHPUNIT_TEST and !during_initial_install()) {
$cache = \cache::make('core', 'observers');
$cached = $cache->get('all');
$dirroot = $cache->get('dirroot');
if ($dirroot === $CFG->dirroot and is_array($cached)) {
self::$allobservers = $cached;
return;
}
}
self::$allobservers = array();
$plugintypes = \core_component::get_plugin_types();
$systemdone = false;
foreach ($plugintypes as $plugintype => $ignored) {
$plugins = \core_component::get_plugin_list($plugintype);
if (!$systemdone) {
$plugins[] = "$CFG->dirroot/lib";
$systemdone = true;
}
foreach ($plugins as $fulldir) {
if (!file_exists("$fulldir/db/events.php")) {
continue;
}
$observers = null;
include("$fulldir/db/events.php");
if (!is_array($observers)) {
continue;
}
self::add_observers($observers, "$fulldir/db/events.php");
}
}
self::order_all_observers();
if (!PHPUNIT_TEST and !during_initial_install()) {
$cache->set('all', self::$allobservers);
$cache->set('dirroot', $CFG->dirroot);
}
}
/**
* Add observers.
* @param array $observers
* @param string $file
*/
protected static function add_observers(array $observers, $file) {
global $CFG;
foreach ($observers as $observer) {
if (empty($observer['eventname']) or !is_string($observer['eventname'])) {
debugging("Invalid 'eventname' detected in $file observer definition", DEBUG_DEVELOPER);
continue;
}
if ($observer['eventname'] !== '*' and strpos($observer['eventname'], '\\') !== 0) {
$observer['eventname'] = '\\'.$observer['eventname'];
}
if (empty($observer['callback'])) {
debugging("Invalid 'callback' detected in $file observer definition", DEBUG_DEVELOPER);
continue;
}
$o = new \stdClass();
$o->callable = $observer['callback'];
if (!isset($observer['priority'])) {
$o->priority = 0;
} else {
$o->priority = (int)$observer['priority'];
}
if (!isset($observer['internal'])) {
$o->internal = true;
} else {
$o->internal = (bool)$observer['internal'];
}
if (empty($observer['includefile'])) {
$o->includefile = null;
} else {
if ($CFG->admin !== 'admin' and strpos($observer['includefile'], '/admin/') === 0) {
$observer['includefile'] = preg_replace('|^/admin/|', '/'.$CFG->admin.'/', $observer['includefile']);
}
if (!file_exists($observer['includefile'])) {
debugging("Invalid 'includefile' detected in $file observer definition", DEBUG_DEVELOPER);
continue;
}
$o->includefile = $observer['includefile'];
}
self::$allobservers[$observer['eventname']][] = $o;
}
}
/**
* Reorder observers to allow quick lookup of observer for each event.
*/
protected static function order_all_observers() {
$catchall = array();
if (isset(self::$allobservers['*'])) {
$catchall = self::$allobservers['*'];
unset(self::$allobservers['*']); // Move it to the end.
\core_collator::asort_objects_by_property($catchall, 'priority', \core_collator::SORT_NUMERIC);
$catchall = array_reverse($catchall);
self::$allobservers['*'] = $catchall;
}
foreach (self::$allobservers as $classname => $observers) {
if ($classname === '*') {
continue;
}
if ($catchall) {
$observers = array_merge($observers, $catchall);
}
\core_collator::asort_objects_by_property($observers, 'priority', \core_collator::SORT_NUMERIC);
self::$allobservers[$classname] = array_reverse($observers);
}
}
/**
* Replace all standard observers.
* @param array $observers
* @return array
*
* @throws \coding_Exception if used outside of unit tests.
*/
public static function phpunit_replace_observers(array $observers) {
if (!PHPUNIT_TEST) {
throw new \coding_exception('Cannot override event observers outside of phpunit tests!');
}
self::phpunit_reset();
self::$allobservers = array();
self::$reloadaftertest = true;
self::add_observers($observers, 'phpunit');
self::order_all_observers();
return self::$allobservers;
}
/**
* Reset everything if necessary.
* @private
*
* @throws \coding_Exception if used outside of unit tests.
*/
public static function phpunit_reset() {
if (!PHPUNIT_TEST) {
throw new \coding_exception('Cannot reset event manager outside of phpunit tests!');
}
self::$buffer = array();
self::$extbuffer = array();
self::$dispatching = false;
if (!self::$reloadaftertest) {
self::$allobservers = null;
}
self::$reloadaftertest = false;
}
}

View File

@ -0,0 +1,80 @@
<?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/>.
namespace core\event;
/**
* Role assigned event.
*
* @package core
* @copyright 2013 Petr Skoda {@link http://skodak.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class role_assigned extends base {
protected function init() {
$this->data['objecttable'] = 'role';
$this->data['crud'] = 'c';
// TODO: MDL-37658 set level
$this->data['level'] = 50;
}
/**
* Returns localised general event name.
*
* @return string|\lang_string
*/
public static function get_name() {
//TODO: MDL-37658 localise
return 'Role assigned';
}
/**
* Returns localised description of what happened.
*
* @return string|\lang_string
*/
public function get_description() {
//TODO: MDL-37658 localise
return 'Role '.$this->objectid.' was assigned to user '.$this->relateduserid.' in context '.$this->contextid;
}
/**
* Returns relevant URL.
* @return \moodle_url
*/
public function get_url() {
return new moodle_url('/admin/roles/assign.php', array('contextid'=>$this->contextid, 'roleid'=>$this->objectid));
}
/**
* Does this event replace legacy event?
*
* @return null|string legacy event name
*/
protected function get_legacy_eventname() {
return 'role_assigned';
}
/**
* Legacy event data if get_legacy_eventname() is not empty.
*
* @return mixed
*/
protected function get_legacy_eventdata() {
return $this->get_record_snapshot('role_assignments', $this->data['other']['id']);
}
}

View File

@ -0,0 +1,80 @@
<?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/>.
namespace core\event;
/**
* Role unassigned event.
*
* @package core
* @copyright 2013 Petr Skoda {@link http://skodak.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class role_unassigned extends base {
protected function init() {
$this->data['objecttable'] = 'role';
$this->data['crud'] = 'd';
// TODO: MDL-37658 set level
$this->data['level'] = 50;
}
/**
* Returns localised general event name.
*
* @return string|\lang_string
*/
public static function get_name() {
//TODO: MDL-37658 localise
return 'Role unassigned';
}
/**
* Returns localised description of what happened.
*
* @return string|\lang_string
*/
public function get_description() {
//TODO: MDL-37658 localise
return 'Role '.$this->objectid.'was unassigned from user '.$this->relateduserid.' in context '.$this->contextid;
}
/**
* Returns relevant URL.
* @return \moodle_url
*/
public function get_url() {
return new moodle_url('/admin/roles/assign.php', array('contextid'=>$this->contextid, 'roleid'=>$this->objectid));
}
/**
* Does this event replace legacy event?
*
* @return null|string legacy event name
*/
protected function get_legacy_eventname() {
return 'role_unassigned';
}
/**
* Legacy event data if get_legacy_eventname() is not empty.
*
* @return mixed
*/
protected function get_legacy_eventdata() {
return $this->get_record_snapshot('role_assignments', $this->data['other']['id']);
}
}

View File

@ -121,6 +121,15 @@ $definitions = array(
'mode' => cache_store::MODE_APPLICATION,
),
// Cache for the list of event observers.
'observers' => array(
'mode' => cache_store::MODE_APPLICATION,
'simplekeys' => true,
'simpledata' => true,
'persistent' => true,
'persistentmaxsize' => 2,
),
// Cache used by the {@link plugininfo_base} class.
'plugininfo_base' => array(
'mode' => cache_store::MODE_APPLICATION,

View File

@ -33,23 +33,10 @@
defined('MOODLE_INTERNAL') || die();
/* List of handlers */
/* List of legacy event handlers */
$handlers = array(
/*
* portfolio queued event - for non interactive file transfers
* NOTE: this is a HACK, please do not add any more things like this here
* (it is just abusing cron to do very time consuming things which is wrong any way)
*
* TODO: this has to be moved into separate queueing framework....
*/
'portfolio_send' => array (
'handlerfile' => '/lib/portfolio.php',
'handlerfunction' => 'portfolio_handle_event', // argument to call_user_func(), could be an array
'schedule' => 'cron',
'internal' => 0,
),
'course_completed' => array (
'handlerfile' => '/lib/badgeslib.php',
'handlerfunction' => 'badges_award_handle_course_criteria_review',
@ -69,6 +56,20 @@ $handlers = array(
'internal' => 1,
),
/*
* portfolio queued event - for non interactive file transfers
* NOTE: this is a HACK, please do not add any more things like this here
* (it is just abusing cron to do very time consuming things which is wrong any way)
*
* TODO: this has to be moved into separate queueing framework....
*/
'portfolio_send' => array (
'handlerfile' => '/lib/portfolio.php',
'handlerfunction' => 'portfolio_handle_event', // argument to call_user_func(), could be an array
'schedule' => 'cron',
'internal' => 0,
),
/* no more here please, core should not consume any events!!!!!!! */
);

View File

@ -2210,6 +2210,10 @@ abstract class moodle_database {
$this->commit_transaction();
}
array_pop($this->transactions);
if (empty($this->transactions)) {
\core\event\manager::database_transaction_commited();
}
}
/**
@ -2255,6 +2259,7 @@ abstract class moodle_database {
if (empty($this->transactions)) {
// finally top most level rolled back
$this->force_rollback = false;
\core\event\manager::database_transaction_rolledback();
}
throw $e;
}

View File

@ -319,6 +319,19 @@ abstract class advanced_testcase extends PHPUnit_Framework_TestCase {
return phpunit_util::start_message_redirection();
}
/**
* Starts event redirection.
*
* You can verify if events were triggered or not by inspecting the events
* array in the returned event sink instance. The redirection
* can be stopped by calling $sink->close();
*
* @return phpunit_event_sink
*/
public function redirectEvents() {
return phpunit_util::start_event_redirection();
}
/**
* Cleanup after all tests are executed.
*

View File

@ -0,0 +1,87 @@
<?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/>.
/**
* Event sink.
*
* @package core
* @category phpunit
* @copyright 2013 Petr Skoda {@link http://skodak.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Event redirection sink.
*
* @package core
* @category phpunit
* @copyright 2013 Petr Skoda {@link http://skodak.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class phpunit_event_sink {
/** @var \core\event\base[] array of events */
protected $events = array();
/**
* Stop event redirection.
*
* Use if you do not want event redirected any more.
*/
public function close() {
phpunit_util::stop_event_redirection();
}
/**
* To be called from phpunit_util only!
*
* @private
* @param \core\event\base $event record from event_read table
*/
public function add_event(\core\event\base $event) {
/* Number events from 0. */
$this->events[] = $event;
}
/**
* Returns all redirected events.
*
* The instances are records form the event_read table.
* The array indexes are numbered from 0 and the order is matching
* the creation of events.
*
* @return \core\event\base[]
*/
public function get_events() {
return $this->events;
}
/**
* Return number of events redirected to this sink.
*
* @return int
*/
public function count() {
return count($this->events);
}
/**
* Removes all previously stored events.
*/
public function clear() {
$this->events = array();
}
}

View File

@ -43,6 +43,9 @@ class phpunit_util extends testing_util {
/** @var phpunit_message_sink alternative target for moodle messaging */
protected static $messagesink = null;
/** @var phpunit_message_sink alternative target for moodle messaging */
protected static $eventsink = null;
/**
* @var array Files to skip when resetting dataroot folder
*/
@ -95,6 +98,9 @@ class phpunit_util extends testing_util {
// Stop any message redirection.
phpunit_util::stop_message_redirection();
// Stop any message redirection.
phpunit_util::stop_event_redirection();
// Release memory and indirectly call destroy() methods to release resource handles, etc.
gc_collect_cycles();
@ -182,6 +188,7 @@ class phpunit_util extends testing_util {
session_set_user($user);
// reset all static caches
\core\event\manager::phpunit_reset();
accesslib_clear_all_caches(true);
get_string_manager()->reset_caches(true);
reset_text_filters_cache(true);
@ -660,4 +667,57 @@ class phpunit_util extends testing_util {
self::$messagesink->add_message($message);
}
}
/**
* Start event redirection.
*
* @private
* Note: Do not call directly from tests,
* use $sink = $this->redirectEvents() instead.
*
* @return phpunit_event_sink
*/
public static function start_event_redirection() {
if (self::$eventsink) {
self::stop_event_redirection();
}
self::$eventsink = new phpunit_event_sink();
return self::$eventsink;
}
/**
* End event redirection.
*
* @private
* Note: Do not call directly from tests,
* use $sink->close() instead.
*/
public static function stop_event_redirection() {
self::$eventsink = null;
}
/**
* Are events redirected to some sink?
*
* Note: to be called from \core\event\base only!
*
* @private
* @return bool
*/
public static function is_redirecting_events() {
return !empty(self::$eventsink);
}
/**
* To be called from \core\event\base only!
*
* @private
* @param \core\event\base $event record from event_read table
* @return bool true means send event, false means event "sent" to sink.
*/
public static function event_triggered(\core\event\base $event) {
if (self::$eventsink) {
self::$eventsink->add_event($event);
}
}
}

View File

@ -29,6 +29,7 @@ require_once('PHPUnit/Autoload.php');
require_once('PHPUnit/Extensions/Database/Autoload.php');
require_once(__DIR__.'/classes/util.php');
require_once(__DIR__.'/classes/event_sink.php');
require_once(__DIR__.'/classes/message_sink.php');
require_once(__DIR__.'/classes/basic_testcase.php');
require_once(__DIR__.'/classes/database_driver_testcase.php');

View File

@ -491,6 +491,26 @@ class accesslib_testcase extends advanced_testcase {
$this->assertSame('1', $ras->itemid);
$this->assertEquals($USER->id, $ras->modifierid);
$this->assertEquals(666, $ras->timemodified);
// Test event triggered.
$user2 = $this->getDataGenerator()->create_user();
$sink = $this->redirectEvents();
$raid = role_assign($role->id, $user2->id, $context->id);
$events = $sink->get_events();
$sink->close();
$this->assertCount(1, $events);
$event = $events[0];
$this->assertInstanceOf('\core\event\role_assigned', $event);
$this->assertEquals('role', $event->target);
$this->assertEquals('role', $event->objecttable);
$this->assertEquals($role->id, $event->objectid);
$this->assertEquals($context->id, $event->contextid);
$this->assertEquals($user2->id, $event->relateduserid);
$this->assertCount(3, $event->other);
$this->assertEquals($raid, $event->other['id']);
$this->assertSame('', $event->other['component']);
$this->assertEquals(0, $event->other['itemid']);
}
/**
@ -506,7 +526,7 @@ class accesslib_testcase extends advanced_testcase {
$course = $this->getDataGenerator()->create_course();
$role = $DB->get_record('role', array('shortname'=>'student'));
$context = context_system::instance();
$context = context_course::instance($course->id);
role_assign($role->id, $user->id, $context->id);
$this->assertTrue($DB->record_exists('role_assignments', array('userid'=>$user->id, 'roleid'=>$role->id, 'contextid'=>$context->id)));
role_unassign($role->id, $user->id, $context->id);
@ -516,6 +536,25 @@ class accesslib_testcase extends advanced_testcase {
$this->assertTrue($DB->record_exists('role_assignments', array('userid'=>$user->id, 'roleid'=>$role->id, 'contextid'=>$context->id)));
role_unassign($role->id, $user->id, $context->id, 'enrol_self', 1);
$this->assertFalse($DB->record_exists('role_assignments', array('userid'=>$user->id, 'roleid'=>$role->id, 'contextid'=>$context->id)));
// Test event triggered.
role_assign($role->id, $user->id, $context->id);
$sink = $this->redirectEvents();
role_unassign($role->id, $user->id, $context->id);
$events = $sink->get_events();
$sink->close();
$this->assertCount(1, $events);
$event = $events[0];
$this->assertInstanceOf('\core\event\role_unassigned', $event);
$this->assertEquals('role', $event->target);
$this->assertEquals('role', $event->objecttable);
$this->assertEquals($role->id, $event->objectid);
$this->assertEquals($context->id, $event->contextid);
$this->assertEquals($user->id, $event->relateduserid);
$this->assertCount(3, $event->other);
$this->assertSame('', $event->other['component']);
$this->assertEquals(0, $event->other['itemid']);
}
/**
@ -530,6 +569,7 @@ class accesslib_testcase extends advanced_testcase {
$user = $this->getDataGenerator()->create_user();
$course = $this->getDataGenerator()->create_course();
$role = $DB->get_record('role', array('shortname'=>'student'));
$role2 = $DB->get_record('role', array('shortname'=>'teacher'));
$syscontext = context_system::instance();
$coursecontext = context_course::instance($course->id);
$page = $this->getDataGenerator()->create_module('page', array('course'=>$course->id));
@ -559,6 +599,18 @@ class accesslib_testcase extends advanced_testcase {
$this->assertEquals(4, $DB->count_records('role_assignments', array('userid'=>$user->id)));
role_unassign_all(array('userid'=>$user->id, 'contextid'=>$coursecontext->id, 'component'=>'enrol_self'), true, true);
$this->assertEquals(1, $DB->count_records('role_assignments', array('userid'=>$user->id)));
// Test events triggered.
role_assign($role2->id, $user->id, $coursecontext->id);
role_assign($role2->id, $user->id, $modcontext->id);
$sink = $this->redirectEvents();
role_unassign_all(array('userid'=>$user->id, 'roleid'=>$role2->id));
$events = $sink->get_events();
$sink->close();
$this->assertCount(2, $events);
$this->assertInstanceOf('\core\event\role_unassigned', $events[0]);
$this->assertInstanceOf('\core\event\role_unassigned', $events[1]);
}
/**

689
lib/tests/event_test.php Normal file
View File

@ -0,0 +1,689 @@
<?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/>.
/**
* Tests for event manager, base event and observers.
*
* @package core
* @category phpunit
* @copyright 2013 Petr Skoda {@link http://skodak.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__.'/fixtures/event_fixtures.php');
class core_event_testcase extends advanced_testcase {
protected function setUp() {
global $CFG;
// No need to always modify log table here.
$CFG->loglifetime = '-1';
}
protected function tearDown() {
global $CFG;
$CFG->loglifetime = '0';
}
public function test_event_properties() {
global $USER;
$system = \context_system::instance();
$event = \core_tests\event\unittest_executed::create(array('courseid'=>1, 'context'=>$system, 'objectid'=>5, 'other'=>array('sample'=>null, 'xx'=>10)));
$this->assertSame('\core_tests\event\unittest_executed', $event->eventname);
$this->assertSame('core_tests', $event->component);
$this->assertSame('executed', $event->action);
$this->assertSame('unittest', $event->target);
$this->assertSame(5, $event->objectid);
$this->assertSame('u', $event->crud);
$this->assertSame(10, $event->level);
$this->assertSame($system, $event->get_context());
$this->assertSame($system->id, $event->contextid);
$this->assertSame($system->contextlevel, $event->contextlevel);
$this->assertSame($system->instanceid, $event->contextinstanceid);
$this->assertSame($USER->id, $event->userid);
$this->assertSame(1, $event->courseid);
$this->assertNull($event->relateduserid);
$this->assertFalse(isset($event->relateduserid));
$this->assertSame(array('sample'=>null, 'xx'=>10), $event->other);
$this->assertTrue(isset($event->other['xx']));
$this->assertFalse(isset($event->other['sample']));
$this->assertLessThanOrEqual(time(), $event->timecreated);
try {
$event->courseid = 2;
$this->fail('Exception expected on event modification');
} catch (\moodle_exception $e) {
$this->assertInstanceOf('coding_exception', $e);
}
try {
$event->xxxx = 1;
$this->fail('Exception expected on event modification');
} catch (\moodle_exception $e) {
$this->assertInstanceOf('coding_exception', $e);
}
$event2 = \core_tests\event\unittest_executed::create(array('courseid'=>1, 'contextid'=>$system->id, 'objectid'=>5, 'other'=>array('sample'=>null, 'xx'=>10)));
$this->assertSame($event->get_context(), $event2->get_context());
}
public function test_observers_parsing() {
$observers = array(
array(
'eventname' => '\core_tests\event\unittest_executed',
'callback' => '\core_tests\event\unittest_observer::observe_one',
'includefile' => 'lib/tests/fixtures/event_fixtures.php',
),
array(
'eventname' => '*',
'callback' => array('\core_tests\event\unittest_observer', 'observe_all'),
'includefile' => null,
'internal' => 1,
'priority' => 9999,
),
array(
'eventname' => '\core\event\unknown_executed',
'callback' => '\core_tests\event\unittest_observer::broken_observer',
'priority' => 100,
),
array(
'eventname' => '\core_tests\event\unittest_executed',
'callback' => '\core_tests\event\unittest_observer::external_observer',
'priority' => 200,
'internal' => 0,
),
);
$result = \core\event\manager::phpunit_replace_observers($observers);
$this->assertCount(3, $result);
end($result);
$this->assertSame('*', key($result));
$expected = array();
$observer = new stdClass();
$observer->callable = array('\core_tests\event\unittest_observer', 'observe_all');
$observer->priority = 9999;
$observer->internal = true;
$observer->includefile = null;
$expected[0] = $observer;
$observer = new stdClass();
$observer->callable = '\core_tests\event\unittest_observer::external_observer';
$observer->priority = 200;
$observer->internal = false;
$observer->includefile = null;
$expected[1] = $observer;
$observer = new stdClass();
$observer->callable = '\core_tests\event\unittest_observer::observe_one';
$observer->priority = 0;
$observer->internal = true;
$observer->includefile = 'lib/tests/fixtures/event_fixtures.php';
$expected[2] = $observer;
$this->assertEquals($expected, $result['\core_tests\event\unittest_executed']);
$expected = array();
$observer = new stdClass();
$observer->callable = array('\core_tests\event\unittest_observer', 'observe_all');
$observer->priority = 9999;
$observer->internal = true;
$observer->includefile = null;
$expected[0] = $observer;
$observer = new stdClass();
$observer->callable = '\core_tests\event\unittest_observer::broken_observer';
$observer->priority = 100;
$observer->internal = true;
$observer->includefile = null;
$expected[1] = $observer;
$this->assertEquals($expected, $result['\core\event\unknown_executed']);
$expected = array();
$observer = new stdClass();
$observer->callable = array('\core_tests\event\unittest_observer', 'observe_all');
$observer->priority = 9999;
$observer->internal = true;
$observer->includefile = null;
$expected[0] = $observer;
$this->assertEquals($expected, $result['*']);
// Now test broken stuff...
$observers = array(
array(
'eventname' => 'core_tests\event\unittest_executed', // Fix leading backslash.
'callback' => '\core_tests\event\unittest_observer::observe_one',
'includefile' => 'lib/tests/fixtures/event_fixtures.php',
'internal' => 1, // Cast to bool.
),
);
$result = \core\event\manager::phpunit_replace_observers($observers);
$this->assertCount(1, $result);
$expected = array();
$observer = new stdClass();
$observer->callable = '\core_tests\event\unittest_observer::observe_one';
$observer->priority = 0;
$observer->internal = true;
$observer->includefile = 'lib/tests/fixtures/event_fixtures.php';
$expected[0] = $observer;
$this->assertEquals($expected, $result['\core_tests\event\unittest_executed']);
$observers = array(
array(
// Missing eventclass.
'callback' => '\core_tests\event\unittest_observer::observe_one',
'includefile' => 'lib/tests/fixtures/event_fixtures.php',
),
);
$result = \core\event\manager::phpunit_replace_observers($observers);
$this->assertCount(0, $result);
$this->assertDebuggingCalled();
$observers = array(
array(
'eventname' => '', // Empty eventclass.
'callback' => '\core_tests\event\unittest_observer::observe_one',
'includefile' => 'lib/tests/fixtures/event_fixtures.php',
),
);
$result = \core\event\manager::phpunit_replace_observers($observers);
$this->assertCount(0, $result);
$this->assertDebuggingCalled();
$observers = array(
array(
'eventname' => '\core_tests\event\unittest_executed',
// Missing callable.
'includefile' => 'lib/tests/fixtures/event_fixtures.php',
),
);
$result = \core\event\manager::phpunit_replace_observers($observers);
$this->assertCount(0, $result);
$this->assertDebuggingCalled();
$observers = array(
array(
'eventname' => '\core_tests\event\unittest_executed',
'callback' => '', // empty callable
'includefile' => 'lib/tests/fixtures/event_fixtures.php',
),
);
$result = \core\event\manager::phpunit_replace_observers($observers);
$this->assertCount(0, $result);
$this->assertDebuggingCalled();
$observers = array(
array(
'eventname' => '\core_tests\event\unittest_executed',
'callback' => '\core_tests\event\unittest_observer::observe_one',
'includefile' => 'lib/tests/fixtures/event_fixtures.php_xxx', // Missing file.
),
);
$result = \core\event\manager::phpunit_replace_observers($observers);
$this->assertCount(0, $result);
$this->assertDebuggingCalled();
}
public function test_normal_dispatching() {
$observers = array(
array(
'eventname' => '\core_tests\event\unittest_executed',
'callback' => '\core_tests\event\unittest_observer::observe_one',
),
array(
'eventname' => '*',
'callback' => '\core_tests\event\unittest_observer::observe_all',
'includefile' => null,
'internal' => 1,
'priority' => 9999,
),
);
\core\event\manager::phpunit_replace_observers($observers);
\core_tests\event\unittest_observer::reset();
$event1 = \core_tests\event\unittest_executed::create(array('courseid'=>1, 'context'=>\context_system::instance(), 'other'=>array('sample'=>1, 'xx'=>10)));
$event1->nest = 1;
$this->assertFalse($event1->is_triggered());
$this->assertFalse($event1->is_dispatched());
$this->assertFalse($event1->is_restored());
$event1->trigger();
$this->assertTrue($event1->is_triggered());
$this->assertTrue($event1->is_dispatched());
$this->assertFalse($event1->is_restored());
$event1 = \core_tests\event\unittest_executed::create(array('courseid'=>2, 'context'=>\context_system::instance(), 'other'=>array('sample'=>2, 'xx'=>10)));
$event1->trigger();
$this->assertSame(
array('observe_all-nesting-1', 'observe_one-1', 'observe_all-3', 'observe_one-3', 'observe_all-2', 'observe_one-2'),
\core_tests\event\unittest_observer::$info);
}
public function test_event_sink() {
$sink = $this->redirectEvents();
$event1 = \core_tests\event\unittest_executed::create(array('courseid'=>1, 'context'=>\context_system::instance(), 'other'=>array('sample'=>1, 'xx'=>10)));
$event1->trigger();
$this->assertSame(1, $sink->count());
$retult = $sink->get_events();
$this->assertSame($event1, $retult[0]);
$event2 = \core_tests\event\unittest_executed::create(array('courseid'=>1, 'context'=>\context_system::instance(), 'other'=>array('sample'=>2, 'xx'=>10)));
$event2->trigger();
$this->assertSame(2, $sink->count());
$retult = $sink->get_events();
$this->assertSame($event1, $retult[0]);
$this->assertSame($event2, $retult[1]);
$sink->clear();
$this->assertSame(0, $sink->count());
$this->assertSame(array(), $sink->get_events());
$event3 = \core_tests\event\unittest_executed::create(array('courseid'=>1, 'context'=>\context_system::instance(), 'other'=>array('sample'=>3, 'xx'=>10)));
$event3->trigger();
$this->assertSame(1, $sink->count());
$retult = $sink->get_events();
$this->assertSame($event3, $retult[0]);
$sink->close();
$event4 = \core_tests\event\unittest_executed::create(array('courseid'=>1, 'context'=>\context_system::instance(), 'other'=>array('sample'=>4, 'xx'=>10)));
$event4->trigger();
$this->assertSame(1, $sink->count());
$retult = $sink->get_events();
$this->assertSame($event3, $retult[0]);
}
public function test_ignore_exceptions() {
$observers = array(
array(
'eventname' => '\core_tests\event\unittest_executed',
'callback' => '\core_tests\event\unittest_observer::observe_one',
),
array(
'eventname' => '\core_tests\event\unittest_executed',
'callback' => '\core_tests\event\unittest_observer::broken_observer',
'priority' => 100,
),
);
\core\event\manager::phpunit_replace_observers($observers);
\core_tests\event\unittest_observer::reset();
$event1 = \core_tests\event\unittest_executed::create(array('courseid'=>1, 'context'=>\context_system::instance(), 'other'=>array('sample'=>1, 'xx'=>10)));
$event1->trigger();
$this->assertDebuggingCalled();
$event1 = \core_tests\event\unittest_executed::create(array('courseid'=>2, 'context'=>\context_system::instance(), 'other'=>array('sample'=>2, 'xx'=>10)));
$event1->trigger();
$this->assertDebuggingCalled();
$this->assertSame(
array('broken_observer-1', 'observe_one-1', 'broken_observer-2', 'observe_one-2'),
\core_tests\event\unittest_observer::$info);
}
public function test_external_buffer() {
global $DB;
$this->preventResetByRollback();
$observers = array(
array(
'eventname' => '\core_tests\event\unittest_executed',
'callback' => '\core_tests\event\unittest_observer::observe_one',
),
array(
'eventname' => '\core_tests\event\unittest_executed',
'callback' => '\core_tests\event\unittest_observer::external_observer',
'priority' => 200,
'internal' => 0,
),
);
\core\event\manager::phpunit_replace_observers($observers);
\core_tests\event\unittest_observer::reset();
$event1 = \core_tests\event\unittest_executed::create(array('courseid'=>1, 'context'=>\context_system::instance(), 'other'=>array('sample'=>1, 'xx'=>10)));
$event1->trigger();
$event2 = \core_tests\event\unittest_executed::create(array('courseid'=>2, 'context'=>\context_system::instance(), 'other'=>array('sample'=>2, 'xx'=>10)));
$event2->trigger();
$this->assertSame(
array('external_observer-1', 'observe_one-1', 'external_observer-2', 'observe_one-2'),
\core_tests\event\unittest_observer::$info);
\core\event\manager::phpunit_replace_observers($observers);
\core_tests\event\unittest_observer::reset();
$this->assertSame(array(), \core_tests\event\unittest_observer::$info);
$trans = $DB->start_delegated_transaction();
$event1 = \core_tests\event\unittest_executed::create(array('courseid'=>1, 'context'=>\context_system::instance(), 'other'=>array('sample'=>1, 'xx'=>10)));
$event1->trigger();
$event2 = \core_tests\event\unittest_executed::create(array('courseid'=>2, 'context'=>\context_system::instance(), 'other'=>array('sample'=>2, 'xx'=>10)));
$event2->trigger();
$this->assertSame(
array('observe_one-1', 'observe_one-2'),
\core_tests\event\unittest_observer::$info);
$trans->allow_commit();
$this->assertSame(
array('observe_one-1', 'observe_one-2', 'external_observer-1', 'external_observer-2'),
\core_tests\event\unittest_observer::$info);
\core\event\manager::phpunit_replace_observers($observers);
\core_tests\event\unittest_observer::reset();
$event1 = \core_tests\event\unittest_executed::create(array('courseid'=>1, 'context'=>\context_system::instance(), 'other'=>array('sample'=>1, 'xx'=>10)));
$event1->trigger();
$trans = $DB->start_delegated_transaction();
$event2 = \core_tests\event\unittest_executed::create(array('courseid'=>2, 'context'=>\context_system::instance(), 'other'=>array('sample'=>2, 'xx'=>10)));
$event2->trigger();
try {
$trans->rollback(new \moodle_exception('xxx'));
$this->fail('Expecting exception');
} catch (\moodle_exception $e) {
}
$this->assertSame(
array('external_observer-1', 'observe_one-1', 'observe_one-2'),
\core_tests\event\unittest_observer::$info);
}
public function test_legacy() {
global $DB;
$this->resetAfterTest(true);
$observers = array(
array(
'eventname' => '\core_tests\event\unittest_executed',
'callback' => '\core_tests\event\unittest_observer::observe_one',
),
array(
'eventname' => '*',
'callback' => '\core_tests\event\unittest_observer::observe_all',
'includefile' => null,
'internal' => 1,
'priority' => 9999,
),
);
$DB->delete_records('log', array());
events_update_definition('unittest');
$DB->delete_records_select('events_handlers', "component <> 'unittest'");
events_get_handlers('reset');
$this->assertEquals(3, $DB->count_records('events_handlers'));
set_config('loglifetime', 60*60*24*5);
\core\event\manager::phpunit_replace_observers($observers);
\core_tests\event\unittest_observer::reset();
$event1 = \core_tests\event\unittest_executed::create(array('courseid'=>1, 'context'=>\context_system::instance(), 'other'=>array('sample'=>5, 'xx'=>10)));
$event1->trigger();
$event2 = \core_tests\event\unittest_executed::create(array('courseid'=>2, 'context'=>\context_system::instance(), 'other'=>array('sample'=>6, 'xx'=>11)));
$event2->nest = true;
$event2->trigger();
$this->assertSame(
array('observe_all-1', 'observe_one-1', 'legacy_handler-1', 'observe_all-nesting-2', 'legacy_handler-3', 'observe_one-2', 'observe_all-3', 'observe_one-3', 'legacy_handler-2'),
\core_tests\event\unittest_observer::$info);
$this->assertSame($event1, \core_tests\event\unittest_observer::$event[0]);
$this->assertSame($event1, \core_tests\event\unittest_observer::$event[1]);
$this->assertSame(array(1, 5), \core_tests\event\unittest_observer::$event[2]);
$logs = $DB->get_records('log', array(), 'id ASC');
$this->assertCount(3, $logs);
$log = array_shift($logs);
$this->assertEquals(1, $log->course);
$this->assertSame('core_unittest', $log->module);
$this->assertSame('view', $log->action);
$log = array_shift($logs);
$this->assertEquals(2, $log->course);
$this->assertSame('core_unittest', $log->module);
$this->assertSame('view', $log->action);
$log = array_shift($logs);
$this->assertEquals(3, $log->course);
$this->assertSame('core_unittest', $log->module);
$this->assertSame('view', $log->action);
}
public function test_restore_event() {
$event1 = \core_tests\event\unittest_executed::create(array('courseid'=>1, 'context'=>\context_system::instance(), 'other'=>array('sample'=>1, 'xx'=>10)));
$data1 = $event1->get_data();
$event2 = \core\event\base::restore($data1, array('origin'=>'clid'));
$data2 = $event2->get_data();
$this->assertTrue($event2->is_triggered());
$this->assertTrue($event2->is_restored());
$this->assertEquals($data1, $data2);
$this->assertInstanceOf('core_tests\event\unittest_executed', $event2);
$this->assertEquals($event1->get_context(), $event2->get_context());
// Now test problematic data.
$data3 = $data1;
$data3['eventname'] = '\\a\\b\\c';
$event3 = \core\event\base::restore($data3, array());
$this->assertFalse($event3, 'Class name must match');
$data4 = $data1;
unset($data4['userid']);
$event4 = \core\event\base::restore($data4, array());
$this->assertInstanceOf('core_tests\event\unittest_executed', $event4);
$this->assertDebuggingCalled();
$data5 = $data1;
$data5['xx'] = 'xx';
$event5 = \core\event\base::restore($data5, array());
$this->assertInstanceOf('core_tests\event\unittest_executed', $event5);
$this->assertDebuggingCalled();
}
public function test_trigger_problems() {
$event = \core_tests\event\unittest_executed::create(array('courseid'=>1, 'context'=>\context_system::instance(), 'other'=>array('sample'=>5, 'xx'=>10)));
$event->trigger();
try {
$event->trigger();
$this->fail('Exception expected on double trigger');
} catch (Exception $e) {
$this->assertInstanceOf('coding_exception', $e);
}
$data = $event->get_data();
$restored = \core_tests\event\unittest_executed::restore($data, array());
$this->assertTrue($restored->is_triggered());
$this->assertTrue($restored->is_restored());
try {
$restored->trigger();
$this->fail('Exception expected on triggering of restored event');
} catch (\moodle_exception $e) {
$this->assertInstanceOf('coding_exception', $e);
}
$event = \core_tests\event\unittest_executed::create(array('courseid'=>1, 'context'=>\context_system::instance(), 'other'=>array('sample'=>5, 'xx'=>10)));
try {
\core\event\manager::dispatch($event);
$this->fail('Exception expected on manual event dispatching');
} catch (\moodle_exception $e) {
$this->assertInstanceOf('coding_exception', $e);
}
}
public function test_bad_events() {
try {
$event = \core_tests\event\unittest_executed::create(array('courseid'=>1, 'other'=>array('sample'=>5, 'xx'=>10)));
$this->fail('Exception expected when context and contextid missing');
} catch (Exception $e) {
$this->assertInstanceOf('coding_exception', $e);
}
$event = \core_tests\event\bad_event1::create(array('context'=>\context_system::instance()));
try {
$event->trigger();
$this->fail('Exception expected when $data not valid');
} catch (\moodle_exception $e) {
$this->assertInstanceOf('\coding_exception', $e);
}
$event = \core_tests\event\bad_event2::create(array('context'=>\context_system::instance()));
try {
$event->trigger();
$this->fail('Exception expected when $data not valid');
} catch (\moodle_exception $e) {
$this->assertInstanceOf('\coding_exception', $e);
}
$event = \core_tests\event\bad_event3::create(array('context'=>\context_system::instance()));
@$event->trigger();
$this->assertDebuggingCalled();
$event = \core_tests\event\bad_event4::create(array('context'=>\context_system::instance()));
@$event->trigger();
$this->assertDebuggingCalled();
$event = \core_tests\event\bad_event5::create(array('context'=>\context_system::instance()));
@$event->trigger();
$this->assertDebuggingCalled();
$event = \core_tests\event\bad_event6::create(array('context'=>\context_system::instance()));
$event->trigger();
$this->assertDebuggingCalled();
$event = \core_tests\event\bad_event7::create(array('objectid'=>1, 'context'=>\context_system::instance()));
try {
$event->trigger();
$this->fail('Exception expected when $data contains objectid by objecttable not specified');
} catch (\moodle_exception $e) {
$this->assertInstanceOf('\coding_exception', $e);
}
}
public function test_problematic_events() {
global $CFG;
$event1 = \core_tests\event\problematic_event1::create(array('context'=>\context_system::instance()));
$this->assertDebuggingNotCalled();
$this->assertNull($event1->xxx);
$this->assertDebuggingCalled();
$event2 = \core_tests\event\problematic_event1::create(array('xxx'=>0, 'context'=>\context_system::instance()));
$this->assertDebuggingCalled();
$CFG->debug = 0;
$event3 = \core_tests\event\problematic_event1::create(array('xxx'=>0, 'context'=>\context_system::instance()));
$this->assertDebuggingNotCalled();
$CFG->debug = E_ALL | E_STRICT;
$event4 = \core_tests\event\problematic_event1::create(array('context'=>\context_system::instance(), 'other'=>array('a'=>1)));
$event4->trigger();
$this->assertDebuggingNotCalled();
$event5 = \core_tests\event\problematic_event1::create(array('context'=>\context_system::instance(), 'other'=>(object)array('a'=>1)));
$this->assertDebuggingNotCalled();
$event5->trigger();
$this->assertDebuggingCalled();
$url = new moodle_url('/admin/');
$event6 = \core_tests\event\problematic_event1::create(array('context'=>\context_system::instance(), 'other'=>array('a'=>$url)));
$this->assertDebuggingNotCalled();
$event6->trigger();
$this->assertDebuggingCalled();
$event = \core_tests\event\problematic_event2::create(array());
$this->assertDebuggingNotCalled();
$event = \core_tests\event\problematic_event2::create(array('context'=>\context_system::instance()));
$this->assertDebuggingCalled();
$event = \core_tests\event\problematic_event3::create(array('other'=>1));
$this->assertDebuggingNotCalled();
$event = \core_tests\event\problematic_event3::create(array());
$this->assertDebuggingCalled();
}
public function test_record_snapshots() {
global $DB;
$event = \core_tests\event\unittest_executed::create(array('courseid'=>1, 'context'=>\context_system::instance(), 'other'=>array('sample'=>1, 'xx'=>10)));
$course1 = $DB->get_record('course', array('id'=>1));
$this->assertNotEmpty($course1);
$event->add_record_snapshot('course', $course1);
$result = $event->get_record_snapshot('course', 1, $course1);
$this->assertSame($course1, $result);
$user = $event->get_record_snapshot('user', 1);
$this->assertEquals(1, $user->id);
$this->assertSame('guest', $user->username);
$event->add_record_snapshot('course', $course1);
$event->trigger();
try {
$event->add_record_snapshot('course', $course1);
$this->fail('Updating of snapshots after trigger is not ok');;
} catch (\moodle_exception $e) {
$this->assertInstanceOf('\coding_exception', $e);
}
$event2 = \core_tests\event\unittest_executed::restore($event->get_data(), array());
try {
$event2->get_record_snapshot('course', 1, $course1);
$this->fail('Reading of snapshots from restored events is not ok');;
} catch (\moodle_exception $e) {
$this->assertInstanceOf('\coding_exception', $e);
}
}
public function test_iteration() {
$event = \core_tests\event\unittest_executed::create(array('courseid'=>1, 'context'=>\context_system::instance(), 'other'=>array('sample'=>1, 'xx'=>10)));
$data = array();
foreach ($event as $k => $v) {
$data[$k] = $v;
}
$this->assertSame($event->get_data(), $data);
}
}

View File

@ -27,7 +27,7 @@
defined('MOODLE_INTERNAL') || die();
class eventslib_testcase extends advanced_testcase {
class core_eventslib_testcase extends advanced_testcase {
/**
* Create temporary entries in the database for these tests.

186
lib/tests/fixtures/event_fixtures.php vendored Normal file
View File

@ -0,0 +1,186 @@
<?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/>.
namespace core_tests\event;
/**
* Fixtures for new event testing.
*
* @package core
* @category phpunit
* @copyright 2013 Petr Skoda {@link http://skodak.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
class unittest_executed extends \core\event\base {
public $nest = false;
public static function get_name() {
return 'xxx';
}
public function get_description() {
return 'yyy';
}
protected function init() {
$this->data['crud'] = 'u';
$this->data['level'] = 10;
}
public function get_url() {
return new moodle_url('/somepath/somefile.php', array('id'=>$this->data['other']['sample']));
}
protected function get_legacy_eventname() {
return 'test_legacy';
}
protected function get_legacy_eventdata() {
return array($this->data['courseid'], $this->data['other']['sample']);
}
protected function get_legacy_logdata() {
return array($this->data['courseid'], 'core_unittest', 'view', 'unittest.php?id='.$this->data['other']['sample']);
}
}
class unittest_observer {
public static $info = array();
public static $event = array();
public static function reset() {
self::$info = array();
self::$event = array();
}
public static function observe_one(unittest_executed $event) {
self::$info[] = 'observe_one-'.$event->courseid;
self::$event[] = $event;
}
public static function external_observer(\core\event\base $event) {
self::$info[] = 'external_observer-'.$event->courseid;
self::$event[] = $event;
}
public static function broken_observer(\core\event\base $event) {
self::$info[] = 'broken_observer-'.$event->courseid;
self::$event[] = $event;
throw new \Exception('someerror');
}
public static function observe_all(unittest_executed $event) {
self::$event[] = $event;
if ($event->nest) {
self::$info[] = 'observe_all-nesting-'.$event->courseid;
unittest_executed::create(array('courseid'=>3, 'context'=>\context_system::instance(), 'other'=>array('sample'=>666, 'xx'=>666)))->trigger();
} else {
self::$info[] = 'observe_all-'.$event->courseid;
}
}
public static function legacy_handler($data) {
self::$info[] = 'legacy_handler-'.$data[0];
self::$event[] = $data;
}
}
class bad_event1 extends \core\event\base {
protected function init() {
//$this->data['crud'] = 'u';
$this->data['level'] = 10;
}
}
class bad_event2 extends \core\event\base {
protected function init() {
$this->data['crud'] = 'u';
//$this->data['level'] = 10;
}
}
class bad_event3 extends \core\event\base {
protected function init() {
$this->data['crud'] = 'u';
$this->data['level'] = 10;
unset($this->data['courseid']);
}
}
class bad_event4 extends \core\event\base {
protected function init() {
$this->data['crud'] = 'u';
$this->data['level'] = 10;
$this->data['xxx'] = 1;
}
}
class bad_event5 extends \core\event\base {
protected function init() {
$this->data['crud'] = 'x';
$this->data['level'] = 10;
}
}
class bad_event6 extends \core\event\base {
protected function init() {
$this->data['crud'] = 'c';
$this->data['level'] = 10;
$this->data['objecttable'] = 'xxx_xxx_xx';
}
}
class bad_event7 extends \core\event\base {
protected function init() {
$this->data['crud'] = 'c';
$this->data['level'] = 10;
$this->data['objecttable'] = null;
}
}
class problematic_event1 extends \core\event\base {
protected function init() {
$this->data['crud'] = 'u';
$this->data['level'] = 10;
}
}
class problematic_event2 extends \core\event\base {
protected function init() {
$this->data['crud'] = 'c';
$this->data['level'] = 10;
$this->context = \context_system::instance();
}
}
class problematic_event3 extends \core\event\base {
protected function init() {
$this->data['crud'] = 'c';
$this->data['level'] = 10;
$this->context = \context_system::instance();
}
protected function validate_data() {
if (empty($this->data['other'])) {
debugging('other is missing');
}
}
}

View File

@ -37,6 +37,14 @@ $handlers = array (
'handlerfunction' => array('eventslib_sample_handler_class', 'static_method'),
'schedule' => 'cron',
'internal' => 1,
)
),
'test_legacy' => array (
'handlerfile' => '/lib/tests/event_test.php',
'handlerfunction' => '\core_tests\event\unittest_observer::legacy_handler',
'schedule' => 'instant',
'internal' => 1,
),
);