MDL-61667 analytics: Deprecate the add_builtin_models() method
[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
f0584045
DM
43 /**
44 * Name of the file where components declare their models.
45 */
46 const ANALYTICS_FILENAME = 'db/analytics.php';
47
369389c9
DM
48 /**
49 * @var \core_analytics\predictor[]
50 */
51 protected static $predictionprocessors = null;
52
e4453adc
DM
53 /**
54 * @var \core_analytics\local\target\base[]
55 */
56 protected static $alltargets = null;
57
369389c9
DM
58 /**
59 * @var \core_analytics\local\indicator\base[]
60 */
61 protected static $allindicators = null;
62
63 /**
64 * @var \core_analytics\local\time_splitting\base[]
65 */
66 protected static $alltimesplittings = null;
67
1611308b
DM
68 /**
69 * Checks that the user can manage models
70 *
71 * @throws \required_capability_exception
72 * @return void
73 */
74 public static function check_can_manage_models() {
75 require_capability('moodle/analytics:managemodels', \context_system::instance());
76 }
77
78 /**
79 * Checks that the user can list that context insights
80 *
81 * @throws \required_capability_exception
82 * @param \context $context
83 * @return void
84 */
85 public static function check_can_list_insights(\context $context) {
86 require_capability('moodle/analytics:listinsights', $context);
87 }
88
584ffa4f
DM
89 /**
90 * Returns all system models that match the provided filters.
91 *
92 * @param bool $enabled
93 * @param bool $trained
08015e18 94 * @param \context|false $predictioncontext
584ffa4f
DM
95 * @return \core_analytics\model[]
96 */
369389c9
DM
97 public static function get_all_models($enabled = false, $trained = false, $predictioncontext = false) {
98 global $DB;
99
1611308b
DM
100 $params = array();
101
88777c57 102 $sql = "SELECT am.* FROM {analytics_models} am";
1611308b 103
88777c57 104 if ($enabled || $trained || $predictioncontext) {
1611308b
DM
105 $conditions = [];
106 if ($enabled) {
107 $conditions[] = 'am.enabled = :enabled';
108 $params['enabled'] = 1;
109 }
110 if ($trained) {
111 $conditions[] = 'am.trained = :trained';
112 $params['trained'] = 1;
113 }
88777c57
DM
114 if ($predictioncontext) {
115 $conditions[] = "EXISTS (SELECT 'x' FROM {analytics_predictions} ap WHERE ap.modelid = am.id AND ap.contextid = :contextid)";
116 $params['contextid'] = $predictioncontext->id;
117 }
e10b29ed 118 $sql .= ' WHERE ' . implode(' AND ', $conditions);
369389c9 119 }
e10b29ed
DM
120 $sql .= ' ORDER BY am.enabled DESC, am.timemodified DESC';
121
1611308b 122 $modelobjs = $DB->get_records_sql($sql, $params);
369389c9
DM
123
124 $models = array();
125 foreach ($modelobjs as $modelobj) {
3a396286
DM
126 $model = new \core_analytics\model($modelobj);
127 if ($model->is_available()) {
128 $models[$modelobj->id] = $model;
129 }
369389c9
DM
130 }
131 return $models;
132 }
133
134 /**
ed12ba6b 135 * Returns the provided predictions processor class.
369389c9 136 *
ed12ba6b 137 * @param false|string $predictionclass Returns the system default processor if false
369389c9
DM
138 * @param bool $checkisready
139 * @return \core_analytics\predictor
140 */
141 public static function get_predictions_processor($predictionclass = false, $checkisready = true) {
142
143 // We want 0 or 1 so we can use it as an array key for caching.
144 $checkisready = intval($checkisready);
145
ed12ba6b 146 if (!$predictionclass) {
369389c9
DM
147 $predictionclass = get_config('analytics', 'predictionsprocessor');
148 }
149
150 if (empty($predictionclass)) {
151 // Use the default one if nothing set.
ed12ba6b 152 $predictionclass = self::default_mlbackend();
369389c9
DM
153 }
154
155 if (!class_exists($predictionclass)) {
156 throw new \coding_exception('Invalid predictions processor ' . $predictionclass . '.');
157 }
158
159 $interfaces = class_implements($predictionclass);
160 if (empty($interfaces['core_analytics\predictor'])) {
161 throw new \coding_exception($predictionclass . ' should implement \core_analytics\predictor.');
162 }
163
164 // Return it from the cached list.
165 if (!isset(self::$predictionprocessors[$checkisready][$predictionclass])) {
166
167 $instance = new $predictionclass();
168 if ($checkisready) {
169 $isready = $instance->is_ready();
170 if ($isready !== true) {
171 throw new \moodle_exception('errorprocessornotready', 'analytics', '', $isready);
172 }
173 }
174 self::$predictionprocessors[$checkisready][$predictionclass] = $instance;
175 }
176
177 return self::$predictionprocessors[$checkisready][$predictionclass];
178 }
179
1611308b
DM
180 /**
181 * Return all system predictions processors.
182 *
08015e18 183 * @return \core_analytics\predictor[]
1611308b 184 */
369389c9
DM
185 public static function get_all_prediction_processors() {
186
187 $mlbackends = \core_component::get_plugin_list('mlbackend');
188
189 $predictionprocessors = array();
190 foreach ($mlbackends as $mlbackend => $unused) {
206d7aa9 191 $classfullpath = '\mlbackend_' . $mlbackend . '\processor';
369389c9
DM
192 $predictionprocessors[$classfullpath] = self::get_predictions_processor($classfullpath, false);
193 }
194 return $predictionprocessors;
195 }
196
ed12ba6b
DM
197 /**
198 * Returns the name of the provided predictions processor.
199 *
200 * @param \core_analytics\predictor $predictionsprocessor
201 * @return string
202 */
203 public static function get_predictions_processor_name(\core_analytics\predictor $predictionsprocessor) {
204 $component = substr(get_class($predictionsprocessor), 0, strpos(get_class($predictionsprocessor), '\\', 1));
205 return get_string('pluginname', $component);
206 }
207
208 /**
209 * Whether the provided plugin is used by any model.
210 *
211 * @param string $plugin
212 * @return bool
213 */
214 public static function is_mlbackend_used($plugin) {
215 $models = self::get_all_models();
216 foreach ($models as $model) {
217 $processor = $model->get_predictions_processor();
218 $noprefixnamespace = ltrim(get_class($processor), '\\');
219 $processorplugin = substr($noprefixnamespace, 0, strpos($noprefixnamespace, '\\'));
220 if ($processorplugin == $plugin) {
221 return true;
222 }
223 }
224
225 // Default predictions processor.
226 $defaultprocessorclass = get_config('analytics', 'predictionsprocessor');
227 $pluginclass = '\\' . $plugin . '\\processor';
228 if ($pluginclass === $defaultprocessorclass) {
229 return true;
230 }
231
232 return false;
233 }
234
369389c9
DM
235 /**
236 * Get all available time splitting methods.
237 *
1cc2b4ba 238 * @return \core_analytics\local\time_splitting\base[]
369389c9
DM
239 */
240 public static function get_all_time_splittings() {
241 if (self::$alltimesplittings !== null) {
242 return self::$alltimesplittings;
243 }
244
245 $classes = self::get_analytics_classes('time_splitting');
246
247 self::$alltimesplittings = [];
248 foreach ($classes as $fullclassname => $classpath) {
249 $instance = self::get_time_splitting($fullclassname);
250 // We need to check that it is a valid time splitting method, it may be an abstract class.
251 if ($instance) {
252 self::$alltimesplittings[$instance->get_id()] = $instance;
253 }
254 }
255
256 return self::$alltimesplittings;
257 }
258
259 /**
260 * Returns the enabled time splitting methods.
261 *
3576b66b
DM
262 * @deprecated since Moodle 3.7
263 * @todo MDL-65086 This will be deleted in Moodle 4.1
264 * @see \core_analytics\manager::get_time_splitting_methods_for_evaluation
369389c9
DM
265 * @return \core_analytics\local\time_splitting\base[]
266 */
267 public static function get_enabled_time_splitting_methods() {
3576b66b
DM
268 debugging('This function has been deprecated. You can use self::get_time_splitting_methods_for_evaluation if ' .
269 'you want to get the default time splitting methods for evaluation, or you can use self::get_all_time_splittings if ' .
270 'you want to get all the time splitting methods available on this site.');
271 return self::get_time_splitting_methods_for_evaluation();
272 }
273
274 /**
275 * Returns the default time splitting methods for model evaluation.
276 *
277 * @return \core_analytics\local\time_splitting\base[]
278 */
279 public static function get_time_splitting_methods_for_evaluation() {
369389c9 280
3576b66b 281 if ($enabledtimesplittings = get_config('analytics', 'defaulttimesplittingsevaluation')) {
369389c9
DM
282 $enabledtimesplittings = array_flip(explode(',', $enabledtimesplittings));
283 }
284
285 $timesplittings = self::get_all_time_splittings();
286 foreach ($timesplittings as $key => $timesplitting) {
287
288 // We remove the ones that are not enabled. This also respects the default value (all methods enabled).
289 if (!empty($enabledtimesplittings) && !isset($enabledtimesplittings[$key])) {
290 unset($timesplittings[$key]);
291 }
292 }
293 return $timesplittings;
294 }
295
296 /**
297 * Returns a time splitting method by its classname.
298 *
299 * @param string $fullclassname
300 * @return \core_analytics\local\time_splitting\base|false False if it is not valid.
301 */
302 public static function get_time_splitting($fullclassname) {
303 if (!self::is_valid($fullclassname, '\core_analytics\local\time_splitting\base')) {
304 return false;
305 }
306 return new $fullclassname();
307 }
308
e4453adc
DM
309 /**
310 * Return all targets in the system.
311 *
312 * @return \core_analytics\local\target\base[]
313 */
314 public static function get_all_targets() : array {
315 if (self::$alltargets !== null) {
316 return self::$alltargets;
317 }
318
319 $classes = self::get_analytics_classes('target');
320
321 self::$alltargets = [];
322 foreach ($classes as $fullclassname => $classpath) {
323 $instance = self::get_target($fullclassname);
324 if ($instance) {
325 self::$alltargets[$instance->get_id()] = $instance;
326 }
327 }
328
329 return self::$alltargets;
330 }
369389c9
DM
331 /**
332 * Return all system indicators.
333 *
334 * @return \core_analytics\local\indicator\base[]
335 */
336 public static function get_all_indicators() {
337 if (self::$allindicators !== null) {
338 return self::$allindicators;
339 }
340
341 $classes = self::get_analytics_classes('indicator');
342
343 self::$allindicators = [];
344 foreach ($classes as $fullclassname => $classpath) {
345 $instance = self::get_indicator($fullclassname);
346 if ($instance) {
b0c24929 347 self::$allindicators[$instance->get_id()] = $instance;
369389c9
DM
348 }
349 }
350
351 return self::$allindicators;
352 }
353
1611308b
DM
354 /**
355 * Returns the specified target
356 *
357 * @param mixed $fullclassname
358 * @return \core_analytics\local\target\base|false False if it is not valid
359 */
369389c9
DM
360 public static function get_target($fullclassname) {
361 if (!self::is_valid($fullclassname, 'core_analytics\local\target\base')) {
362 return false;
363 }
364 return new $fullclassname();
365 }
366
367 /**
368 * Returns an instance of the provided indicator.
369 *
370 * @param string $fullclassname
371 * @return \core_analytics\local\indicator\base|false False if it is not valid.
372 */
373 public static function get_indicator($fullclassname) {
374 if (!self::is_valid($fullclassname, 'core_analytics\local\indicator\base')) {
375 return false;
376 }
377 return new $fullclassname();
378 }
379
380 /**
381 * Returns whether a time splitting method is valid or not.
382 *
383 * @param string $fullclassname
1611308b 384 * @param string $baseclass
369389c9
DM
385 * @return bool
386 */
387 public static function is_valid($fullclassname, $baseclass) {
388 if (is_subclass_of($fullclassname, $baseclass)) {
389 if ((new \ReflectionClass($fullclassname))->isInstantiable()) {
390 return true;
391 }
392 }
393 return false;
394 }
395
f67f35f3 396 /**
1611308b 397 * Returns the logstore used for analytics.
f67f35f3 398 *
2db6e981 399 * @return \core\log\sql_reader|false False if no log stores are enabled.
f67f35f3
DM
400 */
401 public static function get_analytics_logstore() {
402 $readers = get_log_manager()->get_readers('core\log\sql_reader');
403 $analyticsstore = get_config('analytics', 'logstore');
85e50ba4
DM
404
405 if (!empty($analyticsstore) && !empty($readers[$analyticsstore])) {
f67f35f3 406 $logstore = $readers[$analyticsstore];
85e50ba4
DM
407 } else if (empty($analyticsstore) && !empty($readers)) {
408 // The first one, it is the same default than in settings.
409 $logstore = reset($readers);
2db6e981 410 } else if (!empty($readers)) {
f67f35f3
DM
411 $logstore = reset($readers);
412 debugging('The selected log store for analytics is not available anymore. Using "' .
413 $logstore->get_name() . '"', DEBUG_DEVELOPER);
414 }
415
2db6e981
DM
416 if (empty($logstore)) {
417 debugging('No system log stores available to use for analytics', DEBUG_DEVELOPER);
418 return false;
419 }
420
f67f35f3
DM
421 if (!$logstore->is_logging()) {
422 debugging('The selected log store for analytics "' . $logstore->get_name() .
423 '" is not logging activity logs', DEBUG_DEVELOPER);
424 }
425
426 return $logstore;
427 }
428
0690a271
DM
429 /**
430 * Returns this analysable calculations during the provided period.
431 *
432 * @param \core_analytics\analysable $analysable
433 * @param int $starttime
434 * @param int $endtime
435 * @param string $samplesorigin The samples origin as sampleid is not unique across models.
436 * @return array
437 */
438 public static function get_indicator_calculations($analysable, $starttime, $endtime, $samplesorigin) {
439 global $DB;
440
441 $params = array('starttime' => $starttime, 'endtime' => $endtime, 'contextid' => $analysable->get_context()->id,
442 'sampleorigin' => $samplesorigin);
443 $calculations = $DB->get_recordset('analytics_indicator_calc', $params, '', 'indicator, sampleid, value');
444
445 $existingcalculations = array();
446 foreach ($calculations as $calculation) {
447 if (empty($existingcalculations[$calculation->indicator])) {
448 $existingcalculations[$calculation->indicator] = array();
449 }
450 $existingcalculations[$calculation->indicator][$calculation->sampleid] = $calculation->value;
451 }
a938e409 452 $calculations->close();
0690a271
DM
453 return $existingcalculations;
454 }
455
1611308b
DM
456 /**
457 * Returns the models with insights at the provided context.
458 *
459 * @param \context $context
460 * @return \core_analytics\model[]
461 */
462 public static function get_models_with_insights(\context $context) {
463
464 self::check_can_list_insights($context);
465
413f19bc 466 $models = self::get_all_models(true, true, $context);
1611308b
DM
467 foreach ($models as $key => $model) {
468 // Check that it not only have predictions but also generates insights from them.
469 if (!$model->uses_insights()) {
470 unset($models[$key]);
471 }
472 }
473 return $models;
474 }
475
476 /**
477 * Returns a prediction
478 *
479 * @param int $predictionid
480 * @param bool $requirelogin
481 * @return array array($model, $prediction, $context)
482 */
483 public static function get_prediction($predictionid, $requirelogin = false) {
484 global $DB;
485
486 if (!$predictionobj = $DB->get_record('analytics_predictions', array('id' => $predictionid))) {
4a210b06
DM
487 throw new \moodle_exception('errorpredictionnotfound', 'analytics');
488 }
489
490 $context = \context::instance_by_id($predictionobj->contextid, IGNORE_MISSING);
491 if (!$context) {
492 throw new \moodle_exception('errorpredictioncontextnotavailable', 'analytics');
1611308b
DM
493 }
494
495 if ($requirelogin) {
496 list($context, $course, $cm) = get_context_info_array($predictionobj->contextid);
497 require_login($course, false, $cm);
1611308b
DM
498 }
499
413f19bc 500 self::check_can_list_insights($context);
1611308b
DM
501
502 $model = new \core_analytics\model($predictionobj->modelid);
503 $sampledata = $model->prediction_sample_data($predictionobj);
504 $prediction = new \core_analytics\prediction($predictionobj, $sampledata);
505
506 return array($model, $prediction, $context);
507 }
508
e709e544 509 /**
3b7c7918 510 * Used to be used to add models included with the Moodle core.
e709e544 511 *
3b7c7918
DM
512 * @deprecated Deprecated since Moodle 3.7 (MDL-61667) - Use lib/db/analytics.php instead.
513 * @todo Remove this method in Moodle 4.1 (MDL-65186).
e709e544
DM
514 * @return void
515 */
516 public static function add_builtin_models() {
517
3b7c7918
DM
518 debugging('core_analytics\manager::add_builtin_models() has been deprecated. Core models are now automatically '.
519 'updated according to their declaration in the lib/db/analytics.php file.', DEBUG_DEVELOPER);
e709e544
DM
520 }
521
f9222c49
DM
522 /**
523 * Cleans up analytics db tables that do not directly depend on analysables that may have been deleted.
524 */
525 public static function cleanup() {
526 global $DB;
527
528 // Clean up stuff that depends on contexts that do not exist anymore.
529 $sql = "SELECT DISTINCT ap.contextid FROM {analytics_predictions} ap
530 LEFT JOIN {context} ctx ON ap.contextid = ctx.id
531 WHERE ctx.id IS NULL";
532 $apcontexts = $DB->get_records_sql($sql);
533
534 $sql = "SELECT DISTINCT aic.contextid FROM {analytics_indicator_calc} aic
535 LEFT JOIN {context} ctx ON aic.contextid = ctx.id
536 WHERE ctx.id IS NULL";
537 $indcalccontexts = $DB->get_records_sql($sql);
538
539 $contexts = $apcontexts + $indcalccontexts;
540 if ($contexts) {
541 list($sql, $params) = $DB->get_in_or_equal(array_keys($contexts));
e08c74f5 542 $DB->execute("DELETE FROM {analytics_prediction_actions} WHERE predictionid IN
f9222c49
DM
543 (SELECT ap.id FROM {analytics_predictions} ap WHERE ap.contextid $sql)", $params);
544
545 $DB->delete_records_select('analytics_predictions', "contextid $sql", $params);
546 $DB->delete_records_select('analytics_indicator_calc', "contextid $sql", $params);
547 }
548
549 // Clean up stuff that depends on analysable ids that do not exist anymore.
550 $models = self::get_all_models();
551 foreach ($models as $model) {
552 $analyser = $model->get_analyser(array('notimesplitting' => true));
553 $analysables = $analyser->get_analysables();
554 if (!$analysables) {
555 continue;
556 }
557
558 $analysableids = array_map(function($analysable) {
559 return $analysable->get_id();
560 }, $analysables);
561
562 list($notinsql, $params) = $DB->get_in_or_equal($analysableids, SQL_PARAMS_NAMED, 'param', false);
563 $params['modelid'] = $model->get_id();
564
565 $DB->delete_records_select('analytics_predict_samples', "modelid = :modelid AND analysableid $notinsql", $params);
566 $DB->delete_records_select('analytics_train_samples', "modelid = :modelid AND analysableid $notinsql", $params);
567 }
568 }
569
ed12ba6b
DM
570 /**
571 * Default system backend.
572 *
573 * @return string
574 */
575 public static function default_mlbackend() {
576 return self::DEFAULT_MLBACKEND;
577 }
578
369389c9
DM
579 /**
580 * Returns the provided element classes in the site.
581 *
582 * @param string $element
583 * @return string[] Array keys are the FQCN and the values the class path.
584 */
585 private static function get_analytics_classes($element) {
586
587 // Just in case...
1611308b 588 $element = clean_param($element, PARAM_ALPHANUMEXT);
369389c9 589
206d7aa9
DM
590 // Core analytics classes (analytics subsystem should not contain uses of the analytics API).
591 $classes = \core_component::get_component_classes_in_namespace('core', 'analytics\\' . $element);
592
593 // Plugins.
369389c9
DM
594 foreach (\core_component::get_plugin_types() as $type => $unusedplugintypepath) {
595 foreach (\core_component::get_plugin_list($type) as $pluginname => $unusedpluginpath) {
596 $frankenstyle = $type . '_' . $pluginname;
597 $classes += \core_component::get_component_classes_in_namespace($frankenstyle, 'analytics\\' . $element);
598 }
599 }
1cc2b4ba 600
206d7aa9 601 // Core subsystems.
1cc2b4ba
DM
602 foreach (\core_component::get_core_subsystems() as $subsystemname => $unusedsubsystempath) {
603 $componentname = 'core_' . $subsystemname;
604 $classes += \core_component::get_component_classes_in_namespace($componentname, 'analytics\\' . $element);
605 }
606
369389c9
DM
607 return $classes;
608 }
f0584045
DM
609
610 /**
611 * Return the list of models declared by the given component.
612 *
613 * @param string $componentname The name of the component to load models for.
614 * @throws \coding_exception Exception thrown in case of invalid syntax.
615 * @return array The $models description array.
616 */
617 public static function load_default_models_for_component(string $componentname): array {
618
619 $dir = \core_component::get_component_directory($componentname);
620
621 if (!$dir) {
622 // This is either an invalid component, or a core subsystem without its own root directory.
623 return [];
624 }
625
626 $file = $dir . '/' . self::ANALYTICS_FILENAME;
627
628 if (!is_readable($file)) {
629 return [];
630 }
631
632 $models = null;
633 include($file);
634
635 if (!isset($models) || !is_array($models) || empty($models)) {
636 return [];
637 }
638
639 foreach ($models as &$model) {
640 if (!isset($model['enabled'])) {
641 $model['enabled'] = false;
642 } else {
643 $model['enabled'] = clean_param($model['enabled'], PARAM_BOOL);
644 }
645 }
646
647 static::validate_models_declaration($models);
648
649 return $models;
650 }
651
652 /**
653 * Validate the declaration of prediction models according the syntax expected in the component's db folder.
654 *
655 * The expected structure looks like this:
656 *
657 * [
658 * [
659 * 'target' => '\fully\qualified\name\of\the\target\class',
660 * 'indicators' => [
661 * '\fully\qualified\name\of\the\first\indicator',
662 * '\fully\qualified\name\of\the\second\indicator',
663 * ],
664 * 'timesplitting' => '\optional\name\of\the\time_splitting\class',
665 * 'enabled' => true,
666 * ],
667 * ];
668 *
669 * @param array $models List of declared models.
670 * @throws \coding_exception Exception thrown in case of invalid syntax.
671 */
672 public static function validate_models_declaration(array $models) {
673
674 foreach ($models as $model) {
675 if (!isset($model['target'])) {
676 throw new \coding_exception('Missing target declaration');
677 }
678
679 if (!static::is_valid($model['target'], '\core_analytics\local\target\base')) {
680 throw new \coding_exception('Invalid target classname', $model['target']);
681 }
682
683 if (empty($model['indicators']) || !is_array($model['indicators'])) {
684 throw new \coding_exception('Missing indicators declaration');
685 }
686
687 foreach ($model['indicators'] as $indicator) {
688 if (!static::is_valid($indicator, '\core_analytics\local\indicator\base')) {
689 throw new \coding_exception('Invalid indicator classname', $indicator);
690 }
691 }
692
693 if (isset($model['timesplitting'])) {
694 if (substr($model['timesplitting'], 0, 1) !== '\\') {
695 throw new \coding_exception('Expecting fully qualified time splitting classname', $model['timesplitting']);
696 }
697 if (!static::is_valid($model['timesplitting'], '\core_analytics\local\time_splitting\base')) {
698 throw new \coding_exception('Invalid time splitting classname', $model['timesplitting']);
699 }
700 }
701
702 if (!empty($model['enabled']) && !isset($model['timesplitting'])) {
703 throw new \coding_exception('Cannot enable a model without time splitting method specified');
704 }
705 }
706 }
6187213f
DM
707
708 /**
709 * Create the defined model.
710 *
711 * @param array $definition See {@link self::validate_models_declaration()} for the syntax.
712 * @return \core_analytics\model
713 */
714 public static function create_declared_model(array $definition): \core_analytics\model {
715
716 $target = static::get_target($definition['target']);
717
718 $indicators = [];
719
720 foreach ($definition['indicators'] as $indicatorname) {
721 $indicator = static::get_indicator($indicatorname);
722 $indicators[$indicator->get_id()] = $indicator;
723 }
724
725 if (isset($definition['timesplitting'])) {
726 $timesplitting = $definition['timesplitting'];
727 } else {
728 $timesplitting = false;
729 }
730
731 $created = \core_analytics\model::create($target, $indicators, $timesplitting);
732
733 if (!empty($definition['enabled'])) {
734 $created->enable();
735 }
736
737 return $created;
738 }
369389c9 739}