diff --git a/analytics/classes/analysable.php b/analytics/classes/analysable.php index 97faf596e8e..e9dcaae46de 100644 --- a/analytics/classes/analysable.php +++ b/analytics/classes/analysable.php @@ -29,6 +29,10 @@ defined('MOODLE_INTERNAL') || die(); /** * Any element analysers can analyse. * + * Analysers get_analysers method return all analysable elements in the site; + * it is important that analysable elements implement lazy loading to avoid + * big memory footprints. See \core_analytics\course example. + * * @package core_analytics * @copyright 2016 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later diff --git a/analytics/classes/course.php b/analytics/classes/course.php index f934e072ad1..fed39db5c28 100644 --- a/analytics/classes/course.php +++ b/analytics/classes/course.php @@ -40,9 +40,19 @@ require_once($CFG->dirroot . '/lib/enrollib.php'); class course implements \core_analytics\analysable { /** - * @var \core_analytics\course[] $instances + * @var bool Has this course data been already loaded. */ - protected static $instances = array(); + protected $loaded = false; + + /** + * @var int $cachedid self::$cachedinstance analysable id. + */ + protected static $cachedid = 0; + + /** + * @var \core_analytics\course $cachedinstance + */ + protected static $cachedinstance = null; /** * Course object @@ -122,7 +132,7 @@ class course implements \core_analytics\analysable { * Use self::instance() instead to get cached copies of the course. Instances obtained * through this constructor will not be cached. * - * Loads course students and teachers. + * Lazy load of course data, students and teachers. * * @param int|stdClass $course Course id * @return void @@ -130,18 +140,70 @@ class course implements \core_analytics\analysable { public function __construct($course) { if (is_scalar($course)) { - $this->course = get_course($course); + $this->course = new \stdClass(); + $this->course->id = $course; } else { $this->course = $course; } + } - $this->coursecontext = \context_course::instance($this->course->id); + /** + * Returns an analytics course instance. + * + * Lazy load of course data, students and teachers. + * + * @param int|stdClass $course Course object or course id + * @return \core_analytics\course + */ + public static function instance($course) { + + $courseid = $course; + if (!is_scalar($courseid)) { + $courseid = $course->id; + } + + if (self::$cachedid === $courseid) { + return self::$cachedinstance; + } + + $cachedinstance = new \core_analytics\course($course); + self::$cachedinstance = $cachedinstance; + self::$cachedid = (int)$courseid; + return self::$cachedinstance; + } + + /** + * get_id + * + * @return int + */ + public function get_id() { + return $this->course->id; + } + + /** + * Loads the analytics course object. + * + * @return null + */ + protected function load() { + + // The instance constructor could be already loaded with the full course object. Using shortname + // because it is a required course field. + if (empty($this->course->shortname)) { + $this->course = get_course($this->course->id); + } + + $this->coursecontext = $this->get_context(); $this->now = time(); // Get the course users, including users assigned to student and teacher roles at an higher context. $cache = \cache::make_from_params(\cache_store::MODE_REQUEST, 'core_analytics', 'rolearchetypes'); + // Flag the instance as loaded. + $this->loaded = true; + if (!$studentroles = $cache->get('student')) { $studentroles = array_keys(get_archetype_roles('student')); $cache->set('student', $studentroles); @@ -155,53 +217,13 @@ class course implements \core_analytics\analysable { $this->teacherids = $this->get_user_ids($teacherroles); } - /** - * Returns an analytics course instance. - * - * @param int|stdClass $course Course id - * @return \core_analytics\course - */ - public static function instance($course) { - - $courseid = $course; - if (!is_scalar($courseid)) { - $courseid = $course->id; - } - - if (!empty(self::$instances[$courseid])) { - return self::$instances[$courseid]; - } - - $instance = new \core_analytics\course($course); - self::$instances[$courseid] = $instance; - return self::$instances[$courseid]; - } - - /** - * Clears all statically cached instances. - * - * @return void - */ - public static function reset_caches() { - self::$instances = array(); - } - - /** - * get_id - * - * @return int - */ - public function get_id() { - return $this->course->id; - } - /** * The course short name * * @return string */ public function get_name() { - return format_string($this->course->shortname, true, array('context' => $this->get_context())); + return format_string($this->get_course_data()->shortname, true, array('context' => $this->get_context())); } /** @@ -228,8 +250,8 @@ class course implements \core_analytics\analysable { } // The field always exist but may have no valid if the course is created through a sync process. - if (!empty($this->course->startdate)) { - $this->starttime = (int)$this->course->startdate; + if (!empty($this->get_course_data()->startdate)) { + $this->starttime = (int)$this->get_course_data()->startdate; } else { $this->starttime = 0; } @@ -256,7 +278,7 @@ class course implements \core_analytics\analysable { // We first try to find current course student logs. $firstlogs = array(); - foreach ($this->studentids as $studentid) { + foreach ($this->get_students() as $studentid) { // Grrr, we are limited by logging API, we could do this easily with a // select min(timecreated) from xx where courseid = yy group by userid. @@ -278,7 +300,7 @@ class course implements \core_analytics\analysable { sort($firstlogs); $firstlogsmedian = $this->median($firstlogs); - $studentenrolments = enrol_get_course_users($this->get_id(), $this->studentids); + $studentenrolments = enrol_get_course_users($this->get_id(), $this->get_students()); if (empty($studentenrolments)) { return 0; } @@ -306,8 +328,8 @@ class course implements \core_analytics\analysable { } // The enddate field is only available from Moodle 3.2 (MDL-22078). - if (!empty($this->course->enddate)) { - $this->endtime = (int)$this->course->enddate; + if (!empty($this->get_course_data()->enddate)) { + $this->endtime = (int)$this->get_course_data()->enddate; return $this->endtime; } @@ -362,6 +384,11 @@ class course implements \core_analytics\analysable { * @return \stdClass */ public function get_course_data() { + + if (!$this->loaded) { + $this->load(); + } + return $this->course; } @@ -413,7 +440,7 @@ class course implements \core_analytics\analysable { public function get_user_ids($roleids) { // We need to index by ra.id as a user may have more than 1 $roles role. - $records = get_role_users($roleids, $this->coursecontext, true, 'ra.id, u.id AS userid, r.id AS roleid', 'ra.id ASC'); + $records = get_role_users($roleids, $this->get_context(), true, 'ra.id, u.id AS userid, r.id AS roleid', 'ra.id ASC'); // If a user have more than 1 $roles role array_combine will discard the duplicate. $callable = array($this, 'filter_user_id'); @@ -427,6 +454,11 @@ class course implements \core_analytics\analysable { * @return stdClass[] */ public function get_students() { + + if (!$this->loaded) { + $this->load(); + } + return $this->studentids; } @@ -439,7 +471,7 @@ class course implements \core_analytics\analysable { global $DB; // No logs if no students. - if (empty($this->studentids)) { + if (empty($this->get_students())) { return 0; } @@ -592,7 +624,7 @@ class course implements \core_analytics\analysable { // When the course is using format weeks we use the week's end date. $format = course_get_format($activity->get_modinfo()->get_course()); - if ($this->course->format === 'weeks') { + if ($this->get_course_data()->format === 'weeks') { $dates = $format->get_section_dates($section); // We need to consider the +2 hours added by get_section_dates. @@ -614,7 +646,7 @@ class course implements \core_analytics\analysable { return false; } - if (!course_format_uses_sections($this->course->format)) { + if (!course_format_uses_sections($this->get_course_data()->format)) { // If it does not use sections and there are no availability conditions to access it it is available // and we can not magically classify it into any other time range than this one. return true; @@ -717,7 +749,7 @@ class course implements \core_analytics\analysable { } // Check the amount of student logs in the 4 previous weeks. - list($studentssql, $studentsparams) = $DB->get_in_or_equal($this->studentids, SQL_PARAMS_NAMED); + list($studentssql, $studentsparams) = $DB->get_in_or_equal($this->get_students(), SQL_PARAMS_NAMED); $filterselect = $prefix . 'courseid = :courseid AND ' . $prefix . 'userid ' . $studentssql; $filterparams = array('courseid' => $this->course->id) + $studentsparams; diff --git a/analytics/classes/local/analyser/by_course.php b/analytics/classes/local/analyser/by_course.php index 66618069e00..7d9f28a7145 100644 --- a/analytics/classes/local/analyser/by_course.php +++ b/analytics/classes/local/analyser/by_course.php @@ -47,14 +47,14 @@ abstract class by_course extends base { $courses = $this->options['filter']; } else { // Iterate through all potentially valid courses. - $courses = get_courses('all', 'c.sortorder ASC'); + $courses = get_courses('all', 'c.sortorder ASC', 'c.id'); } unset($courses[SITEID]); $analysables = array(); foreach ($courses as $course) { // Skip the frontpage course. - $analysable = \core_analytics\course::instance($course); + $analysable = \core_analytics\course::instance($course->id); $analysables[$analysable->get_id()] = $analysable; } diff --git a/analytics/tests/prediction_test.php b/analytics/tests/prediction_test.php index 269a5c1301d..39c2b98a167 100644 --- a/analytics/tests/prediction_test.php +++ b/analytics/tests/prediction_test.php @@ -24,6 +24,7 @@ defined('MOODLE_INTERNAL') || die(); +global $CFG; require_once(__DIR__ . '/fixtures/test_indicator_max.php'); require_once(__DIR__ . '/fixtures/test_indicator_min.php'); require_once(__DIR__ . '/fixtures/test_indicator_fullname.php'); diff --git a/lib/phpunit/classes/util.php b/lib/phpunit/classes/util.php index c257f63eac9..909024110e7 100644 --- a/lib/phpunit/classes/util.php +++ b/lib/phpunit/classes/util.php @@ -220,7 +220,6 @@ class phpunit_util extends testing_util { if (class_exists('core_media_manager', false)) { core_media_manager::reset_caches(); } - \core_analytics\course::reset_caches(); // Reset static unit test options. if (class_exists('\availability_date\condition', false)) {