Passed
Pull Request — master (#521)
by Alexander
06:25 queued 02:59
created

CompareHandler::prepareNumber()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
cc 2
dl 0
loc 7
rs 10
c 0
b 0
f 0
eloc 3
nc 2
nop 1
ccs 0
cts 0
cp 0
crap 6
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Validator\Rule;
6
7
use Stringable;
8
use Yiisoft\Validator\Exception\UnexpectedRuleException;
9
use Yiisoft\Validator\Result;
10
use Yiisoft\Validator\RuleHandlerInterface;
11
use Yiisoft\Validator\ValidationContext;
12
13
use function gettype;
14
use function in_array;
15
16
/**
17
 * Compares the specified value with another value.
18
 *
19
 * @see AbstractCompare
20
 * @see Equal
21
 * @see GreaterThan
22
 * @see GreaterThanOrEqual
23
 * @see LessThan
24
 * @see LessThanOrEqual
25
 * @see Compare
26
 * @see NotEqual
27 84
 */
28
final class CompareHandler implements RuleHandlerInterface
29 84
{
30 1
    public function validate(mixed $value, object $rule, ValidationContext $context): Result
31
    {
32
        if (!$rule instanceof AbstractCompare) {
33 83
            throw new UnexpectedRuleException(AbstractCompare::class, $rule);
34 83
        }
35 4
36 4
        $result = new Result();
37 4
        if (!$this->isInputCorrect($rule, $value)) {
38
            return $result->addError($rule->getIncorrectInputMessage(), [
39
                'attribute' => $context->getTranslatedAttribute(),
40
                'type' => get_debug_type($value),
41 79
            ]);
42 79
        }
43
44 79
        $targetValue = $rule->getTargetValue();
45
        $targetAttribute = $rule->getTargetAttribute();
46 8
47 8
        if ($targetValue === null && $targetAttribute !== null) {
48 3
            /** @var mixed $targetValue */
49 3
            $targetValue = $context->getDataSet()->getAttributeValue($targetAttribute);
50
            if (!$this->isInputCorrect($rule, $targetValue)) {
51
                return $result->addError($rule->getIncorrectDataSetTypeMessage(), [
52
                    'type' => get_debug_type($targetValue),
53
                ]);
54 76
            }
55 34
        }
56
57
        if ($this->compareValues($rule->getOperator(), $rule->getType(), $value, $targetValue)) {
58 42
            return new Result();
59 42
        }
60 42
61 42
        return (new Result())->addError($rule->getMessage(), [
62
            'attribute' => $context->getTranslatedAttribute(),
63
            'targetValue' => $rule->getTargetValue(),
64
            'targetAttribute' => $targetAttribute,
65
            'targetValueOrAttribute' => isset($targetValue) ? $this->getFormattedValue($targetValue) : $targetAttribute,
66
            'value' => $this->getFormattedValue($value),
67
        ]);
68
    }
69
70
    private function isInputCorrect(AbstractCompare $rule, mixed $value)
71
    {
72
        return $rule->getType() !== CompareType::ORIGINAL ? $this->isValueSimple($value) : true;
73
    }
74
75
    private function isValueSimple(mixed $value): bool
76
    {
77 76
        return $value === null || is_scalar($value) || $value instanceof Stringable;
78
    }
79 76
80 2
    private function getFormattedValue(mixed $value): int|float|string|Stringable|bool|null
81 2
    {
82
        return $this->isValueSimple($value) ? $value : get_debug_type($value);
83 74
    }
84 74
85
    /**
86
     * Compares two values with the specified operator.
87 76
     *
88 17
     * @param string $operator The comparison operator. One of `==`, `===`, `!=`, `!==`, `>`, `>=`, `<`, `<=`.
89 10
     * @param string $type The type of the values being compared.
90 8
     * @psalm-param CompareType::ORIGINAL | CompareType::STRING | CompareType::NUMBER $type
91 6
     *
92 8
     * @param mixed $value The value being compared.
93 8
     * @param mixed $targetValue Another value being compared.
94 8
     *
95 76
     * @return bool Whether the result of comparison using the specified operator is true.
96
     */
97
    private function compareValues(string $operator, string $type, mixed $value, mixed $targetValue): bool
98
    {
99
        if (!in_array($operator, ['==', '===', '!=', '!=='])) {
100
            if ($type === CompareType::STRING) {
101
                $value = (string) $value;
102
                $targetValue = (string) $targetValue;
103
            } elseif ($type === CompareType::NUMBER) {
104
                $value = $this->prepareNumber($value);
105
                $targetValue = $this->prepareNumber($targetValue);
106
            }
107
        }
108
109
        return match ($operator) {
110
            '==' => $this->checkValuesAreEqual($type, $value, $targetValue),
111
            '===' => $this->checkValuesAreEqual($type, $value, $targetValue, strict: true),
112
            '!=' => !$this->checkValuesAreEqual($type, $value, $targetValue),
113
            '!==' => !$this->checkValuesAreEqual($type, $value, $targetValue, strict: true),
114
            '>' => $value > $targetValue,
115
            '>=' => $value >= $targetValue,
116
            '<' => $value < $targetValue,
117
            '<=' => $value <= $targetValue,
118
        };
119
    }
120
121
    private function checkValuesAreEqual(string $type, mixed $value, mixed $targetValue, bool $strict = false): bool
122
    {
123
        if ($type === CompareType::ORIGINAL) {
124
            return $strict ? $value === $targetValue : $value == $targetValue;
125
        }
126
127
        if ($strict && gettype($value) !== gettype($targetValue)) {
128
            return false;
129
        }
130
131
        return match ($type) {
132
            CompareType::STRING => (string) $value === (string) $targetValue,
133
            CompareType::NUMBER => $this->checkFloatsAreEqual(
134
                $this->prepareNumber($value),
135
                $this->prepareNumber($targetValue),
136
            ),
137
        };
138
    }
139
140
    private function checkFloatsAreEqual(float $value, float $targetValue): bool
141
    {
142
        return abs($value - $targetValue) < PHP_FLOAT_EPSILON;
143
    }
144
145
    private function prepareNumber(int|float|string|Stringable|bool|null $number): float
146
    {
147
        if ($number instanceof Stringable) {
148
            $number = (string) $number;
149
        }
150
151
        return (float) $number;
152
    }
153
}
154