MDL-30985 Fixed up event API phpdocs
[moodle.git] / lib / eventslib.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 /**
18  * Library of functions for events manipulation.
19  *
20  * The public API is all at the end of this file.
21  *
22  * @package core_event
23  * @copyright 1999 onwards Martin Dougiamas  {@link http://moodle.com}
24  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25  */
27 defined('MOODLE_INTERNAL') || die();
29 /**
30  * Loads the events definitions for the component (from file). If no
31  * events are defined for the component, we simply return an empty array.
32  *
33  * @access protected INTERNAL - to be used from eventslib only
34  *
35  * @param string $component examples: 'moodle', 'mod_forum', 'block_quiz_results'
36  * @return array of capabilities or empty array if not exists
37  */
38 function events_load_def($component) {
39     global $CFG;
40     if ($component === 'unittest') {
41         $defpath = $CFG->dirroot.'/lib/simpletest/fixtures/events.php';
42     } else {
43         $defpath = get_component_directory($component).'/db/events.php';
44     }
46     $handlers = array();
48     if (file_exists($defpath)) {
49         require($defpath);
50     }
52     // make sure the definitions are valid and complete; tell devs what is wrong
53     foreach ($handlers as $eventname => $handler) {
54         if ($eventname === 'reset') {
55             debugging("'reset' can not be used as event name.");
56             unset($handlers['reset']);
57             continue;
58         }
59         if (!is_array($handler)) {
60             debugging("Handler of '$eventname' must be specified as array'");
61             unset($handlers[$eventname]);
62             continue;
63         }
64         if (!isset($handler['handlerfile'])) {
65             debugging("Handler of '$eventname' must include 'handlerfile' key'");
66             unset($handlers[$eventname]);
67             continue;
68         }
69         if (!isset($handler['handlerfunction'])) {
70             debugging("Handler of '$eventname' must include 'handlerfunction' key'");
71             unset($handlers[$eventname]);
72             continue;
73         }
74         if (!isset($handler['schedule'])) {
75             $handler['schedule'] = 'instant';
76         }
77         if ($handler['schedule'] !== 'instant' and $handler['schedule'] !== 'cron') {
78             debugging("Handler of '$eventname' must include valid 'schedule' type (instant or cron)'");
79             unset($handlers[$eventname]);
80             continue;
81         }
82         if (!isset($handler['internal'])) {
83             $handler['internal'] = 1;
84         }
85         $handlers[$eventname] = $handler;
86     }
88     return $handlers;
89 }
91 /**
92  * Gets the capabilities that have been cached in the database for this
93  * component.
94  *
95  * @access protected INTERNAL - to be used from eventslib only
96  *
97  * @param string $component examples: 'moodle', 'mod_forum', 'block_quiz_results'
98  * @return array of events
99  */
100 function events_get_cached($component) {
101     global $DB;
103     $cachedhandlers = array();
105     if ($storedhandlers = $DB->get_records('events_handlers', array('component'=>$component))) {
106         foreach ($storedhandlers as $handler) {
107             $cachedhandlers[$handler->eventname] = array (
108                 'id'              => $handler->id,
109                 'handlerfile'     => $handler->handlerfile,
110                 'handlerfunction' => $handler->handlerfunction,
111                 'schedule'        => $handler->schedule,
112                 'internal'        => $handler->internal);
113         }
114     }
116     return $cachedhandlers;
119 /**
120  * We can not removed all event handlers in table, then add them again
121  * because event handlers could be referenced by queued items
122  *
123  * Note that the absence of the db/events.php event definition file
124  * will cause any queued events for the component to be removed from
125  * the database.
126  *
127  * @category event
128  *
129  * @param string $component examples: 'moodle', 'mod_forum', 'block_quiz_results'
130  * @return boolean always returns true
131  */
132 function events_update_definition($component='moodle') {
133     global $DB;
135     // load event definition from events.php
136     $filehandlers = events_load_def($component);
138     // load event definitions from db tables
139     // if we detect an event being already stored, we discard from this array later
140     // the remaining needs to be removed
141     $cachedhandlers = events_get_cached($component);
143     foreach ($filehandlers as $eventname => $filehandler) {
144         if (!empty($cachedhandlers[$eventname])) {
145             if ($cachedhandlers[$eventname]['handlerfile'] === $filehandler['handlerfile'] &&
146                 $cachedhandlers[$eventname]['handlerfunction'] === serialize($filehandler['handlerfunction']) &&
147                 $cachedhandlers[$eventname]['schedule'] === $filehandler['schedule'] &&
148                 $cachedhandlers[$eventname]['internal'] == $filehandler['internal']) {
149                 // exact same event handler already present in db, ignore this entry
151                 unset($cachedhandlers[$eventname]);
152                 continue;
154             } else {
155                 // same event name matches, this event has been updated, update the datebase
156                 $handler = new stdClass();
157                 $handler->id              = $cachedhandlers[$eventname]['id'];
158                 $handler->handlerfile     = $filehandler['handlerfile'];
159                 $handler->handlerfunction = serialize($filehandler['handlerfunction']); // static class methods stored as array
160                 $handler->schedule        = $filehandler['schedule'];
161                 $handler->internal        = $filehandler['internal'];
163                 $DB->update_record('events_handlers', $handler);
165                 unset($cachedhandlers[$eventname]);
166                 continue;
167             }
169         } else {
170             // if we are here, this event handler is not present in db (new)
171             // add it
172             $handler = new stdClass();
173             $handler->eventname       = $eventname;
174             $handler->component       = $component;
175             $handler->handlerfile     = $filehandler['handlerfile'];
176             $handler->handlerfunction = serialize($filehandler['handlerfunction']); // static class methods stored as array
177             $handler->schedule        = $filehandler['schedule'];
178             $handler->status          = 0;
179             $handler->internal        = $filehandler['internal'];
181             $DB->insert_record('events_handlers', $handler);
182         }
183     }
185     // clean up the left overs, the entries in cached events array at this points are deprecated event handlers
186     // and should be removed, delete from db
187     events_cleanup($component, $cachedhandlers);
189     events_get_handlers('reset');
191     return true;
194 /**
195  * Remove all event handlers and queued events
196  *
197  * @category event
198  * @param string $component examples: 'moodle', 'mod_forum', 'block_quiz_results'
199  */
200 function events_uninstall($component) {
201     $cachedhandlers = events_get_cached($component);
202     events_cleanup($component, $cachedhandlers);
204     events_get_handlers('reset');
207 /**
208  * Deletes cached events that are no longer needed by the component.
209  *
210  * @access protected INTERNAL - to be used from eventslib only
211  *
212  * @param string $component examples: 'moodle', 'mod_forum', 'block_quiz_results'
213  * @param array $cachedhandlers array of the cached events definitions that will be
214  * @return int number of unused handlers that have been removed
215  */
216 function events_cleanup($component, $cachedhandlers) {
217     global $DB;
219     $deletecount = 0;
220     foreach ($cachedhandlers as $eventname => $cachedhandler) {
221         if ($qhandlers = $DB->get_records('events_queue_handlers', array('handlerid'=>$cachedhandler['id']))) {
222             //debugging("Removing pending events from queue before deleting of event handler: $component - $eventname");
223             foreach ($qhandlers as $qhandler) {
224                 events_dequeue($qhandler);
225             }
226         }
227         $DB->delete_records('events_handlers', array('eventname'=>$eventname, 'component'=>$component));
228         $deletecount++;
229     }
231     return $deletecount;
234 /****************** End of Events handler Definition code *******************/
236 /**
237  * Puts a handler on queue
238  *
239  * @access protected INTERNAL - to be used from eventslib only
240  *
241  * @param stdClass $handler event handler object from db
242  * @param stdClass $event event data object
243  * @param string $errormessage The error message indicating the problem
244  * @return int id number of new queue handler
245  */
246 function events_queue_handler($handler, $event, $errormessage) {
247     global $DB;
249     if ($qhandler = $DB->get_record('events_queue_handlers', array('queuedeventid'=>$event->id, 'handlerid'=>$handler->id))) {
250         debugging("Please check code: Event id $event->id is already queued in handler id $qhandler->id");
251         return $qhandler->id;
252     }
254     // make a new queue handler
255     $qhandler = new stdClass();
256     $qhandler->queuedeventid  = $event->id;
257     $qhandler->handlerid      = $handler->id;
258     $qhandler->errormessage   = $errormessage;
259     $qhandler->timemodified   = time();
260     if ($handler->schedule === 'instant' and $handler->status == 1) {
261         $qhandler->status     = 1; //already one failed attempt to dispatch this event
262     } else {
263         $qhandler->status     = 0;
264     }
266     return $DB->insert_record('events_queue_handlers', $qhandler);
269 /**
270  * trigger a single event with a specified handler
271  *
272  * @access protected INTERNAL - to be used from eventslib only
273  *
274  * @param stdClass $hander Row from db
275  * @param stdClass $eventdata dataobject
276  * @param string $errormessage error message indicating problem
277  * @return bool|null True means event processed, false means retry event later; may throw exception, NULL means internal error
278  */
279 function events_dispatch($handler, $eventdata, &$errormessage) {
280     global $CFG;
282     $function = unserialize($handler->handlerfunction);
284     if (is_callable($function)) {
285         // oki, no need for includes
287     } else if (file_exists($CFG->dirroot.$handler->handlerfile)) {
288         include_once($CFG->dirroot.$handler->handlerfile);
290     } else {
291         $errormessage = "Handler file of component $handler->component: $handler->handlerfile can not be found!";
292         return null;
293     }
295     // checks for handler validity
296     if (is_callable($function)) {
297         $result = call_user_func($function, $eventdata);
298         if ($result === false) {
299             $errormessage = "Handler function of component $handler->component: $handler->handlerfunction requested resending of event!";
300             return false;
301         }
302         return true;
304     } else {
305         $errormessage = "Handler function of component $handler->component: $handler->handlerfunction not callable function or class method!";
306         return null;
307     }
310 /**
311  * given a queued handler, call the respective event handler to process the event
312  *
313  * @access protected INTERNAL - to be used from eventslib only
314  *
315  * @param stdClass $qhandler events_queued_handler row from db
316  * @return boolean true means event processed, false means retry later, NULL means fatal failure
317  */
318 function events_process_queued_handler($qhandler) {
319     global $DB;
321     // get handler
322     if (!$handler = $DB->get_record('events_handlers', array('id'=>$qhandler->handlerid))) {
323         debugging("Error processing queue handler $qhandler->id, missing handler id: $qhandler->handlerid");
324         //irrecoverable error, remove broken queue handler
325         events_dequeue($qhandler);
326         return NULL;
327     }
329     // get event object
330     if (!$event = $DB->get_record('events_queue', array('id'=>$qhandler->queuedeventid))) {
331         // can't proceed with no event object - might happen when two crons running at the same time
332         debugging("Error processing queue handler $qhandler->id, missing event id: $qhandler->queuedeventid");
333         //irrecoverable error, remove broken queue handler
334         events_dequeue($qhandler);
335         return NULL;
336     }
338     // call the function specified by the handler
339     try {
340         $errormessage = 'Unknown error';
341         if (events_dispatch($handler, unserialize(base64_decode($event->eventdata)), $errormessage)) {
342             //everything ok
343             events_dequeue($qhandler);
344             return true;
345         }
346     } catch (Exception $e) {
347         // the problem here is that we do not want one broken handler to stop all others,
348         // cron handlers are very tricky because the needed data might have been deleted before the cron execution
349         $errormessage = "Handler function of component $handler->component: $handler->handlerfunction threw exception :" .
350                 $e->getMessage() . "\n" . format_backtrace($e->getTrace(), true);
351         if (!empty($e->debuginfo)) {
352             $errormessage .= $e->debuginfo;
353         }
354     }
356     //dispatching failed
357     $qh = new stdClass();
358     $qh->id           = $qhandler->id;
359     $qh->errormessage = $errormessage;
360     $qh->timemodified = time();
361     $qh->status       = $qhandler->status + 1;
362     $DB->update_record('events_queue_handlers', $qh);
364     return false;
367 /**
368  * Removes this queued handler from the events_queued_handler table
369  *
370  * Removes events_queue record from events_queue if no more references to this event object exists
371  *
372  * @access protected INTERNAL - to be used from eventslib only
373  *
374  * @param stdClass $qhandler events_queued_handler row from db
375  */
376 function events_dequeue($qhandler) {
377     global $DB;
379     // first delete the queue handler
380     $DB->delete_records('events_queue_handlers', array('id'=>$qhandler->id));
382     // if no more queued handler is pointing to the same event - delete the event too
383     if (!$DB->record_exists('events_queue_handlers', array('queuedeventid'=>$qhandler->queuedeventid))) {
384         $DB->delete_records('events_queue', array('id'=>$qhandler->queuedeventid));
385     }
388 /**
389  * Returns handlers for given event. Uses caching for better perf.
390  *
391  * @access protected INTERNAL - to be used from eventslib only
392  *
393  * @staticvar array $handlers
394  * @param string $eventanme name of even or 'reset'
395  * @return array|false array of handlers or false otherwise
396  */
397 function events_get_handlers($eventname) {
398     global $DB;
399     static $handlers = array();
401     if ($eventname === 'reset') {
402         $handlers = array();
403         return false;
404     }
406     if (!array_key_exists($eventname, $handlers)) {
407         $handlers[$eventname] = $DB->get_records('events_handlers', array('eventname'=>$eventname));
408     }
410     return $handlers[$eventname];
413 /****** Public events API starts here, do not use functions above in 3rd party code ******/
416 /**
417  * Events cron will try to empty the events queue by processing all the queued events handlers
418  *
419  * @access public Part of the public API
420  * @category event
421  *
422  * @param string $eventname empty means all
423  * @return int number of dispatched events
424  */
425 function events_cron($eventname='') {
426     global $DB;
428     $failed = array();
429     $processed = 0;
431     if ($eventname) {
432         $sql = "SELECT qh.*
433                   FROM {events_queue_handlers} qh, {events_handlers} h
434                  WHERE qh.handlerid = h.id AND h.eventname=?
435               ORDER BY qh.id";
436         $params = array($eventname);
437     } else {
438         $sql = "SELECT *
439                   FROM {events_queue_handlers}
440               ORDER BY id";
441         $params = array();
442     }
444     $rs = $DB->get_recordset_sql($sql, $params);
445     foreach ($rs as $qhandler) {
446         if (isset($failed[$qhandler->handlerid])) {
447             // do not try to dispatch any later events when one already asked for retry or ended with exception
448             continue;
449         }
450         $status = events_process_queued_handler($qhandler);
451         if ($status === false) {
452             // handler is asking for retry, do not send other events to this handler now
453             $failed[$qhandler->handlerid] = $qhandler->handlerid;
454         } else if ($status === NULL) {
455             // means completely broken handler, event data was purged
456             $failed[$qhandler->handlerid] = $qhandler->handlerid;
457         } else {
458             $processed++;
459         }
460     }
461     $rs->close();
463     // remove events that do not have any handlers waiting
464     $sql = "SELECT eq.id
465               FROM {events_queue} eq
466               LEFT JOIN {events_queue_handlers} qh ON qh.queuedeventid = eq.id
467              WHERE qh.id IS NULL";
468     $rs = $DB->get_recordset_sql($sql);
469     foreach ($rs as $event) {
470         //debugging('Purging stale event '.$event->id);
471         $DB->delete_records('events_queue', array('id'=>$event->id));
472     }
473     $rs->close();
475     return $processed;
479 /**
480  * Function to call all event handlers when triggering an event
481  *
482  * @access public Part of the public API.
483  * @category event
484  *
485  * @param string $eventname name of the event
486  * @param object $eventdata event data object
487  * @return int number of failed events
488  */
489 function events_trigger($eventname, $eventdata) {
490     global $CFG, $USER, $DB;
492     $failedcount = 0; // number of failed events.
494     // pull out all registered event handlers
495     if ($handlers = events_get_handlers($eventname)) {
496         foreach ($handlers as $handler) {
497             $errormessage = '';
499             if ($handler->schedule === 'instant') {
500                 if ($handler->status) {
501                     //check if previous pending events processed
502                     if (!$DB->record_exists('events_queue_handlers', array('handlerid'=>$handler->id))) {
503                         // ok, queue is empty, lets reset the status back to 0 == ok
504                         $handler->status = 0;
505                         $DB->set_field('events_handlers', 'status', 0, array('id'=>$handler->id));
506                         // reset static handler cache
507                         events_get_handlers('reset');
508                     }
509                 }
511                 // dispatch the event only if instant schedule and status ok
512                 if ($handler->status or (!$handler->internal and $DB->is_transaction_started())) {
513                     // increment the error status counter
514                     $handler->status++;
515                     $DB->set_field('events_handlers', 'status', $handler->status, array('id'=>$handler->id));
516                     // reset static handler cache
517                     events_get_handlers('reset');
519                 } else {
520                     $errormessage = 'Unknown error';;
521                     $result = events_dispatch($handler, $eventdata, $errormessage);
522                     if ($result === true) {
523                         // everything is fine - event dispatched
524                         continue;
525                     } else if ($result === false) {
526                         // retry later - set error count to 1 == send next instant into cron queue
527                         $DB->set_field('events_handlers', 'status', 1, array('id'=>$handler->id));
528                         // reset static handler cache
529                         events_get_handlers('reset');
530                     } else {
531                         // internal problem - ignore the event completely
532                         $failedcount ++;
533                         continue;
534                     }
535                 }
537                 // update the failed counter
538                 $failedcount ++;
540             } else if ($handler->schedule === 'cron') {
541                 //ok - use queueing of events only
543             } else {
544                 // unknown schedule - ignore event completely
545                 debugging("Unknown handler schedule type: $handler->schedule");
546                 $failedcount ++;
547                 continue;
548             }
550             // if even type is not instant, or dispatch asked for retry, queue it
551             $event = new stdClass();
552             $event->userid      = $USER->id;
553             $event->eventdata   = base64_encode(serialize($eventdata));
554             $event->timecreated = time();
555             if (debugging()) {
556                 $dump = '';
557                 $callers = debug_backtrace();
558                 foreach ($callers as $caller) {
559                     if (!isset($caller['line'])) {
560                         $caller['line'] = '?';
561                     }
562                     if (!isset($caller['file'])) {
563                         $caller['file'] = '?';
564                     }
565                     $dump .= 'line ' . $caller['line'] . ' of ' . substr($caller['file'], strlen($CFG->dirroot) + 1);
566                     if (isset($caller['function'])) {
567                         $dump .= ': call to ';
568                         if (isset($caller['class'])) {
569                             $dump .= $caller['class'] . $caller['type'];
570                         }
571                         $dump .= $caller['function'] . '()';
572                     }
573                     $dump .= "\n";
574                 }
575                 $event->stackdump = $dump;
576             } else {
577                 $event->stackdump = '';
578             }
579             $event->id = $DB->insert_record('events_queue', $event);
580             events_queue_handler($handler, $event, $errormessage);
581         }
582     } else {
583         // No handler found for this event name - this is ok!
584     }
586     return $failedcount;
589 /**
590  * checks if an event is registered for this component
591  *
592  * @access public Part of the public API
593  *
594  * @param string $eventname name of the event
595  * @param string $component component name, can be mod/data or moodle
596  * @return bool
597  */
598 function events_is_registered($eventname, $component) {
599     global $DB;
600     return $DB->record_exists('events_handlers', array('component'=>$component, 'eventname'=>$eventname));
603 /**
604  * checks if an event is queued for processing - either cron handlers attached or failed instant handlers
605  *
606  * @access public Part of the public API
607  *
608  * @param string $eventname name of the event
609  * @return int number of queued events
610  */
611 function events_pending_count($eventname) {
612     global $DB;
614     $sql = "SELECT COUNT('x')
615               FROM {events_queue_handlers} qh
616               JOIN {events_handlers} h ON h.id = qh.handlerid
617              WHERE h.eventname = ?";
619     return $DB->count_records_sql($sql, array($eventname));