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

AbstractCompare::getTargetAttribute()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
eloc 1
c 1
b 1
f 0
nc 1
nop 0
dl 0
loc 3
cc 1
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Validator\Rule;
6
7
use Closure;
8
use InvalidArgumentException;
9
use Yiisoft\Validator\Rule\Trait\SkipOnEmptyTrait;
10
use Yiisoft\Validator\Rule\Trait\SkipOnErrorTrait;
11
use Yiisoft\Validator\Rule\Trait\WhenTrait;
12
use Yiisoft\Validator\RuleWithOptionsInterface;
13
use Yiisoft\Validator\SkipOnEmptyInterface;
14
use Yiisoft\Validator\SkipOnErrorInterface;
15
use Yiisoft\Validator\WhenInterface;
16
17
use function in_array;
18
19
/**
20
 * Abstract base for all the comparison validation rules.
21
 *
22
 * The value being compared with {@see AbstractCompare::$targetValue} or {@see AbstractCompare::$targetAttribute}, which
23
 * is set in the constructor.
24
 *
25
 * It supports different comparison operators, specified
26
 * via the {@see AbstractCompare::$operator}.
27
 *
28
 * The default comparison is based on string values, which means the values
29
 * are compared byte by byte. When comparing numbers, make sure to change {@see AbstractCompare::$type} to
30
 * {@see CompareType::NUMBER} to enable numeric comparison.
31
 *
32
 * @see CompareHandler
33
 *
34
 * @psalm-import-type WhenType from WhenInterface
35
 */
36
abstract class AbstractCompare implements
37
    RuleWithOptionsInterface,
38
    SkipOnEmptyInterface,
39
    SkipOnErrorInterface,
40
    WhenInterface
