weekly release 3.7dev
[moodle.git] / analytics / classes / manager.php
CommitLineData
369389c9
DM
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/>.
16
17/**
b94dbb55 18 * Analytics basic actions manager.
369389c9
DM
19 *
20 * @package core_analytics
21 * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23 */
24
25namespace core_analytics;
26
27defined('MOODLE_INTERNAL') || die();
28
29/**
b94dbb55 30 * Analytics basic actions manager.
369389c9
DM
31 *
32 * @package core_analytics
33 * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
34 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
35 */
36class manager {
37
ed12ba6b
DM
38 /**
39 * Default mlbackend
40 */
41 const DEFAULT_MLBACKEND = '\mlbackend_php\processor';
42
369389c9
DM
43 /**
44 * @var \core_analytics\predictor[]
45 */
46 protected static $predictionprocessors = null;
47
e4453adc
DM
48 /**
49 * @var \core_analytics\local\target\base[]
50 */
51 protected static $alltargets = null;
52
369389c9
DM
53 /**
54 * @var \core_analytics\local\indicator\base[]
55 */
56 protected static $allindicators = null;
57
58 /**
59 * @var \core_analytics\local\time_splitting\base[]
60 */
61 protected static $alltimesplittings = null;
62
1611308b
DM
63 /**
64 * Checks that the user can manage models
65 *
66 * @throws \required_capability_exception
67 * @return void
68 */
69 public static function check_can_manage_models() {
70 require_capability('moodle/analytics:managemodels', \context_system::instance());
71 }
72
73 /**
74 * Checks that the user can list that context insights
75 *
76 * @throws \required_capability_exception
77 * @param \context $context
78 * @return void
79 */
80 public static function check_can_list_insights(\context $context) {
81 require_capability('moodle/analytics:listinsights', $context);
82 }
83
584ffa4f
DM
84 /**
85 * Returns all system models that match the provided filters.
86 *
87 * @param bool $enabled
88 * @param bool $trained
08015e18 89 * @param \context|false $predictioncontext
584ffa4f
DM
90 * @return \core_analytics\model[]
91 */
369389c9
DM
92 public static function get_all_models($enabled = false, $trained = false, $predictioncontext = false) {
93 global $DB;
94
1611308b
DM
95 $params = array();
96
88777c57 97 $sql = "SELECT am.* FROM {analytics_models} am";
1611308b 98
88777c57 99 if ($enabled || $trained || $predictioncontext) {
1611308b
DM
100 $conditions = [];
101 if ($enabled) {
102 $conditions[] = 'am.enabled = :enabled';
103 $params['enabled'] = 1;
104 }
105 if ($trained) {
106 $conditions[] = 'am.trained = :trained';
107 $params['trained'] = 1;
108 }
88777c57
DM
109 if ($predictioncontext) {
110 $conditions[] = "EXISTS (SELECT 'x' FROM {analytics_predictions} ap WHERE ap.modelid = am.id AND ap.contextid = :contextid)";
111 $params['contextid'] = $predictioncontext->id;
112 }
e10b29ed 113 $sql .= ' WHERE ' . implode(' AND ', $conditions);
369389c9 114 }
e10b29ed
DM
115 $sql .= ' ORDER BY am.enabled DESC, am.timemodified DESC';
116
1611308b 117 $modelobjs = $DB->get_records_sql($sql, $params);
369389c9
DM
118
119 $models = array();
120 foreach ($modelobjs as $modelobj) {
3a396286
DM
121 $model = new \core_analytics\model($modelobj);
122 if ($model->is_available()) {
123 $models[$modelobj->id] = $model;
124 }
369389c9
DM
125 }
126 return $models;
127 }
128
129 /**
ed12ba6b 130 * Returns the provided predictions processor class.
369389c9 131 *
ed12ba6b 132 * @param false|string $predictionclass Returns the system default processor if false
369389c9
DM
133 * @param bool $checkisready
134 * @return \core_analytics\predictor
135 */
136 public static function get_predictions_processor($predictionclass = false, $checkisready = true) {
137
138 // We want 0 or 1 so we can use it as an array key for caching.
139 $checkisready = intval($checkisready);
140
ed12ba6b 141 if (!$predictionclass) {
369389c9
DM
142 $predictionclass = get_config('analytics', 'predictionsprocessor');
143 }
144
145 if (empty($predictionclass)) {
146 // Use the default one if nothing set.
ed12ba6b 147 $predictionclass = self::default_mlbackend();
369389c9
DM
148 }
149
150 if (!class_exists($predictionclass)) {
151 throw new \coding_exception('Invalid predictions processor ' . $predictionclass . '.');
152 }
153
154 $interfaces = class_implements($predictionclass);
155 if (empty($interfaces['core_analytics\predictor'])) {
156 throw new \coding_exception($predictionclass . ' should implement \core_analytics\predictor.');
157 }
158
159 // Return it from the cached list.
160 if (!isset(self::$predictionprocessors[$checkisready][$predictionclass])) {
161
162 $instance = new $predictionclass();
163 if ($checkisready) {
164 $isready = $instance->is_ready();
165 if ($isready !== true) {
166 throw new \moodle_exception('errorprocessornotready', 'analytics', '', $isready);
167 }
168 }
169 self::$predictionprocessors[$checkisready][$predictionclass] = $instance;
170 }
171
172 return self::$predictionprocessors[$checkisready][$predictionclass];
173 }
174
1611308b
DM
175 /**
176 * Return all system predictions processors.
177 *
08015e18 178 * @return \core_analytics\predictor[]
1611308b 179 */
369389c9
DM
180 public static function get_all_prediction_processors() {
181
182 $mlbackends = \core_component::get_plugin_list('mlbackend');
183
184 $predictionprocessors = array();
185 foreach ($mlbackends as $mlbackend => $unused) {
206d7aa9 186 $classfullpath = '\mlbackend_' . $mlbackend . '\processor';
369389c9
DM
187 $predictionprocessors[$classfullpath] = self::get_predictions_processor($classfullpath, false);
188 }
189 return $predictionprocessors;
190 }
191
ed12ba6b
DM
192 /**
193 * Returns the name of the provided predictions processor.
194 *
195 * @param \core_analytics\predictor $predictionsprocessor
196 * @return string
197 */
198 public static function get_predictions_processor_name(\core_analytics\predictor $predictionsprocessor) {
199 $component = substr(get_class($predictionsprocessor), 0, strpos(get_class($predictionsprocessor), '\\', 1));
200 return get_string('pluginname', $component);
201 }
202
203 /**
204 * Whether the provided plugin is used by any model.
205 *
206 * @param string $plugin
207 * @return bool
208 */
209 public static function is_mlbackend_used($plugin) {
210 $models = self::get_all_models();
211 foreach ($models as $model) {
212 $processor = $model->get_predictions_processor();
213 $noprefixnamespace = ltrim(get_class($processor), '\\');
214 $processorplugin = substr($noprefixnamespace, 0, strpos($noprefixnamespace, '\\'));
215 if ($processorplugin == $plugin) {
216 return true;
217 }
218 }
219
220 // Default predictions processor.
221 $defaultprocessorclass = get_config('analytics', 'predictionsprocessor');
222 $pluginclass = '\\' . $plugin . '\\processor';
223 if ($pluginclass === $defaultprocessorclass) {
224 return true;
225 }
226
227 return false;
228 }
229
369389c9
DM
230 /**
231 * Get all available time splitting methods.
232 *
1cc2b4ba 233 * @return \core_analytics\local\time_splitting\base[]
369389c9
DM
234 */
235 public static function get_all_time_splittings() {
236 if (self::$alltimesplittings !== null) {
237 return self::$alltimesplittings;
238 }
239
240 $classes = self::get_analytics_classes('time_splitting');
241
242 self::$alltimesplittings = [];
243 foreach ($classes as $fullclassname => $classpath) {
244 $instance = self::get_time_splitting($fullclassname);
245 // We need to check that it is a valid time splitting method, it may be an abstract class.
246 if ($instance) {
247 self::$alltimesplittings[$instance->get_id()] = $instance;
248 }
249 }
250
251 return self::$alltimesplittings;
252 }
253
254 /**
255 * Returns the enabled time splitting methods.
256 *
257 * @return \core_analytics\local\time_splitting\base[]
258 */
259 public static function get_enabled_time_splitting_methods() {
260
261 if ($enabledtimesplittings = get_config('analytics', 'timesplittings')) {
262 $enabledtimesplittings = array_flip(explode(',', $enabledtimesplittings));
263 }
264
265 $timesplittings = self::get_all_time_splittings();
266 foreach ($timesplittings as $key => $timesplitting) {
267
268 // We remove the ones that are not enabled. This also respects the default value (all methods enabled).
269 if (!empty($enabledtimesplittings) && !isset($enabledtimesplittings[$key])) {
270 unset($timesplittings[$key]);
271 }
272 }
273 return $timesplittings;
274 }
275
276 /**
277 * Returns a time splitting method by its classname.
278 *
279 * @param string $fullclassname
280 * @return \core_analytics\local\time_splitting\base|false False if it is not valid.
281 */
282 public static function get_time_splitting($fullclassname) {
283 if (!self::is_valid($fullclassname, '\core_analytics\local\time_splitting\base')) {
284 return false;
285 }
286 return new $fullclassname();
287 }
288
e4453adc
DM
289 /**
290 * Return all targets in the system.
291 *
292 * @return \core_analytics\local\target\base[]
293 */
294 public static function get_all_targets() : array {
295 if (self::$alltargets !== null) {
296 return self::$alltargets;
297 }
298
299 $classes = self::get_analytics_classes('target');
300
301 self::$alltargets = [];
302 foreach ($classes as $fullclassname => $classpath) {
303 $instance = self::get_target($fullclassname);
304 if ($instance) {
305 self::$alltargets[$instance->get_id()] = $instance;
306 }
307 }
308
309 return self::$alltargets;
310 }
369389c9
DM
311 /**
312 * Return all system indicators.
313 *
314 * @return \core_analytics\local\indicator\base[]
315 */
316 public static function get_all_indicators() {
317 if (self::$allindicators !== null) {
318 return self::$allindicators;
319 }
320
321 $classes = self::get_analytics_classes('indicator');
322
323 self::$allindicators = [];
324 foreach ($classes as $fullclassname => $classpath) {
325 $instance = self::get_indicator($fullclassname);
326 if ($instance) {
b0c24929 327 self::$allindicators[$instance->get_id()] = $instance;
369389c9
DM
328 }
329 }
330
331 return self::$allindicators;
332 }
333
1611308b
DM
334 /**
335 * Returns the specified target
336 *
337 * @param mixed $fullclassname
338 * @return \core_analytics\local\target\base|false False if it is not valid
339 */
369389c9
DM
340 public static function get_target($fullclassname) {
341 if (!self::is_valid($fullclassname, 'core_analytics\local\target\base')) {
342 return false;
343 }
344 return new $fullclassname();
345 }
346
347 /**
348 * Returns an instance of the provided indicator.
349 *
350 * @param string $fullclassname
351 * @return \core_analytics\local\indicator\base|false False if it is not valid.
352 */
353 public static function get_indicator($fullclassname) {
354 if (!self::is_valid($fullclassname, 'core_analytics\local\indicator\base')) {
355 return false;
356 }
357 return new $fullclassname();
358 }
359
360 /**
361 * Returns whether a time splitting method is valid or not.
362 *
363 * @param string $fullclassname
1611308b 364 * @param string $baseclass
369389c9
DM
365 * @return bool
366 */
367 public static function is_valid($fullclassname, $baseclass) {
368 if (is_subclass_of($fullclassname, $baseclass)) {
369 if ((new \ReflectionClass($fullclassname))->isInstantiable()) {
370 return true;
371 }
372 }
373 return false;
374 }
375
f67f35f3 376 /**
1611308b 377 * Returns the logstore used for analytics.
f67f35f3 378 *
2db6e981 379 * @return \core\log\sql_reader|false False if no log stores are enabled.
f67f35f3
DM
380 */
381 public static function get_analytics_logstore() {
382 $readers = get_log_manager()->get_readers('core\log\sql_reader');
383 $analyticsstore = get_config('analytics', 'logstore');
85e50ba4
DM
384
385 if (!empty($analyticsstore) && !empty($readers[$analyticsstore])) {
f67f35f3 386 $logstore = $readers[$analyticsstore];
85e50ba4
DM
387 } else if (empty($analyticsstore) && !empty($readers)) {
388 // The first one, it is the same default than in settings.
389 $logstore = reset($readers);
2db6e981 390 } else if (!empty($readers)) {
f67f35f3
DM
391 $logstore = reset($readers);
392 debugging('The selected log store for analytics is not available anymore. Using "' .
393 $logstore->get_name() . '"', DEBUG_DEVELOPER);
394 }
395
2db6e981
DM
396 if (empty($logstore)) {
397 debugging('No system log stores available to use for analytics', DEBUG_DEVELOPER);
398 return false;
399 }
400
f67f35f3
DM
401 if (!$logstore->is_logging()) {
402 debugging('The selected log store for analytics "' . $logstore->get_name() .
403 '" is not logging activity logs', DEBUG_DEVELOPER);
404 }
405
406 return $logstore;
407 }
408
0690a271
DM
409 /**
410 * Returns this analysable calculations during the provided period.
411 *
412 * @param \core_analytics\analysable $analysable
413 * @param int $starttime
414 * @param int $endtime
415 * @param string $samplesorigin The samples origin as sampleid is not unique across models.
416 * @return array
417 */
418 public static function get_indicator_calculations($analysable, $starttime, $endtime, $samplesorigin) {
419 global $DB;
420
421 $params = array('starttime' => $starttime, 'endtime' => $endtime, 'contextid' => $analysable->get_context()->id,
422 'sampleorigin' => $samplesorigin);
423 $calculations = $DB->get_recordset('analytics_indicator_calc', $params, '', 'indicator, sampleid, value');
424
425 $existingcalculations = array();
426 foreach ($calculations as $calculation) {
427 if (empty($existingcalculations[$calculation->indicator])) {
428 $existingcalculations[$calculation->indicator] = array();
429 }
430 $existingcalculations[$calculation->indicator][$calculation->sampleid] = $calculation->value;
431 }
a938e409 432 $calculations->close();
0690a271
DM
433 return $existingcalculations;
434 }
435
1611308b
DM
436 /**
437 * Returns the models with insights at the provided context.
438 *
439 * @param \context $context
440 * @return \core_analytics\model[]
441 */
442 public static function get_models_with_insights(\context $context) {
443
444 self::check_can_list_insights($context);
445
413f19bc 446 $models = self::get_all_models(true, true, $context);
1611308b
DM
447 foreach ($models as $key => $model) {
448 // Check that it not only have predictions but also generates insights from them.
449 if (!$model->uses_insights()) {
450 unset($models[$key]);
451 }
452 }
453 return $models;
454 }
455
456 /**
457 * Returns a prediction
458 *
459 * @param int $predictionid
460 * @param bool $requirelogin
461 * @return array array($model, $prediction, $context)
462 */
463 public static function get_prediction($predictionid, $requirelogin = false) {
464 global $DB;
465
466 if (!$predictionobj = $DB->get_record('analytics_predictions', array('id' => $predictionid))) {
4a210b06
DM
467 throw new \moodle_exception('errorpredictionnotfound', 'analytics');
468 }
469
470 $context = \context::instance_by_id($predictionobj->contextid, IGNORE_MISSING);
471 if (!$context) {
472 throw new \moodle_exception('errorpredictioncontextnotavailable', 'analytics');
1611308b
DM
473 }
474
475 if ($requirelogin) {
476 list($context, $course, $cm) = get_context_info_array($predictionobj->contextid);
477 require_login($course, false, $cm);
1611308b
DM
478 }
479
413f19bc 480 self::check_can_list_insights($context);
1611308b
DM
481
482 $model = new \core_analytics\model($predictionobj->modelid);
483 $sampledata = $model->prediction_sample_data($predictionobj);
484 $prediction = new \core_analytics\prediction($predictionobj, $sampledata);
485
486 return array($model, $prediction, $context);
487 }
488
e709e544
DM
489 /**
490 * Adds the models included with moodle core to the system.
491 *
492 * @return void
493 */
494 public static function add_builtin_models() {
495
690ad875 496 $target = self::get_target('\core\analytics\target\course_dropout');
e709e544
DM
497
498 // Community of inquiry indicators.
499 $coiindicators = array(
500 '\mod_assign\analytics\indicator\cognitive_depth',
501 '\mod_assign\analytics\indicator\social_breadth',
502 '\mod_book\analytics\indicator\cognitive_depth',
503 '\mod_book\analytics\indicator\social_breadth',
504 '\mod_chat\analytics\indicator\cognitive_depth',
505 '\mod_chat\analytics\indicator\social_breadth',
506 '\mod_choice\analytics\indicator\cognitive_depth',
507 '\mod_choice\analytics\indicator\social_breadth',
508 '\mod_data\analytics\indicator\cognitive_depth',
509 '\mod_data\analytics\indicator\social_breadth',
510 '\mod_feedback\analytics\indicator\cognitive_depth',
511 '\mod_feedback\analytics\indicator\social_breadth',
512 '\mod_folder\analytics\indicator\cognitive_depth',
513 '\mod_folder\analytics\indicator\social_breadth',
514 '\mod_forum\analytics\indicator\cognitive_depth',
515 '\mod_forum\analytics\indicator\social_breadth',
516 '\mod_glossary\analytics\indicator\cognitive_depth',
517 '\mod_glossary\analytics\indicator\social_breadth',
518 '\mod_imscp\analytics\indicator\cognitive_depth',
519 '\mod_imscp\analytics\indicator\social_breadth',
520 '\mod_label\analytics\indicator\cognitive_depth',
521 '\mod_label\analytics\indicator\social_breadth',
522 '\mod_lesson\analytics\indicator\cognitive_depth',
523 '\mod_lesson\analytics\indicator\social_breadth',
524 '\mod_lti\analytics\indicator\cognitive_depth',
525 '\mod_lti\analytics\indicator\social_breadth',
526 '\mod_page\analytics\indicator\cognitive_depth',
527 '\mod_page\analytics\indicator\social_breadth',
528 '\mod_quiz\analytics\indicator\cognitive_depth',
529 '\mod_quiz\analytics\indicator\social_breadth',
530 '\mod_resource\analytics\indicator\cognitive_depth',
531 '\mod_resource\analytics\indicator\social_breadth',
532 '\mod_scorm\analytics\indicator\cognitive_depth',
533 '\mod_scorm\analytics\indicator\social_breadth',
534 '\mod_survey\analytics\indicator\cognitive_depth',
535 '\mod_survey\analytics\indicator\social_breadth',
536 '\mod_url\analytics\indicator\cognitive_depth',
537 '\mod_url\analytics\indicator\social_breadth',
538 '\mod_wiki\analytics\indicator\cognitive_depth',
539 '\mod_wiki\analytics\indicator\social_breadth',
540 '\mod_workshop\analytics\indicator\cognitive_depth',
541 '\mod_workshop\analytics\indicator\social_breadth',
0328f12b
DM
542 '\core_course\analytics\indicator\completion_enabled',
543 '\core_course\analytics\indicator\potential_cognitive_depth',
544 '\core_course\analytics\indicator\potential_social_breadth',
545 '\core\analytics\indicator\any_access_after_end',
546 '\core\analytics\indicator\any_access_before_start',
6e98bca0 547 '\core\analytics\indicator\any_write_action_in_course',
0328f12b 548 '\core\analytics\indicator\read_actions',
e709e544
DM
549 );
550 $indicators = array();
551 foreach ($coiindicators as $coiindicator) {
690ad875 552 $indicator = self::get_indicator($coiindicator);
e709e544
DM
553 $indicators[$indicator->get_id()] = $indicator;
554 }
555 if (!\core_analytics\model::exists($target, $indicators)) {
556 $model = \core_analytics\model::create($target, $indicators);
557 }
558
559 // No teaching model.
690ad875 560 $target = self::get_target('\core\analytics\target\no_teaching');
e709e544 561 $timesplittingmethod = '\core\analytics\time_splitting\single_range';
690ad875 562 $noteacher = self::get_indicator('\core_course\analytics\indicator\no_teacher');
0927604f
DM
563 $nostudent = self::get_indicator('\core_course\analytics\indicator\no_student');
564 $indicators = array($noteacher->get_id() => $noteacher, $nostudent->get_id() => $nostudent);
e709e544
DM
565 if (!\core_analytics\model::exists($target, $indicators)) {
566 \core_analytics\model::create($target, $indicators, $timesplittingmethod);
567 }
568 }
569
f9222c49
DM
570 /**
571 * Cleans up analytics db tables that do not directly depend on analysables that may have been deleted.
572 */
573 public static function cleanup() {
574 global $DB;
575
576 // Clean up stuff that depends on contexts that do not exist anymore.
577 $sql = "SELECT DISTINCT ap.contextid FROM {analytics_predictions} ap
578 LEFT JOIN {context} ctx ON ap.contextid = ctx.id
579 WHERE ctx.id IS NULL";
580 $apcontexts = $DB->get_records_sql($sql);
581
582 $sql = "SELECT DISTINCT aic.contextid FROM {analytics_indicator_calc} aic
583 LEFT JOIN {context} ctx ON aic.contextid = ctx.id
584 WHERE ctx.id IS NULL";
585 $indcalccontexts = $DB->get_records_sql($sql);
586
587 $contexts = $apcontexts + $indcalccontexts;
588 if ($contexts) {
589 list($sql, $params) = $DB->get_in_or_equal(array_keys($contexts));
e08c74f5 590 $DB->execute("DELETE FROM {analytics_prediction_actions} WHERE predictionid IN
f9222c49
DM
591 (SELECT ap.id FROM {analytics_predictions} ap WHERE ap.contextid $sql)", $params);
592
593 $DB->delete_records_select('analytics_predictions', "contextid $sql", $params);
594 $DB->delete_records_select('analytics_indicator_calc', "contextid $sql", $params);
595 }
596
597 // Clean up stuff that depends on analysable ids that do not exist anymore.
598 $models = self::get_all_models();
599 foreach ($models as $model) {
600 $analyser = $model->get_analyser(array('notimesplitting' => true));
601 $analysables = $analyser->get_analysables();
602 if (!$analysables) {
603 continue;
604 }
605
606 $analysableids = array_map(function($analysable) {
607 return $analysable->get_id();
608 }, $analysables);
609
610 list($notinsql, $params) = $DB->get_in_or_equal($analysableids, SQL_PARAMS_NAMED, 'param', false);
611 $params['modelid'] = $model->get_id();
612
613 $DB->delete_records_select('analytics_predict_samples', "modelid = :modelid AND analysableid $notinsql", $params);
614 $DB->delete_records_select('analytics_train_samples', "modelid = :modelid AND analysableid $notinsql", $params);
615 }
616 }
617
ed12ba6b
DM
618 /**
619 * Default system backend.
620 *
621 * @return string
622 */
623 public static function default_mlbackend() {
624 return self::DEFAULT_MLBACKEND;
625 }
626
369389c9
DM
627 /**
628 * Returns the provided element classes in the site.
629 *
630 * @param string $element
631 * @return string[] Array keys are the FQCN and the values the class path.
632 */
633 private static function get_analytics_classes($element) {
634
635 // Just in case...
1611308b 636 $element = clean_param($element, PARAM_ALPHANUMEXT);
369389c9 637
206d7aa9
DM
638 // Core analytics classes (analytics subsystem should not contain uses of the analytics API).
639 $classes = \core_component::get_component_classes_in_namespace('core', 'analytics\\' . $element);
640
641 // Plugins.
369389c9
DM
642 foreach (\core_component::get_plugin_types() as $type => $unusedplugintypepath) {
643 foreach (\core_component::get_plugin_list($type) as $pluginname => $unusedpluginpath) {
644 $frankenstyle = $type . '_' . $pluginname;
645 $classes += \core_component::get_component_classes_in_namespace($frankenstyle, 'analytics\\' . $element);
646 }
647 }
1cc2b4ba 648
206d7aa9 649 // Core subsystems.
1cc2b4ba
DM
650 foreach (\core_component::get_core_subsystems() as $subsystemname => $unusedsubsystempath) {
651 $componentname = 'core_' . $subsystemname;
652 $classes += \core_component::get_component_classes_in_namespace($componentname, 'analytics\\' . $element);
653 }
654
369389c9
DM
655 return $classes;
656 }
657}