MDL-44720 modinfo: whitespace fix
[moodle.git] / lib / classes / event / base.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 namespace core\event;
19 defined('MOODLE_INTERNAL') || die();
21 /**
22  * Base event class.
23  *
24  * @package    core
25  * @copyright  2013 Petr Skoda {@link http://skodak.org}
26  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
27  */
29 /**
30  * All other event classes must extend this class.
31  *
32  * @package    core
33  * @copyright  2013 Petr Skoda {@link http://skodak.org}
34  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
35  *
36  * @property-read string $eventname Name of the event (=== class name with leading \)
37  * @property-read string $component Full frankenstyle component name
38  * @property-read string $action what happened
39  * @property-read string $target what/who was target of the action
40  * @property-read string $objecttable name of database table where is object record stored
41  * @property-read int $objectid optional id of the object
42  * @property-read string $crud letter indicating event type
43  * @property-read int $edulevel log level (one of the constants LEVEL_)
44  * @property-read int $contextid
45  * @property-read int $contextlevel
46  * @property-read int $contextinstanceid
47  * @property-read int $userid who did this?
48  * @property-read int $courseid
49  * @property-read int $relateduserid
50  * @property-read int $anonymous 1 means event should not be visible in reports, 0 means normal event,
51  *                    create() argument may be also true/false.
52  * @property-read mixed $other array or scalar, can not contain objects
53  * @property-read int $timecreated
54  */
55 abstract class base implements \IteratorAggregate {
57     /**
58      * Other level.
59      */
60     const LEVEL_OTHER = 0;
62     /**
63      * Teaching level.
64      *
65      * Any event that is performed by someone (typically a teacher) and has a teaching value,
66      * anything that is affecting the learning experience/environment of the students.
67      */
68     const LEVEL_TEACHING = 1;
70     /**
71      * Participating level.
72      *
73      * Any event that is performed by a user, and is related (or could be related) to his learning experience.
74      */
75     const LEVEL_PARTICIPATING = 2;
77     /** @var array event data */
78     protected $data;
80     /** @var array the format is standardised by logging API */
81     protected $logextra;
83     /** @var \context of this event */
84     protected $context;
86     /**
87      * @var bool indicates if event was already triggered,
88      *           this prevents second attempt to trigger event.
89      */
90     private $triggered;
92     /**
93      * @var bool indicates if event was already dispatched,
94      *           this prevents direct calling of manager::dispatch($event).
95      */
96     private $dispatched;
98     /**
99      * @var bool indicates if event was restored from storage,
100      *           this prevents triggering of restored events.
101      */
102     private $restored;
104     /** @var array list of event properties */
105     private static $fields = array(
106         'eventname', 'component', 'action', 'target', 'objecttable', 'objectid', 'crud', 'edulevel', 'contextid',
107         'contextlevel', 'contextinstanceid', 'userid', 'courseid', 'relateduserid', 'anonymous', 'other',
108         'timecreated');
110     /** @var array simple record cache */
111     private $recordsnapshots = array();
113     /**
114      * Private constructor, use create() or restore() methods instead.
115      */
116     private final function __construct() {
117         $this->data = array_fill_keys(self::$fields, null);
119         // Define some basic details.
120         $classname = get_called_class();
121         $parts = explode('\\', $classname);
122         if (count($parts) !== 3 or $parts[1] !== 'event') {
123             throw new \coding_exception("Invalid event class name '$classname', it must be defined in component\\event\\
124                     namespace");
125         }
126         $this->data['eventname'] = '\\'.$classname;
127         $this->data['component'] = $parts[0];
129         $pos = strrpos($parts[2], '_');
130         if ($pos === false) {
131             throw new \coding_exception("Invalid event class name '$classname', there must be at least one underscore separating
132                     object and action words");
133         }
134         $this->data['target'] = substr($parts[2], 0, $pos);
135         $this->data['action'] = substr($parts[2], $pos + 1);
136     }
138     /**
139      * Create new event.
140      *
141      * The optional data keys as:
142      * 1/ objectid - the id of the object specified in class name
143      * 2/ context - the context of this event
144      * 3/ other - the other data describing the event, can not contain objects
145      * 4/ relateduserid - the id of user which is somehow related to this event
146      *
147      * @param array $data
148      * @return \core\event\base returns instance of new event
149      *
150      * @throws \coding_exception
151      */
152     public static final function create(array $data = null) {
153         global $USER, $CFG;
155         $data = (array)$data;
157         /** @var \core\event\base $event */
158         $event = new static();
159         $event->triggered = false;
160         $event->restored = false;
161         $event->dispatched = false;
163         // By default all events are visible in logs.
164         $event->data['anonymous'] = 0;
166         // Set static event data specific for child class.
167         $event->init();
169         if (isset($event->data['level'])) {
170             if (!isset($event->data['edulevel'])) {
171                 debugging('level property is deprecated, use edulevel property instead', DEBUG_DEVELOPER);
172                 $event->data['edulevel'] = $event->data['level'];
173             }
174             unset($event->data['level']);
175         }
177         // Set automatic data.
178         $event->data['timecreated'] = time();
180         // Set optional data or use defaults.
181         $event->data['objectid'] = isset($data['objectid']) ? $data['objectid'] : null;
182         $event->data['courseid'] = isset($data['courseid']) ? $data['courseid'] : null;
183         $event->data['userid'] = isset($data['userid']) ? $data['userid'] : $USER->id;
184         $event->data['other'] = isset($data['other']) ? $data['other'] : null;
185         $event->data['relateduserid'] = isset($data['relateduserid']) ? $data['relateduserid'] : null;
186         if (isset($data['anonymous'])) {
187             $event->data['anonymous'] = $data['anonymous'];
188         }
189         $event->data['anonymous'] = (int)(bool)$event->data['anonymous'];
191         if (isset($event->context)) {
192             if (isset($data['context'])) {
193                 debugging('Context was already set in init() method, ignoring context parameter', DEBUG_DEVELOPER);
194             }
196         } else if (!empty($data['context'])) {
197             $event->context = $data['context'];
199         } else if (!empty($data['contextid'])) {
200             $event->context = \context::instance_by_id($data['contextid'], MUST_EXIST);
202         } else {
203             throw new \coding_exception('context (or contextid) is a required event property, system context may be hardcoded in init() method.');
204         }
206         $event->data['contextid'] = $event->context->id;
207         $event->data['contextlevel'] = $event->context->contextlevel;
208         $event->data['contextinstanceid'] = $event->context->instanceid;
210         if (!isset($event->data['courseid'])) {
211             if ($coursecontext = $event->context->get_course_context(false)) {
212                 $event->data['courseid'] = $coursecontext->instanceid;
213             } else {
214                 $event->data['courseid'] = 0;
215             }
216         }
218         if (!array_key_exists('relateduserid', $data) and $event->context->contextlevel == CONTEXT_USER) {
219             $event->data['relateduserid'] = $event->context->instanceid;
220         }
222         // Warn developers if they do something wrong.
223         if ($CFG->debugdeveloper) {
224             static $automatickeys = array('eventname', 'component', 'action', 'target', 'contextlevel', 'contextinstanceid', 'timecreated');
225             static $initkeys = array('crud', 'level', 'objecttable', 'edulevel');
227             foreach ($data as $key => $ignored) {
228                 if ($key === 'context') {
229                     continue;
231                 } else if (in_array($key, $automatickeys)) {
232                     debugging("Data key '$key' is not allowed in \\core\\event\\base::create() method, it is set automatically", DEBUG_DEVELOPER);
234                 } else if (in_array($key, $initkeys)) {
235                     debugging("Data key '$key' is not allowed in \\core\\event\\base::create() method, you need to set it in init() method", DEBUG_DEVELOPER);
237                 } else if (!in_array($key, self::$fields)) {
238                     debugging("Data key '$key' does not exist in \\core\\event\\base");
239                 }
240             }
241             $expectedcourseid = 0;
242             if ($coursecontext = $event->context->get_course_context(false)) {
243                 $expectedcourseid = $coursecontext->instanceid;
244             }
245             if ($expectedcourseid != $event->data['courseid']) {
246                 debugging("Inconsistent courseid - context combination detected.", DEBUG_DEVELOPER);
247             }
248         }
250         // Let developers validate their custom data (such as $this->data['other'], contextlevel, etc.).
251         $event->validate_data();
253         return $event;
254     }
256     /**
257      * Override in subclass.
258      *
259      * Set all required data properties:
260      *  1/ crud - letter [crud]
261      *  2/ edulevel - using a constant self::LEVEL_*.
262      *  3/ objecttable - name of database table if objectid specified
263      *
264      * Optionally it can set:
265      * a/ fixed system context
266      *
267      * @return void
268      */
269     protected abstract function init();
271     /**
272      * Let developers validate their custom data (such as $this->data['other'], contextlevel, etc.).
273      *
274      * Throw \coding_exception or debugging() notice in case of any problems.
275      */
276     protected function validate_data() {
277         // Override if you want to validate event properties when
278         // creating new events.
279     }
281     /**
282      * Returns localised general event name.
283      *
284      * Override in subclass, we can not make it static and abstract at the same time.
285      *
286      * @return string
287      */
288     public static function get_name() {
289         // Override in subclass with real lang string.
290         $parts = explode('\\', get_called_class());
291         if (count($parts) !== 3) {
292             return get_string('unknownevent', 'error');
293         }
294         return $parts[0].': '.str_replace('_', ' ', $parts[2]);
295     }
297     /**
298      * Returns non-localised event description with id's for admin use only.
299      *
300      * @return string
301      */
302     public function get_description() {
303         return null;
304     }
306     /**
307      * This method was originally intended for granular
308      * access control on the event level, unfortunately
309      * the proper implementation would be too expensive
310      * in many cases.
311      *
312      * @deprecated since 2.7
313      *
314      * @param int|\stdClass $user_or_id ID of the user.
315      * @return bool True if the user can view the event, false otherwise.
316      */
317     public function can_view($user_or_id = null) {
318         debugging('can_view() method is deprecated, use anonymous flag instead if necessary.', DEBUG_DEVELOPER);
319         return is_siteadmin($user_or_id);
320     }
322     /**
323      * Restore event from existing historic data.
324      *
325      * @param array $data
326      * @param array $logextra the format is standardised by logging API
327      * @return bool|\core\event\base
328      */
329     public static final function restore(array $data, array $logextra) {
330         $classname = $data['eventname'];
331         $component = $data['component'];
332         $action = $data['action'];
333         $target = $data['target'];
335         // Security: make 100% sure this really is an event class.
336         if ($classname !== "\\{$component}\\event\\{$target}_{$action}") {
337             return false;
338         }
340         if (!class_exists($classname)) {
341             return false;
342         }
343         $event = new $classname();
344         if (!($event instanceof \core\event\base)) {
345             return false;
346         }
348         $event->init(); // Init method of events could be setting custom properties.
349         $event->restored = true;
350         $event->triggered = true;
351         $event->dispatched = true;
352         $event->logextra = $logextra;
354         foreach (self::$fields as $key) {
355             if (!array_key_exists($key, $data)) {
356                 debugging("Event restore data must contain key $key");
357                 $data[$key] = null;
358             }
359         }
360         if (count($data) != count(self::$fields)) {
361             foreach ($data as $key => $value) {
362                 if (!in_array($key, self::$fields)) {
363                     debugging("Event restore data cannot contain key $key");
364                     unset($data[$key]);
365                 }
366             }
367         }
368         $event->data = $data;
370         return $event;
371     }
373     /**
374      * Create fake event from legacy log data.
375      *
376      * @param \stdClass $legacy
377      * @return base
378      */
379     public static final function restore_legacy($legacy) {
380         $classname = get_called_class();
381         /** @var base $event */
382         $event = new $classname();
383         $event->restored = true;
384         $event->triggered = true;
385         $event->dispatched = true;
387         $context = false;
388         $component = 'legacy';
389         if ($legacy->cmid) {
390             $context = \context_module::instance($legacy->cmid, IGNORE_MISSING);
391             $component = 'mod_'.$legacy->module;
392         } else if ($legacy->course) {
393             $context = \context_course::instance($legacy->course, IGNORE_MISSING);
394         }
395         if (!$context) {
396             $context = \context_system::instance();
397         }
399         $event->data = array();
401         $event->data['eventname'] = $legacy->module.'_'.$legacy->action;
402         $event->data['component'] = $component;
403         $event->data['action'] = $legacy->action;
404         $event->data['target'] = null;
405         $event->data['objecttable'] = null;
406         $event->data['objectid'] = null;
407         if (strpos($legacy->action, 'view') !== false) {
408             $event->data['crud'] = 'r';
409         } else if (strpos($legacy->action, 'print') !== false) {
410             $event->data['crud'] = 'r';
411         } else if (strpos($legacy->action, 'update') !== false) {
412             $event->data['crud'] = 'u';
413         } else if (strpos($legacy->action, 'hide') !== false) {
414             $event->data['crud'] = 'u';
415         } else if (strpos($legacy->action, 'move') !== false) {
416             $event->data['crud'] = 'u';
417         } else if (strpos($legacy->action, 'write') !== false) {
418             $event->data['crud'] = 'u';
419         } else if (strpos($legacy->action, 'tag') !== false) {
420             $event->data['crud'] = 'u';
421         } else if (strpos($legacy->action, 'remove') !== false) {
422             $event->data['crud'] = 'u';
423         } else if (strpos($legacy->action, 'delete') !== false) {
424             $event->data['crud'] = 'p';
425         } else if (strpos($legacy->action, 'create') !== false) {
426             $event->data['crud'] = 'c';
427         } else if (strpos($legacy->action, 'post') !== false) {
428             $event->data['crud'] = 'c';
429         } else if (strpos($legacy->action, 'add') !== false) {
430             $event->data['crud'] = 'c';
431         } else {
432             // End of guessing...
433             $event->data['crud'] = 'r';
434         }
435         $event->data['edulevel'] = $event::LEVEL_OTHER;
436         $event->data['contextid'] = $context->id;
437         $event->data['contextlevel'] = $context->contextlevel;
438         $event->data['contextinstanceid'] = $context->instanceid;
439         $event->data['userid'] = ($legacy->userid ? $legacy->userid : null);
440         $event->data['courseid'] = ($legacy->course ? $legacy->course : null);
441         $event->data['relateduserid'] = ($legacy->userid ? $legacy->userid : null);
442         $event->data['timecreated'] = $legacy->time;
444         $event->logextra = array();
445         if ($legacy->ip) {
446             $event->logextra['origin'] = 'web';
447             $event->logextra['ip'] = $legacy->ip;
448         } else {
449             $event->logextra['origin'] = 'cli';
450             $event->logextra['ip'] = null;
451         }
452         $event->logextra['realuserid'] = null;
454         $event->data['other'] = (array)$legacy;
456         return $event;
457     }
459     /**
460      * Returns event context.
461      * @return \context
462      */
463     public function get_context() {
464         if (isset($this->context)) {
465             return $this->context;
466         }
467         $this->context = \context::instance_by_id($this->data['contextid'], IGNORE_MISSING);
468         return $this->context;
469     }
471     /**
472      * Returns relevant URL, override in subclasses.
473      * @return \moodle_url
474      */
475     public function get_url() {
476         return null;
477     }
479     /**
480      * Return standardised event data as array.
481      *
482      * @return array All elements are scalars except the 'other' field which is array.
483      */
484     public function get_data() {
485         return $this->data;
486     }
488     /**
489      * Return auxiliary data that was stored in logs.
490      *
491      * List of standard properties:
492      *  - origin: IP number, cli,cron
493      *  - realuserid: id of the user when logged-in-as
494      *
495      * @return array the format is standardised by logging API
496      */
497     public function get_logextra() {
498         return $this->logextra;
499     }
501     /**
502      * Does this event replace legacy event?
503      *
504      * Note: do not use directly!
505      *
506      * @return null|string legacy event name
507      */
508     public static function get_legacy_eventname() {
509         return null;
510     }
512     /**
513      * Legacy event data if get_legacy_eventname() is not empty.
514      *
515      * Note: do not use directly!
516      *
517      * @return mixed
518      */
519     protected function get_legacy_eventdata() {
520         return null;
521     }
523     /**
524      * Doest this event replace add_to_log() statement?
525      *
526      * Note: do not use directly!
527      *
528      * @return null|array of parameters to be passed to legacy add_to_log() function.
529      */
530     protected function get_legacy_logdata() {
531         return null;
532     }
534     /**
535      * Validate all properties right before triggering the event.
536      *
537      * This throws coding exceptions for fatal problems and debugging for minor problems.
538      *
539      * @throws \coding_exception
540      */
541     protected final function validate_before_trigger() {
542         global $DB, $CFG;
544         if (empty($this->data['crud'])) {
545             throw new \coding_exception('crud must be specified in init() method of each method');
546         }
547         if (!isset($this->data['edulevel'])) {
548             throw new \coding_exception('edulevel must be specified in init() method of each method');
549         }
550         if (!empty($this->data['objectid']) and empty($this->data['objecttable'])) {
551             throw new \coding_exception('objecttable must be specified in init() method if objectid present');
552         }
554         if ($CFG->debugdeveloper) {
555             // Ideally these should be coding exceptions, but we need to skip these for performance reasons
556             // on production servers.
558             if (!in_array($this->data['crud'], array('c', 'r', 'u', 'd'), true)) {
559                 debugging("Invalid event crud value specified.", DEBUG_DEVELOPER);
560             }
561             if (!in_array($this->data['edulevel'], array(self::LEVEL_OTHER, self::LEVEL_TEACHING, self::LEVEL_PARTICIPATING))) {
562                 // Bitwise combination of levels is not allowed at this stage.
563                 debugging('Event property edulevel must a constant value, see event_base::LEVEL_*', DEBUG_DEVELOPER);
564             }
565             if (self::$fields !== array_keys($this->data)) {
566                 debugging('Number of event data fields must not be changed in event classes', DEBUG_DEVELOPER);
567             }
568             $encoded = json_encode($this->data['other']);
569             // The comparison here is not set to strict as whole float numbers will be converted to integers through JSON encoding /
570             // decoding and send an unwanted debugging message.
571             if ($encoded === false or $this->data['other'] != json_decode($encoded, true)) {
572                 debugging('other event data must be compatible with json encoding', DEBUG_DEVELOPER);
573             }
574             if ($this->data['userid'] and !is_number($this->data['userid'])) {
575                 debugging('Event property userid must be a number', DEBUG_DEVELOPER);
576             }
577             if ($this->data['courseid'] and !is_number($this->data['courseid'])) {
578                 debugging('Event property courseid must be a number', DEBUG_DEVELOPER);
579             }
580             if ($this->data['objectid'] and !is_number($this->data['objectid'])) {
581                 debugging('Event property objectid must be a number', DEBUG_DEVELOPER);
582             }
583             if ($this->data['relateduserid'] and !is_number($this->data['relateduserid'])) {
584                 debugging('Event property relateduserid must be a number', DEBUG_DEVELOPER);
585             }
586             if ($this->data['objecttable']) {
587                 if (!$DB->get_manager()->table_exists($this->data['objecttable'])) {
588                     debugging('Unknown table specified in objecttable field', DEBUG_DEVELOPER);
589                 }
590                 if (!isset($this->data['objectid'])) {
591                     debugging('Event property objectid must be set when objecttable is defined', DEBUG_DEVELOPER);
592                 }
593             }
594         }
595     }
597     /**
598      * Trigger event.
599      */
600     public final function trigger() {
601         global $CFG;
603         if ($this->restored) {
604             throw new \coding_exception('Can not trigger restored event');
605         }
606         if ($this->triggered or $this->dispatched) {
607             throw new \coding_exception('Can not trigger event twice');
608         }
610         $this->validate_before_trigger();
612         $this->triggered = true;
614         if (isset($CFG->loglifetime) and $CFG->loglifetime != -1) {
615             if ($data = $this->get_legacy_logdata()) {
616                 $manager = get_log_manager();
617                 if (method_exists($manager, 'legacy_add_to_log')) {
618                     call_user_func_array(array($manager, 'legacy_add_to_log'), $data);
619                 }
620             }
621         }
623         if (PHPUNIT_TEST and \phpunit_util::is_redirecting_events()) {
624             $this->dispatched = true;
625             \phpunit_util::event_triggered($this);
626             return;
627         }
629         \core\event\manager::dispatch($this);
631         $this->dispatched = true;
633         if ($legacyeventname = static::get_legacy_eventname()) {
634             events_trigger_legacy($legacyeventname, $this->get_legacy_eventdata());
635         }
636     }
638     /**
639      * Was this event already triggered?
640      *
641      * @return bool
642      */
643     public final function is_triggered() {
644         return $this->triggered;
645     }
647     /**
648      * Used from event manager to prevent direct access.
649      *
650      * @return bool
651      */
652     public final function is_dispatched() {
653         return $this->dispatched;
654     }
656     /**
657      * Was this event restored?
658      *
659      * @return bool
660      */
661     public final function is_restored() {
662         return $this->restored;
663     }
665     /**
666      * Add cached data that will be most probably used in event observers.
667      *
668      * This is used to improve performance, but it is required for data
669      * that was just deleted.
670      *
671      * @param string $tablename
672      * @param \stdClass $record
673      *
674      * @throws \coding_exception if used after ::trigger()
675      */
676     public final function add_record_snapshot($tablename, $record) {
677         global $DB, $CFG;
679         if ($this->triggered) {
680             throw new \coding_exception('It is not possible to add snapshots after triggering of events');
681         }
683         // Special case for course module, allow instance of cm_info to be passed instead of stdClass.
684         if ($tablename === 'course_modules' && $record instanceof \cm_info) {
685             $record = $record->get_course_module_record();
686         }
688         // NOTE: this might use some kind of MUC cache,
689         //       hopefully we will not run out of memory here...
690         if ($CFG->debugdeveloper) {
691             if (!($record instanceof \stdClass)) {
692                 debugging('Argument $record must be an instance of stdClass.', DEBUG_DEVELOPER);
693             }
694             if (!$DB->get_manager()->table_exists($tablename)) {
695                 debugging("Invalid table name '$tablename' specified, database table does not exist.", DEBUG_DEVELOPER);
696             } else {
697                 $columns = $DB->get_columns($tablename);
698                 $missingfields = array_diff(array_keys($columns), array_keys((array)$record));
699                 if (!empty($missingfields)) {
700                     debugging("Fields list in snapshot record does not match fields list in '$tablename'. Record is missing fields: ".
701                             join(', ', $missingfields), DEBUG_DEVELOPER);
702                 }
703             }
704         }
705         $this->recordsnapshots[$tablename][$record->id] = $record;
706     }
708     /**
709      * Returns cached record or fetches data from database if not cached.
710      *
711      * @param string $tablename
712      * @param int $id
713      * @return \stdClass
714      *
715      * @throws \coding_exception if used after ::restore()
716      */
717     public final function get_record_snapshot($tablename, $id) {
718         global $DB;
720         if ($this->restored) {
721             throw new \coding_exception('It is not possible to get snapshots from restored events');
722         }
724         if (isset($this->recordsnapshots[$tablename][$id])) {
725             return clone($this->recordsnapshots[$tablename][$id]);
726         }
728         $record = $DB->get_record($tablename, array('id'=>$id));
729         $this->recordsnapshots[$tablename][$id] = $record;
731         return $record;
732     }
734     /**
735      * Magic getter for read only access.
736      *
737      * @param string $name
738      * @return mixed
739      */
740     public function __get($name) {
741         if ($name === 'level') {
742             debugging('level property is deprecated, use edulevel property instead', DEBUG_DEVELOPER);
743             return $this->data['edulevel'];
744         }
745         if (array_key_exists($name, $this->data)) {
746             return $this->data[$name];
747         }
749         debugging("Accessing non-existent event property '$name'");
750     }
752     /**
753      * Magic setter.
754      *
755      * Note: we must not allow modification of data from outside,
756      *       after trigger() the data MUST NOT CHANGE!!!
757      *
758      * @param string $name
759      * @param mixed $value
760      *
761      * @throws \coding_exception
762      */
763     public function __set($name, $value) {
764         throw new \coding_exception('Event properties must not be modified.');
765     }
767     /**
768      * Is data property set?
769      *
770      * @param string $name
771      * @return bool
772      */
773     public function __isset($name) {
774         if ($name === 'level') {
775             debugging('level property is deprecated, use edulevel property instead', DEBUG_DEVELOPER);
776             return isset($this->data['edulevel']);
777         }
778         return isset($this->data[$name]);
779     }
781     /**
782      * Create an iterator because magic vars can't be seen by 'foreach'.
783      *
784      * @return \ArrayIterator
785      */
786     public function getIterator() {
787         return new \ArrayIterator($this->data);
788     }