41
{
42
    use SkipOnEmptyTrait;
43
    use SkipOnErrorTrait;
44
    use WhenTrait;
45
46
    /**
47
     * A default for {@see $incorrectInputMessage}.
48
     */
49
    protected const DEFAULT_INCORRECT_INPUT_MESSAGE = 'The allowed types are integer, float, string, boolean and null.';
50
    /**
51
     * A default for {@see $incorrectDataSetTypeMessage}.
52
     */
53
    protected const DEFAULT_INCORRECT_DATA_SET_TYPE_MESSAGE = 'The attribute value returned from a custom data set ' .
54
    'must have a scalar type.';
55
56
    /**
57
     * @var array Map of valid operators.
58
     * It's used instead of a list for better performance.
59
     */
60
    private array $validOperatorsMap = [
61
        '==' => 1,
62
        '===' => 1,
63
        '!=' => 1,
64
        '!==' => 1,
65
        '>' => 1,
66
        '>=' => 1,
67
        '<' => 1,
68
        '<=' => 1,
69
    ];
70
71
    /**
72
     * @param scalar|null $targetValue The constant value to be compared with. When both this property and
73
     * {@see $targetAttribute} are set, this property takes precedence.
74
     * @param string|null $targetAttribute The name of the attribute to be compared with. When both this property and
75
     * {@see $targetValue} are set, the {@see $targetValue} takes precedence.
76
     * @param string $incorrectInputMessage A message used when the input is incorrect.
77
     *
78
     * You may use the following placeholders in the message:
79
     *
80
     * - `{attribute}`: the translated label of the attribute being validated.
81
     * - `{type}`: the type of the value being validated.
82
     * @param string $incorrectDataSetTypeMessage A message used when the value returned from a custom
83
     * data set is not scalar.
84
     *
85
     * You may use the following placeholders in the message:
86
     *
87
     * - `{type}`: type of the value.
88
     * @param string|null $message A message used when the value is not valid.
89
     *
90
     * You may use the following placeholders in the message:
91
     *
92
     * - `{attribute}`: the translated label of the attribute being validated.
93
     * - `{targetValue}`: the constant value to be compared with.
94
     * - `{targetAttribute}`: the name of the attribute to be compared with.
95
     * - `{targetValueOrAttribute}`: the constant value to be compared with or, if it's absent, the name of
96
     *   the attribute to be compared with.
97
     * - `{value}`: the value of the attribute being validated.
98
     * @param string $type The type of the values being compared. Either {@see CompareType::STRING}
99
     * or {@see CompareType::NUMBER}.
100
     * @psalm-param CompareType::STRING | CompareType::NUMBER $type
101
     *
102
     * @param string $operator The operator for comparison. The following operators are supported:
103
     *
104
     * - `==`: check if two values are equal. The comparison is done in non-strict mode.
105
     * - `===`: check if two values are equal. The comparison is done in strict mode.
106
     * - `!=`: check if two values are NOT equal. The comparison is done in non-strict mode.
107
     * - `!==`: check if two values are NOT equal. The comparison is done in strict mode.
108
     * - `>`: check if value being validated is greater than the value being compared with.
109
     * - `>=`: check if value being validated is greater than or equal to the value being compared with.
110
     * - `<`: check if value being validated is less than the value being compared with.
111
     * - `<=`: check if value being validated is less than or equal to the value being compared with.
112
     *
113
     * When you want to compare numbers, make sure to also change {@see $type} to {@see TYPE_NUMBER}.
114
     * @param bool|callable|null $skipOnEmpty Whether to skip this rule if the value validated is empty.
115
     * See {@see SkipOnEmptyInterface}.
116
     * @param bool $skipOnError Whether to skip this rule if any of the previous rules gave an error.
117
     * See {@see SkipOnErrorInterface}.
118
     * @param Closure|null $when A callable to define a condition for applying the rule.
119
     * See {@see WhenInterface}.
120
     * @psalm-param WhenType $when
121
     */
122
    public function __construct(
123
        private int|float|string|bool|null $targetValue = null,
124
        private string|null $targetAttribute = null,
125
        private string $incorrectInputMessage = self::DEFAULT_INCORRECT_INPUT_MESSAGE,
126
        private string $incorrectDataSetTypeMessage = self::DEFAULT_INCORRECT_DATA_SET_TYPE_MESSAGE,
127
        private string|null $message = null,
128
        private string $type = CompareType::STRING,
129
        private string $operator = '==',
130
        private mixed $skipOnEmpty = null,
131
        private bool $skipOnError = false,
132
        private Closure|null $when = null,
133
    ) {
134
        $validTypes = [CompareType::STRING, CompareType::NUMBER];
135
        if (!in_array($this->type, $validTypes)) {
136
            $validTypesString = $this->getQuotedList($validTypes);
137
            $message = "Type \"$this->type\" is not supported. The valid types are: $validTypesString.";
138
139
            throw new InvalidArgumentException($message);
140
        }
141
142
        if (!isset($this->validOperatorsMap[$this->operator])) {
143
            /** @var string[] $validOperators */
144
            $validOperators = array_keys($this->validOperatorsMap);
145
            $validOperatorsString = $this->getQuotedList($validOperators);
146
            $message = "Operator \"$operator\" is not supported. The valid operators are: $validOperatorsString.";
147
148
            throw new InvalidArgumentException($message);
149
        }
150
    }
151
152
    /**
153
     * Get the constant value to be compared with.
154
     *
155
     * @return scalar|null Value to be compared with or `null` if it was not configured.
156
     *
157
     * @see $targetValue
158
     */
159
    public function getTargetValue(): int|float|string|bool|null
160
    {
161
        return $this->targetValue;
162
    }
163
164
    /**
165
     * Get the name of the attribute to be compared with.
166
     *
167
     * @return string|null Name of the attribute to be compared with or `null` if it was not configured.
168
     *
169
     * @see $targetAttribute
170
     */
171
    public function getTargetAttribute(): string|null
172
    {
173
        return $this->targetAttribute;
174
    }
175
176
    /**
177
     * Get the type of the values being compared.
178
     *
179
     * @return string The type of the values being compared. Either {@see CompareType::STRING}
180
     * or {@see CompareType::NUMBER}.
181
     * @psalm-return CompareType::STRING | CompareType::NUMBER $type
182
     *
183
     * @see $type
184
     */
185
    public function getType(): string
186
    {
187
        return $this->type;
188
    }
189
190
    /**
191
     * Get the operator for comparison.
192
     *
193
     * @return string The operator for comparison.
194
     *
195
     * @see $operator
196
     */
197
    public function getOperator(): string
198
    {
199
        return $this->operator;
200
    }
201
202
    /**
203
     * Get message used when the input is incorrect.
204
     *
205
     * @return string Error message.
206
     *
207
     * @see $incorrectInputMessage
208
     */
209
    public function getIncorrectInputMessage(): string
210
    {
211
        return $this->incorrectInputMessage;
212
    }
213
214
    /**
215
     * Get message used when the value returned from a custom
216
     * data set s not scalar.
217
     *
218
     * @return string Error message.
219
     *
220
     * @see $incorrectDataSetTypeMessage
221
     */
222
    public function getIncorrectDataSetTypeMessage(): string
223
    {
224
        return $this->incorrectDataSetTypeMessage;
225
    }
226
227
    /**
228
     * Get a message used when the value is not valid.
229
     *
230
     * @return string Error message.
231
     *
232
     * @see $message
233
     */
234
    public function getMessage(): string
235
    {
236
        return $this->message ?? match ($this->operator) {
237
            '==', '===' => 'Value must be equal to "{targetValueOrAttribute}".',
238
            '!=', '!==' => 'Value must not be equal to "{targetValueOrAttribute}".',
239
            '>' => 'Value must be greater than "{targetValueOrAttribute}".',
240
            '>=' => 'Value must be greater than or equal to "{targetValueOrAttribute}".',
241
            '<' => 'Value must be less than "{targetValueOrAttribute}".',
242
            '<=' => 'Value must be less than or equal to "{targetValueOrAttribute}".',
243
        };
244
    }
245
246
    public function getOptions(): array
247
    {
248
        $messageParameters = [
249
            'targetValue' => $this->targetValue,
250
            'targetAttribute' => $this->targetAttribute,
251
            'targetValueOrAttribute' => $this->targetValue ?? $this->targetAttribute,
252
        ];
253
254
        return [
255
            'targetValue' => $this->targetValue,
256
            'targetAttribute' => $this->targetAttribute,
257
            'incorrectInputMessage' => [
258
                'template' => $this->incorrectInputMessage,
259
                'parameters' => $messageParameters,
260
            ],
261
            'incorrectDataSetTypeMessage' => [
262
                'template' => $this->incorrectDataSetTypeMessage,
263
                'parameters' => $messageParameters,
264
            ],
265
            'message' => [
266
                'template' => $this->getMessage(),
267
                'parameters' => $messageParameters,
268
            ],
269
            'type' => $this->type,
270
            'operator' => $this->operator,
271
            'skipOnEmpty' => $this->getSkipOnEmptyOption(),
272
            'skipOnError' => $this->skipOnError,
273
        ];
274
    }
275
276
    public function getHandler(): string
277
    {
278
        return CompareHandler::class;
279
    }
280
281
    /**
282
     * Formats list of strings as a single string where items separated by the comma and each item is wrapped with
283
     * double quotes.
284
     *
285
     * For example, for `['item1', 'item2']` list, the output will be `"item1", "item2"`.
286
     *
287
     * @param string[] $items Initial list of strings to format.
288
     *
289
     * @return string Resulting formatted string.
290
     */
291
    private function getQuotedList(array $items): string
292
    {
293
        $wrapInQuotesCallable = static fn (string $item): string => '"' . $item . '"';
294
295
        return implode(', ', array_map($wrapInQuotesCallable, $items));
296
    }
297
}
298