MDL-61667 analytics: Don't autoenable models with time splitting defined
[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 *
3576b66b
DM
257 * @deprecated since Moodle 3.7
258 * @todo MDL-65086 This will be deleted in Moodle 4.1
259 * @see \core_analytics\manager::get_time_splitting_methods_for_evaluation
369389c9
DM
260 * @return \core_analytics\local\time_splitting\base[]
261 */
262 public static function get_enabled_time_splitting_methods() {
3576b66b
DM
263 debugging('This function has been deprecated. You can use self::get_time_splitting_methods_for_evaluation if ' .
264 'you want to get the default time splitting methods for evaluation, or you can use self::get_all_time_splittings if ' .
265 'you want to get all the time splitting methods available on this site.');
266 return self::get_time_splitting_methods_for_evaluation();
267 }
268
269 /**
270 * Returns the default time splitting methods for model evaluation.
271 *
272 * @return \core_analytics\local\time_splitting\base[]
273 */
274 public static function get_time_splitting_methods_for_evaluation() {
369389c9 275
3576b66b 276 if ($enabledtimesplittings = get_config('analytics', 'defaulttimesplittingsevaluation')) {
369389c9
DM
277 $enabledtimesplittings = array_flip(explode(',', $enabledtimesplittings));
278 }
279
280 $timesplittings = self::get_all_time_splittings();
281 foreach ($timesplittings as $key => $timesplitting) {
282
283 // We remove the ones that are not enabled. This also respects the default value (all methods enabled).
284 if (!empty($enabledtimesplittings) && !isset($enabledtimesplittings[$key])) {
285 unset($timesplittings[$key]);
286 }
287 }
288 return $timesplittings;
289 }
290
291 /**
292 * Returns a time splitting method by its classname.
293 *
294 * @param string $fullclassname
295 * @return \core_analytics\local\time_splitting\base|false False if it is not valid.
296 */
297 public static function get_time_splitting($fullclassname) {
298 if (!self::is_valid($fullclassname, '\core_analytics\local\time_splitting\base')) {
299 return false;
300 }
301 return new $fullclassname();
302 }
303
e4453adc
DM
304 /**
305 * Return all targets in the system.
306 *
307 * @return \core_analytics\local\target\base[]
308 */
309 public static function get_all_targets() : array {
310 if (self::$alltargets !== null) {
311 return self::$alltargets;
312 }
313
314 $classes = self::get_analytics_classes('target');
315
316 self::$alltargets = [];
317 foreach ($classes as $fullclassname => $classpath) {
318 $instance = self::get_target($fullclassname);
319 if ($instance) {
320 self::$alltargets[$instance->get_id()] = $instance;
321 }
322 }
323
324 return self::$alltargets;
325 }
369389c9
DM
326 /**
327 * Return all system indicators.
328 *
329 * @return \core_analytics\local\indicator\base[]
330 */
331 public static function get_all_indicators() {
332 if (self::$allindicators !== null) {
333 return self::$allindicators;
334 }
335
336 $classes = self::get_analytics_classes('indicator');
337
338 self::$allindicators = [];
339 foreach ($classes as $fullclassname => $classpath) {
340 $instance = self::get_indicator($fullclassname);
341 if ($instance) {
b0c24929 342 self::$allindicators[$instance->get_id()] = $instance;
369389c9
DM
343 }
344 }
345
346 return self::$allindicators;
347 }
348
1611308b
DM
349 /**
350 * Returns the specified target
351 *
352 * @param mixed $fullclassname
353 * @return \core_analytics\local\target\base|false False if it is not valid
354 */
369389c9
DM
355 public static function get_target($fullclassname) {
356 if (!self::is_valid($fullclassname, 'core_analytics\local\target\base')) {
357 return false;
358 }
359 return new $fullclassname();
360 }
361
362 /**
363 * Returns an instance of the provided indicator.
364 *
365 * @param string $fullclassname
366 * @return \core_analytics\local\indicator\base|false False if it is not valid.
367 */
368 public static function get_indicator($fullclassname) {
369 if (!self::is_valid($fullclassname, 'core_analytics\local\indicator\base')) {
370 return false;
371 }
372 return new $fullclassname();
373 }
374
375 /**
376 * Returns whether a time splitting method is valid or not.
377 *
378 * @param string $fullclassname
1611308b 379 * @param string $baseclass
369389c9
DM
380 * @return bool
381 */
382 public static function is_valid($fullclassname, $baseclass) {
383 if (is_subclass_of($fullclassname, $baseclass)) {
384 if ((new \ReflectionClass($fullclassname))->isInstantiable()) {
385 return true;
386 }
387 }
388 return false;
389 }
390
f67f35f3 391 /**
1611308b 392 * Returns the logstore used for analytics.
f67f35f3 393 *
2db6e981 394 * @return \core\log\sql_reader|false False if no log stores are enabled.
f67f35f3
DM
395 */
396 public static function get_analytics_logstore() {
397 $readers = get_log_manager()->get_readers('core\log\sql_reader');
398 $analyticsstore = get_config('analytics', 'logstore');
85e50ba4
DM
399
400 if (!empty($analyticsstore) && !empty($readers[$analyticsstore])) {
f67f35f3 401 $logstore = $readers[$analyticsstore];
85e50ba4
DM
402 } else if (empty($analyticsstore) && !empty($readers)) {
403 // The first one, it is the same default than in settings.
404 $logstore = reset($readers);
2db6e981 405 } else if (!empty($readers)) {
f67f35f3
DM
406 $logstore = reset($readers);
407 debugging('The selected log store for analytics is not available anymore. Using "' .
408 $logstore->get_name() . '"', DEBUG_DEVELOPER);
409 }
410
2db6e981
DM
411 if (empty($logstore)) {
412 debugging('No system log stores available to use for analytics', DEBUG_DEVELOPER);
413 return false;
414 }
415
f67f35f3
DM
416 if (!$logstore->is_logging()) {
417 debugging('The selected log store for analytics "' . $logstore->get_name() .
418 '" is not logging activity logs', DEBUG_DEVELOPER);
419 }
420
421 return $logstore;
422 }
423
0690a271
DM
424 /**
425 * Returns this analysable calculations during the provided period.
426 *
427 * @param \core_analytics\analysable $analysable
428 * @param int $starttime
429 * @param int $endtime
430 * @param string $samplesorigin The samples origin as sampleid is not unique across models.
431 * @return array
432 */
433 public static function get_indicator_calculations($analysable, $starttime, $endtime, $samplesorigin) {
434 global $DB;
435
436 $params = array('starttime' => $starttime, 'endtime' => $endtime, 'contextid' => $analysable->get_context()->id,
437 'sampleorigin' => $samplesorigin);
438 $calculations = $DB->get_recordset('analytics_indicator_calc', $params, '', 'indicator, sampleid, value');
439
440 $existingcalculations = array();
441 foreach ($calculations as $calculation) {
442 if (empty($existingcalculations[$calculation->indicator])) {
443 $existingcalculations[$calculation->indicator] = array();
444 }
445 $existingcalculations[$calculation->indicator][$calculation->sampleid] = $calculation->value;
446 }
a938e409 447 $calculations->close();
0690a271
DM
448 return $existingcalculations;
449 }
450
1611308b
DM
451 /**
452 * Returns the models with insights at the provided context.
453 *
454 * @param \context $context
455 * @return \core_analytics\model[]
456 */
457 public static function get_models_with_insights(\context $context) {
458
459 self::check_can_list_insights($context);
460
413f19bc 461 $models = self::get_all_models(true, true, $context);
1611308b
DM
462 foreach ($models as $key => $model) {
463 // Check that it not only have predictions but also generates insights from them.
464 if (!$model->uses_insights()) {
465 unset($models[$key]);
466 }
467 }
468 return $models;
469 }
470
471 /**
472 * Returns a prediction
473 *
474 * @param int $predictionid
475 * @param bool $requirelogin
476 * @return array array($model, $prediction, $context)
477 */
478 public static function get_prediction($predictionid, $requirelogin = false) {
479 global $DB;
480
481 if (!$predictionobj = $DB->get_record('analytics_predictions', array('id' => $predictionid))) {
4a210b06
DM
482 throw new \moodle_exception('errorpredictionnotfound', 'analytics');
483 }
484
485 $context = \context::instance_by_id($predictionobj->contextid, IGNORE_MISSING);
486 if (!$context) {
487 throw new \moodle_exception('errorpredictioncontextnotavailable', 'analytics');
1611308b
DM
488 }
489
490 if ($requirelogin) {
491 list($context, $course, $cm) = get_context_info_array($predictionobj->contextid);
492 require_login($course, false, $cm);
1611308b
DM
493 }
494
413f19bc 495 self::check_can_list_insights($context);
1611308b
DM
496
497 $model = new \core_analytics\model($predictionobj->modelid);
498 $sampledata = $model->prediction_sample_data($predictionobj);
499 $prediction = new \core_analytics\prediction($predictionobj, $sampledata);
500
501 return array($model, $prediction, $context);
502 }
503
e709e544
DM
504 /**
505 * Adds the models included with moodle core to the system.
506 *
507 * @return void
508 */
509 public static function add_builtin_models() {
510
690ad875 511 $target = self::get_target('\core\analytics\target\course_dropout');
e709e544
DM
512
513 // Community of inquiry indicators.
514 $coiindicators = array(
515 '\mod_assign\analytics\indicator\cognitive_depth',
516 '\mod_assign\analytics\indicator\social_breadth',
517 '\mod_book\analytics\indicator\cognitive_depth',
518 '\mod_book\analytics\indicator\social_breadth',
519 '\mod_chat\analytics\indicator\cognitive_depth',
520 '\mod_chat\analytics\indicator\social_breadth',
521 '\mod_choice\analytics\indicator\cognitive_depth',
522 '\mod_choice\analytics\indicator\social_breadth',
523 '\mod_data\analytics\indicator\cognitive_depth',
524 '\mod_data\analytics\indicator\social_breadth',
525 '\mod_feedback\analytics\indicator\cognitive_depth',
526 '\mod_feedback\analytics\indicator\social_breadth',
527 '\mod_folder\analytics\indicator\cognitive_depth',
528 '\mod_folder\analytics\indicator\social_breadth',
529 '\mod_forum\analytics\indicator\cognitive_depth',
530 '\mod_forum\analytics\indicator\social_breadth',
531 '\mod_glossary\analytics\indicator\cognitive_depth',
532 '\mod_glossary\analytics\indicator\social_breadth',
533 '\mod_imscp\analytics\indicator\cognitive_depth',
534 '\mod_imscp\analytics\indicator\social_breadth',
535 '\mod_label\analytics\indicator\cognitive_depth',
536 '\mod_label\analytics\indicator\social_breadth',
537 '\mod_lesson\analytics\indicator\cognitive_depth',
538 '\mod_lesson\analytics\indicator\social_breadth',
539 '\mod_lti\analytics\indicator\cognitive_depth',
540 '\mod_lti\analytics\indicator\social_breadth',
541 '\mod_page\analytics\indicator\cognitive_depth',
542 '\mod_page\analytics\indicator\social_breadth',
543 '\mod_quiz\analytics\indicator\cognitive_depth',
544 '\mod_quiz\analytics\indicator\social_breadth',
545 '\mod_resource\analytics\indicator\cognitive_depth',
546 '\mod_resource\analytics\indicator\social_breadth',
547 '\mod_scorm\analytics\indicator\cognitive_depth',
548 '\mod_scorm\analytics\indicator\social_breadth',
549 '\mod_survey\analytics\indicator\cognitive_depth',
550 '\mod_survey\analytics\indicator\social_breadth',
551 '\mod_url\analytics\indicator\cognitive_depth',
552 '\mod_url\analytics\indicator\social_breadth',
553 '\mod_wiki\analytics\indicator\cognitive_depth',
554 '\mod_wiki\analytics\indicator\social_breadth',
555 '\mod_workshop\analytics\indicator\cognitive_depth',
556 '\mod_workshop\analytics\indicator\social_breadth',
0328f12b
DM
557 '\core_course\analytics\indicator\completion_enabled',
558 '\core_course\analytics\indicator\potential_cognitive_depth',
559 '\core_course\analytics\indicator\potential_social_breadth',
560 '\core\analytics\indicator\any_access_after_end',
561 '\core\analytics\indicator\any_access_before_start',
6e98bca0 562 '\core\analytics\indicator\any_write_action_in_course',
0328f12b 563 '\core\analytics\indicator\read_actions',
e709e544
DM
564 );
565 $indicators = array();
566 foreach ($coiindicators as $coiindicator) {
690ad875 567 $indicator = self::get_indicator($coiindicator);
e709e544
DM
568 $indicators[$indicator->get_id()] = $indicator;
569 }
570 if (!\core_analytics\model::exists($target, $indicators)) {
571 $model = \core_analytics\model::create($target, $indicators);
572 }
573
574 // No teaching model.
690ad875 575 $target = self::get_target('\core\analytics\target\no_teaching');
e709e544 576 $timesplittingmethod = '\core\analytics\time_splitting\single_range';
690ad875 577 $noteacher = self::get_indicator('\core_course\analytics\indicator\no_teacher');
0927604f
DM
578 $nostudent = self::get_indicator('\core_course\analytics\indicator\no_student');
579 $indicators = array($noteacher->get_id() => $noteacher, $nostudent->get_id() => $nostudent);
e709e544
DM
580 if (!\core_analytics\model::exists($target, $indicators)) {
581 \core_analytics\model::create($target, $indicators, $timesplittingmethod);
582 }
583 }
584
f9222c49
DM
585 /**
586 * Cleans up analytics db tables that do not directly depend on analysables that may have been deleted.
587 */
588 public static function cleanup() {
589 global $DB;
590
591 // Clean up stuff that depends on contexts that do not exist anymore.
592 $sql = "SELECT DISTINCT ap.contextid FROM {analytics_predictions} ap
593 LEFT JOIN {context} ctx ON ap.contextid = ctx.id
594 WHERE ctx.id IS NULL";
595 $apcontexts = $DB->get_records_sql($sql);
596
597 $sql = "SELECT DISTINCT aic.contextid FROM {analytics_indicator_calc} aic
598 LEFT JOIN {context} ctx ON aic.contextid = ctx.id
599 WHERE ctx.id IS NULL";
600 $indcalccontexts = $DB->get_records_sql($sql);
601
602 $contexts = $apcontexts + $indcalccontexts;
603 if ($contexts) {
604 list($sql, $params) = $DB->get_in_or_equal(array_keys($contexts));
e08c74f5 605 $DB->execute("DELETE FROM {analytics_prediction_actions} WHERE predictionid IN
f9222c49
DM
606 (SELECT ap.id FROM {analytics_predictions} ap WHERE ap.contextid $sql)", $params);
607
608 $DB->delete_records_select('analytics_predictions', "contextid $sql", $params);
609 $DB->delete_records_select('analytics_indicator_calc', "contextid $sql", $params);
610 }
611
612 // Clean up stuff that depends on analysable ids that do not exist anymore.
613 $models = self::get_all_models();
614 foreach ($models as $model) {
615 $analyser = $model->get_analyser(array('notimesplitting' => true));
616 $analysables = $analyser->get_analysables();
617 if (!$analysables) {
618 continue;
619 }
620
621 $analysableids = array_map(function($analysable) {
622 return $analysable->get_id();
623 }, $analysables);
624
625 list($notinsql, $params) = $DB->get_in_or_equal($analysableids, SQL_PARAMS_NAMED, 'param', false);
626 $params['modelid'] = $model->get_id();
627
628 $DB->delete_records_select('analytics_predict_samples', "modelid = :modelid AND analysableid $notinsql", $params);
629 $DB->delete_records_select('analytics_train_samples', "modelid = :modelid AND analysableid $notinsql", $params);
630 }
631 }
632
ed12ba6b
DM
633 /**
634 * Default system backend.
635 *
636 * @return string
637 */
638 public static function default_mlbackend() {
639 return self::DEFAULT_MLBACKEND;
640 }
641
369389c9
DM
642 /**
643 * Returns the provided element classes in the site.
644 *
645 * @param string $element
646 * @return string[] Array keys are the FQCN and the values the class path.
647 */
648 private static function get_analytics_classes($element) {
649
650 // Just in case...
1611308b 651 $element = clean_param($element, PARAM_ALPHANUMEXT);
369389c9 652
206d7aa9
DM
653 // Core analytics classes (analytics subsystem should not contain uses of the analytics API).
654 $classes = \core_component::get_component_classes_in_namespace('core', 'analytics\\' . $element);
655
656 // Plugins.
369389c9
DM
657 foreach (\core_component::get_plugin_types() as $type => $unusedplugintypepath) {
658 foreach (\core_component::get_plugin_list($type) as $pluginname => $unusedpluginpath) {
659 $frankenstyle = $type . '_' . $pluginname;
660 $classes += \core_component::get_component_classes_in_namespace($frankenstyle, 'analytics\\' . $element);
661 }
662 }
1cc2b4ba 663
206d7aa9 664 // Core subsystems.
1cc2b4ba
DM
665 foreach (\core_component::get_core_subsystems() as $subsystemname => $unusedsubsystempath) {
666 $componentname = 'core_' . $subsystemname;
667 $classes += \core_component::get_component_classes_in_namespace($componentname, 'analytics\\' . $element);
668 }
669
369389c9
DM
670 return $classes;
671 }
672}