Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.22% covered (success)
97.22%
35 / 36
87.50% covered (warning)
87.50%
7 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
condition_node
97.22% covered (success)
97.22%
35 / 36
87.50% covered (warning)
87.50%
7 / 8
27
0.00% covered (danger)
0.00%
0 / 1
 type
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 params
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 branches
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 execute
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
12
 compare
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 contains
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 is_empty
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
3
 scalar
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
4.13
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\nodes;
20
21use tool_autoflow\local\engine\node_failed;
22use tool_autoflow\local\engine\node_result;
23use tool_autoflow\local\engine\node_type;
24use tool_autoflow\local\engine\param;
25
26/**
27 * Compares two values and follows the "true" or "false" branch.
28 *
29 * Two numbers compare as numbers, so "10" is greater than "9". Anything else
30 * compares as text, case sensitive. true and false read as the words, null as "".
31 *
32 * @package    tool_autoflow
33 * @copyright  2026 DevLMS <contact@devlms.com>
34 * @license    https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
35 */
36final class condition_node implements node_type {
37    /**
38     * Type name.
39     *
40     * @return string
41     */
42    public function type(): string {
43        return 'logic.condition';
44    }
45
46    /**
47     * Parameters.
48     *
49     * @return param[]
50     */
51    public function params(): array {
52        return [
53            new param('left', 'text', true),
54            new param('operator', 'select', true, ['eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'contains', 'empty', 'notempty']),
55            new param('right', 'text', false),
56        ];
57    }
58
59    /**
60     * Branches.
61     *
62     * @return string[]
63     */
64    public function branches(): array {
65        return ['true', 'false'];
66    }
67
68    /**
69     * Compare left and right with the operator.
70     *
71     * @param array $params left, operator, right
72     * @param array $context
73     * @return node_result
74     */
75    public function execute(array $params, array $context): node_result {
76        $left = $params['left'] ?? null;
77        $right = $params['right'] ?? null;
78        $operator = $params['operator'] ?? '';
79        $result = match ($operator) {
80            'eq' => self::compare($left, $right) === 0,
81            'neq' => self::compare($left, $right) !== 0,
82            'gt' => self::compare($left, $right) > 0,
83            'gte' => self::compare($left, $right) >= 0,
84            'lt' => self::compare($left, $right) < 0,
85            'lte' => self::compare($left, $right) <= 0,
86            'contains' => self::contains($left, $right),
87            'empty' => self::is_empty($left),
88            'notempty' => !self::is_empty($left),
89            default => throw new node_failed("Unknown condition operator \"$operator\"."),
90        };
91        return new node_result(['result' => $result], $result ? 'true' : 'false');
92    }
93
94    /**
95     * Compare as numbers when both are numeric, otherwise as text.
96     *
97     * @param mixed $a
98     * @param mixed $b
99     * @return int Negative, zero or positive.
100     */
101    private static function compare($a, $b): int {
102        // On two strings PHP 8 compares as numbers when both are numeric, and as text otherwise.
103        return self::scalar($a) <=> self::scalar($b);
104    }
105
106    /**
107     * Whether text contains a piece, or a list contains a value.
108     *
109     * @param mixed $haystack
110     * @param mixed $needle
111     * @return bool
112     */
113    private static function contains($haystack, $needle): bool {
114        if (is_array($haystack)) {
115            foreach ($haystack as $item) {
116                if (self::compare($item, $needle) === 0) {
117                    return true;
118                }
119            }
120            return false;
121        }
122        return str_contains(self::scalar($haystack), self::scalar($needle));
123    }
124
125    /**
126     * Empty means null, empty text or an empty list. Zero is a value, not empty.
127     *
128     * @param mixed $value
129     * @return bool
130     */
131    private static function is_empty($value): bool {
132        return $value === null || $value === '' || $value === [];
133    }
134
135    /**
136     * A value as text for comparing.
137     *
138     * @param mixed $value
139     * @return string
140     */
141    private static function scalar($value): string {
142        if (is_bool($value)) {
143            return $value ? 'true' : 'false';
144        }
145        if (is_array($value)) {
146            return json_encode($value);
147        }
148        return (string) $value;
149    }
150}