Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
89.19% covered (warning)
89.19%
33 / 37
75.00% covered (warning)
75.00%
3 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
runner
89.19% covered (warning)
89.19%
33 / 37
75.00% covered (warning)
75.00%
3 / 4
11.15
0.00% covered (danger)
0.00%
0 / 1
 queue
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 current_depth
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 run
84.00% covered (warning)
84.00%
21 / 25
0.00% covered (danger)
0.00%
0 / 1
5.10
 trigger_user
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2// This file is part of Moodle - https://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 <https://www.gnu.org/licenses/>.
16
17namespace tool_autoflow\local;
18
19use tool_autoflow\local\engine\executor;
20use tool_autoflow\local\engine\graph;
21use tool_autoflow\local\engine\invalid_graph;
22use tool_autoflow\task\run_workflow;
23
24/**
25 * Queues workflow runs as ad hoc tasks and runs them.
26 *
27 * Every run carries a depth. A run started by an event that another run fired is
28 * one level deeper. Past MAX_DEPTH nothing is queued, so a workflow that triggers
29 * itself stops instead of looping, while ordinary chains of workflows still work.
30 *
31 * @package    tool_autoflow
32 * @copyright  2026 DevLMS <contact@devlms.com>
33 * @license    https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
34 */
35final class runner {
36    /** Deepest allowed chain of runs started by other runs. */
37    public const MAX_DEPTH = 3;
38
39    /** @var int|null Depth of the run executing in this process, or null. */
40    private static ?int $depth = null;
41
42    /**
43     * Queue a run.
44     *
45     * @param int $workflowid
46     * @param array $trigger Data about what started it.
47     * @param string $triggertype manual, schedule or event.
48     * @param int $depth 0 unless another run caused this one.
49     * @return bool False when the chain is already too deep.
50     */
51    public static function queue(int $workflowid, array $trigger, string $triggertype, int $depth = 0): bool {
52        global $DB;
53        if ($depth > self::MAX_DEPTH) {
54            return false;
55        }
56        $runas = $DB->get_field('tool_autoflow_workflow', 'runasuserid', ['id' => $workflowid]);
57        $task = new run_workflow();
58        $task->set_custom_data(['workflowid' => $workflowid, 'trigger' => $trigger, 'triggertype' => $triggertype,
59            'depth' => $depth]);
60        $task->set_userid($runas ?: get_admin()->id);
61        \core\task\manager::queue_adhoc_task($task);
62        return true;
63    }
64
65    /**
66     * Depth of the run executing now, or null when no run is executing.
67     *
68     * @return int|null
69     */
70    public static function current_depth(): ?int {
71        return self::$depth;
72    }
73
74    /**
75     * Run a workflow now and record the result.
76     *
77     * @param int $workflowid
78     * @param array $trigger
79     * @param string $triggertype
80     * @param int $depth
81     * @return \stdClass|null The run record, or null when the workflow is gone or disabled.
82     */
83    public static function run(int $workflowid, array $trigger, string $triggertype, int $depth): ?\stdClass {
84        global $DB;
85        $workflow = $DB->get_record('tool_autoflow_workflow', ['id' => $workflowid]);
86        if (!$workflow || (!$workflow->enabled && $triggertype !== 'manual')) {
87            return null;
88        }
89        $run = (object) [
90            'workflowid' => $workflowid,
91            'status' => 'running',
92            'triggertype' => $triggertype,
93            'userid' => self::trigger_user($trigger),
94            'timecreated' => time(),
95        ];
96        $run->id = $DB->insert_record('tool_autoflow_run', $run);
97
98        $previous = self::$depth;
99        self::$depth = $depth;
100        try {
101            $result = (new executor(node_catalogue::registry()))->run(graph::from_json($workflow->graph), $trigger);
102            $run->status = $result->status;
103            $run->log = json_encode($result->steps);
104            $run->error = $result->error;
105        } catch (invalid_graph $e) {
106            $run->status = 'failed';
107            $run->log = '[]';
108            $run->error = $e->getMessage();
109        } finally {
110            self::$depth = $previous;
111        }
112        $run->timefinished = time();
113        $DB->update_record('tool_autoflow_run', $run);
114        return $DB->get_record('tool_autoflow_run', ['id' => $run->id]);
115    }
116
117    /**
118     * The user a trigger is about, if any.
119     *
120     * @param array $trigger
121     * @return int|null
122     */
123    private static function trigger_user(array $trigger): ?int {
124        $userid = $trigger['relateduserid'] ?? $trigger['userid'] ?? null;
125        return $userid ? (int) $userid : null;
126    }
127}