Passed
Pull Request — master (#219)
by
unknown
02:48
created

CompareTo::asNumber()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 0
dl 0
loc 6
ccs 4
cts 4
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Validator\Rule;
6
7
use Attribute;
8
use InvalidArgumentException;
9
use RuntimeException;
10
use Yiisoft\Validator\FormatterInterface;
11
use Yiisoft\Validator\Result;
12
use Yiisoft\Validator\Rule;
13
use Yiisoft\Validator\ValidationContext;
14
15
/**
16
 * Compares the specified value with another value.
17
 *
18
 * The value being compared with a constant {@see CompareTo::$compareValue}, which is set
19
 * in the constructor.
20
 *
21
 * It supports different comparison operators, specified
22
 * via the {@see CompareTo::$operator}.
23
 *
24
 * The default comparison function is based on string values, which means the values
25
 * are compared byte by byte. When comparing numbers, make sure to change {@see CompareTo::$type} to
26
 * {@see CompareTo::TYPE_NUMBER} to enable numeric comparison.
27
 */
28
#[Attribute(Attribute::TARGET_PROPERTY)]
29
final class CompareTo extends Rule
30
{
31
    /**
32
     * Constant for specifying the comparison as string values.
33
     * No conversion will be done before comparison.
34
     *
35
     * @see $type
36
     */
37
    public const TYPE_STRING = 'string';
38
    /**
39
     * Constant for specifying the comparison as numeric values.
40
     * String values will be converted into numbers before comparison.
41
     *
42
     * @see $type
43
     */
44
    public const TYPE_NUMBER = 'number';
45
    private array $validOperators = [
46
        '==' => 1,
47
        '===' => 1,
48
        '!=' => 1,
49
        '!==' => 1,
50
        '>' => 1,
51
        '>=' => 1,
52
        '<' => 1,
53
        '<=' => 1,
54
    ];
55
56 38
    public function __construct(
57
        /**
58
         * @var mixed the constant value to be compared with.
59
         */
60
        private mixed $compareValue,
61
        /**
62
         * @var string|null user-defined error message
63
         */
64
        private ?string $message = null,
65
        /**
66
         * @var string the type of the values being compared.
67
         */
68
        private string $type = self::TYPE_STRING,
69
        /**
70
         * @var string the operator for comparison. The following operators are supported:
71
         *
72
         * - `==`: check if two values are equal. The comparison is done is non-strict mode.
73
         * - `===`: check if two values are equal. The comparison is done is strict mode.
74
         * - `!=`: check if two values are NOT equal. The comparison is done is non-strict mode.
75
         * - `!==`: check if two values are NOT equal. The comparison is done is strict mode.
76
         * - `>`: check if value being validated is greater than the value being compared with.
77
         * - `>=`: check if value being validated is greater than or equal to the value being compared with.
78
         * - `<`: check if value being validated is less than the value being compared with.
79
         * - `<=`: check if value being validated is less than or equal to the value being compared with.
80
         *
81
         * When you want to compare numbers, make sure to also chabge @see CompareTo::$type} to
82
         * {@see CompareTo::TYPE_NUMBER}.
83
         */
84
        private string $operator = '==',
85
        ?FormatterInterface $formatter = null,
86
        bool $skipOnEmpty = false,
87
        bool $skipOnError = false,
88
        $when = null
89
    ) {
90 38
        parent::__construct(formatter: $formatter, skipOnEmpty: $skipOnEmpty, skipOnError: $skipOnError, when: $when);
91 38
        $this->checkOperator($operator);
92 37
        $this->checkType($this->type);
93
    }
94
95 38
    private function checkOperator($value): void
96
    {
97 38
        if (!isset($this->validOperators[$value])) {
98 2
            throw new InvalidArgumentException("Operator \"$value\" is not supported.");
99
        }
100
    }
101
102 37
    private function checkType($value): void
103
    {
104 37
        if ($value !== self::TYPE_NUMBER && $value !== self::TYPE_STRING) {
105 1
            throw new InvalidArgumentException("Type \"$value\" is not supported.");
106
        }
107
    }
108
109
    /**
110
     * @see $compareValue
111
     */
112 1
    public function compareValue(mixed $value): self
113
    {
114 1
        $new = clone $this;
115 1
        $new->compareValue = $value;
116
117 1
        return $new;
118
    }
119
120
    /**
121
     * @see $message
122
     */
123 1
    public function message(string $value): self
124
    {
125 1
        $new = clone $this;
126 1
        $new->message = $value;
127
128 1
        return $new;
129
    }
130
131
    /**
132
     * @see $type
133
     */
134 1
    public function asString(): self
135
    {
136 1
        $new = clone $this;
137 1
        $new->type = self::TYPE_STRING;
138
139 1
        return $new;
140
    }
141
142
    /**
143
     * @see $type
144
     */
145 1
    public function asNumber(): self
146
    {
147 1
        $new = clone $this;
148 1
        $new->type = self::TYPE_NUMBER;
149
150 1
        return $new;
151
    }
152
153
    /**
154
     * @see $operator
155
     */
156 2
    public function operator(string $value): self
157
    {
158 2
        $this->checkOperator($value);
159
160 1
        $new = clone $this;
161 1
        $new->operator = $value;
162
163 1
        return $new;
164
    }
165
166 26
    private function getMessage(): string
167
    {
168 26
        if ($this->message !== null) {
169 2
            return $this->message;
170
        }
171
172 24
        switch ($this->operator) {
173 24
            case '==':
174 16
            case '===':
175 10
                return 'Value must be equal to "{value}".';
176 14
            case '!=':
177 11
            case '!==':
178 7
                return 'Value must not be equal to "{value}".';
179 7
            case '>':
180 2
                return 'Value must be greater than "{value}".';
181 5
            case '>=':
182 2
                return 'Value must be greater than or equal to "{value}".';
183 3
            case '<':
184 2
                return 'Value must be less than "{value}".';
185 1
            case '<=':
186 1
                return 'Value must be less than or equal to "{value}".';
187
            default:
188
                throw new RuntimeException("Unknown operator: {$this->operator}");
189
        }
190
    }
191
192 29
    protected function validateValue($value, ?ValidationContext $context = null): Result
193
    {
194 29
        $result = new Result();
195
196 29
        if (!$this->compareValues($this->operator, $this->type, $value, $this->compareValue)) {
197 14
            $message = $this->formatMessage($this->getMessage(), ['value' => $this->compareValue]);
198 14
            $result->addError($message);
199
        }
200
201 29
        return $result;
202
    }
203
204
    /**
205
     * Compares two values with the specified operator.
206
     *
207
     * @param string $operator the comparison operator
208
     * @param string $type the type of the values being compared
209
     * @param mixed $value the value being compared
210
     * @param mixed $compareValue another value being compared
211
     *
212
     * @return bool whether the comparison using the specified operator is true.
213
     */
214 29
    protected function compareValues(string $operator, string $type, $value, $compareValue): bool
215
    {
216 29
        if ($type === self::TYPE_NUMBER) {
217
            $value = (float) $value;
218
            $compareValue = (float)$compareValue;
219
        } else {
220 29
            $value = (string) $value;
221 29
            $compareValue = (string) $compareValue;
222
        }
223 29
        switch ($operator) {
224 29
            case '==':
225 4
                return $value == $compareValue;
226 25
            case '===':
227 4
                return $value === $compareValue;
228 21
            case '!=':
229 5
                return $value != $compareValue;
230 16
            case '!==':
231 4
                return $value !== $compareValue;
232 12
            case '>':
233 3
                return $value > $compareValue;
234 9
            case '>=':
235 3
                return $value >= $compareValue;
236 6
            case '<':
237 3
                return $value < $compareValue;
238 3
            case '<=':
239 3
                return $value <= $compareValue;
240
            default:
241
                return false;
242
        }
243
    }
244
245 12
    public function getOptions(): array
246
    {
247 12
        return array_merge(parent::getOptions(), [
248 12
            'compareValue' => $this->compareValue,
249 12
            'message' => $this->formatMessage($this->getMessage(), ['value' => $this->compareValue]),
250 12
            'type' => $this->type,
251 12
            'operator' => $this->operator,
252
        ]);
253
    }
254
}
255