Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
2 / 2
CRAP
100.00% covered (success)
100.00%
1 / 1
executor
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
2 / 2
6
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 run
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
1 / 1
5
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
17declare(strict_types=1);
18
19namespace tool_autoflow\local\engine;
20
21/**
22 * Runs a workflow graph from its trigger.
23 *
24 * Nodes run in the order they are reached, breadth first. A node reached by two
25 * paths runs once per arrival, the same as n8n and Zapier paths. A step limit
26 * stops graphs that fan out too far.
27 *
28 * @package    tool_autoflow
29 * @copyright  2026 DevLMS <contact@devlms.com>
30 * @license    https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
31 */
32final class executor {
33    /**
34     * Create an executor.
35     *
36     * @param registry $registry Node types the run may use.
37     * @param int $maxsteps Most node runs allowed in one workflow run.
38     */
39    public function __construct(
40        /** @var registry Node types. */
41        private readonly registry $registry,
42        /** @var int Step limit. */
43        private readonly int $maxsteps = 200,
44    ) {
45    }
46
47    /**
48     * Run the graph.
49     *
50     * @param graph $graph
51     * @param array $trigger Data about what started the run, readable as {{trigger.*}}.
52     * @return run_result
53     */
54    public function run(graph $graph, array $trigger): run_result {
55        $context = ['trigger' => $trigger, 'nodes' => [], 'vars' => []];
56        $errors = $graph->errors();
57        if ($errors) {
58            return new run_result(
59                'failed',
60                [],
61                $context,
62                'The workflow is not valid: ' . implode(', ', array_column($errors, 'code')) . '.'
63            );
64        }
65
66        $steps = [];
67        $queue = [$graph->trigger()['id']];
68        while ($queue) {
69            if (count($steps) === $this->maxsteps) {
70                return new run_result('failed', $steps, $context, "Stopped after {$this->maxsteps} steps.");
71            }
72            $node = $graph->node(array_shift($queue));
73            $step = ['node' => $node['id'], 'type' => $node['type']];
74            try {
75                $params = template::resolve($node['data']['params'] ?? [], $context);
76                $result = $this->registry->get($node['type'])->execute($params, $context);
77            } catch (node_failed $e) {
78                $steps[] = $step + ['status' => 'error', 'branch' => null, 'output' => [], 'error' => $e->getMessage()];
79                return new run_result('failed', $steps, $context, $e->getMessage());
80            }
81            $context['nodes'][$node['id']] = $result->output;
82            $context['vars'] = array_merge($context['vars'], $result->vars);
83            $steps[] = $step + ['status' => 'ok', 'branch' => $result->branch, 'output' => $result->output];
84            array_push($queue, ...$graph->next($node['id'], $result->branch));
85        }
86        return new run_result('success', $steps, $context);
87    }
88}