Passed
Pull Request — master (#521)
by Alexander
05:26 queued 02:36
created

CompareHandler::compareValues()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 2

Importance

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