Passed
Push — master ( e05223...e18482 )
by
unknown
06:52 queued 03:38
created

AbstractCompare::getName()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
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 Closure;
8
use DateTimeInterface;
9
use InvalidArgumentException;
10
use Stringable;
11
use Yiisoft\Validator\Rule\Trait\SkipOnEmptyTrait;
12
use Yiisoft\Validator\Rule\Trait\SkipOnErrorTrait;
13
use Yiisoft\Validator\Rule\Trait\WhenTrait;
14
use Yiisoft\Validator\DumpedRuleInterface;
15
use Yiisoft\Validator\SkipOnEmptyInterface;
16
use Yiisoft\Validator\SkipOnErrorInterface;
17
use Yiisoft\Validator\WhenInterface;
18
19
use function in_array;
20
21
/**
22
 * Abstract base for all the comparison validation rules.
23
 *
24
 * The validated value is compared with {@see AbstractCompare::$targetValue} or
25
 * {@see AbstractCompare::$targetAttribute} value of validated data set.
26
 *
27
 * The default comparison is based on number values (including float values). It's also possible to compare values as
28
 * strings byte by byte and compare original values as is. See {@see AbstractCompare::$type} for all possible options.
29
 *
30
 * It supports different comparison operators, specified via the {@see AbstractCompare::$operator}.
31
 *
32
 * @see CompareHandler
33
 *
34
 * @psalm-import-type WhenType from WhenInterface
35
 */
36
abstract class AbstractCompare implements
37
    DumpedRuleInterface,
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, null ' .
50
    'and object implementing \Stringable interface or \DateTimeInterface.';
51
    /**
52
     * A default for {@see $incorrectDataSetTypeMessage}.
53
     */
54
    protected const DEFAULT_INCORRECT_DATA_SET_TYPE_MESSAGE = 'The attribute value returned from a custom data set ' .
55
    'must have one of the following types: integer, float, string, boolean, null or an object implementing ' .
56
    '\Stringable interface or \DateTimeInterface.';
57
    /**
58
     * List of valid types.
59
     *
60
     * @see CompareType
61
     */
62
    private const VALID_TYPES = [CompareType::ORIGINAL, CompareType::STRING, CompareType::NUMBER];
63
    /**
64
     * Map of valid operators. It's used instead of a list for better performance.
65
     */
66
    private const VALID_OPERATORS_MAP = [
67
        '==' => 1,
68
        '===' => 1,
69
        '!=' => 1,
70
        '!==' => 1,
71
        '>' => 1,
72
        '>=' => 1,
73
        '<' => 1,
74
        '<=' => 1,
75
    ];
76
77
    /**
78
     * @param mixed $targetValue The value to be compared with. When both this property and {@see $targetAttribute} are
79
     * set, this property takes precedence.
80
     * @param string|null $targetAttribute The name of the attribute to be compared with. When both this property and
81
     * {@see $targetValue} are set, the {@see $targetValue} takes precedence.
82
     * @param string $incorrectInputMessage A message used when the input is incorrect.
83
     *
84
     * You may use the following placeholders in the message:
85
     *
86
     * - `{attribute}`: the translated label of the attribute being validated.
87
     * - `{type}`: the type of the value being validated.
88
     * @param string $incorrectDataSetTypeMessage A message used when the value returned from a custom
89
     * data set is neither scalar nor null.
90
     *
91
     * You may use the following placeholders in the message:
92
     *
93
     * - `{type}`: type of the value.
94
     * @param string|null $message A message used when the value is not valid.
95
     *
96
     * You may use the following placeholders in the message:
97
     *
98
     * - `{attribute}`: the translated label of the attribute being validated.
99
     * - `{targetValue}`: the value to be compared with.
100
     * - `{targetAttribute}`: the name of the attribute to be compared with.
101
     * - `{targetAttributeValue}`: the value extracted from the attribute to be compared with if this attribute was set.
102
     * - `{targetValueOrAttribute}`: the value to be compared with or, if it's absent, the name of the attribute to be
103
     * compared with.
104
     * - `{value}`: the value being validated.
105
     *
106
     * When {@see CompareType::ORIGINAL} is used with complex types (neither scalar nor `null`), `{targetValue}`,
107
     * `{targetAttributeValue}` and `{targetValueOrAttribute}` parameters might contain the actual type instead of the
108
     * value, e.g. "object" for predictable formatting.
109
     * @param string $type The type of the values being compared:
110
     *
111
     * - {@see CompareType::NUMBER} - default, both values will be converted to float numbers before comparison.
112
     * - {@see CompareType::ORIGINAL} - compare the values as is.
113
     * - {@see CompareType::STRING} - cast both values to strings before comparison.
114
     *
115
     * {@see CompareType::NUMBER} and {@see CompareType::STRING} allow only scalar and `null` values, also objects
116
     * implementing {@see Stringable} interface or {@see DateTimeInterface} (validated values must be in Unix Timestamp
117
     * format).
118
     *
119
     * {@see CompareType::ORIGINAL} allows any values. All PHP comparison rules apply here, see comparison operators -
120
     * {@see https://www.php.net/manual/en/language.operators.comparison.php} and PHP type comparison tables -
121
     * {@see https://www.php.net/manual/en/types.comparisons.php} sections in official PHP documentation.
122
     *
123
     * @psalm-param CompareType::ORIGINAL | CompareType::STRING | CompareType::NUMBER $type
124
     *
125
     * @param string $operator The operator for comparison. The following operators are supported:
126
     *
127
     * - `==`: check if two values are equal. The comparison is done in non-strict mode.
128
     * - `===`: check if two values are equal. The comparison is done in strict mode.
129
     * - `!=`: check if two values are NOT equal. The comparison is done in non-strict mode.
130
     * - `!==`: check if two values are NOT equal. The comparison is done in strict mode.
131
     * - `>`: check if value being validated is greater than the value being compared with.
132
     * - `>=`: check if value being validated is greater than or equal to the value being compared with.
133
     * - `<`: check if value being validated is less than the value being compared with.
134
     * - `<=`: check if value being validated is less than or equal to the value being compared with.
135
     * @param bool|callable|null $skipOnEmpty Whether to skip this rule if the value validated is empty.
136
     * See {@see SkipOnEmptyInterface}.
137
     * @param bool $skipOnError Whether to skip this rule if any of the previous rules gave an error.
138
     * See {@see SkipOnErrorInterface}.
139
     * @param Closure|null $when A callable to define a condition for applying the rule.
140
     * See {@see WhenInterface}.
141
     *
142
     * @psalm-param WhenType $when
143
     */
144
    public function __construct(
145
        private mixed $targetValue = null,
146
        private string|null $targetAttribute = null,
147
        private string $incorrectInputMessage = self::DEFAULT_INCORRECT_INPUT_MESSAGE,
148
        private string $incorrectDataSetTypeMessage = self::DEFAULT_INCORRECT_DATA_SET_TYPE_MESSAGE,
149
        private string|null $message = null,
150
        private string $type = CompareType::NUMBER,
151
        private string $operator = '==',
152
        private mixed $skipOnEmpty = null,
153
        private bool $skipOnError = false,
154
        private Closure|null $when = null,
155
    ) {
156
        if (!in_array($this->type, self::VALID_TYPES)) {
157
            $validTypesString = $this->getQuotedList(self::VALID_TYPES);
158
            $message = "Type \"$this->type\" is not supported. The valid types are: $validTypesString.";
159
160
            throw new InvalidArgumentException($message);
161
        }
162
163
        if (!isset(self::VALID_OPERATORS_MAP[$this->operator])) {
164
            $validOperators = array_keys(self::VALID_OPERATORS_MAP);
165
            $validOperatorsString = $this->getQuotedList($validOperators);
166
            $message = "Operator \"$operator\" is not supported. The valid operators are: $validOperatorsString.";
167
168
            throw new InvalidArgumentException($message);
169
        }
170
    }
171
172
    public function getName(): string
173
    {
174
        return static::class;
175
    }
176
177
    /**
178
     * Get value to be compared with.
179
     *
180
     * @return mixed Value to be compared with or `null` if it was not configured.
181
     *
182
     * @see $targetValue
183
     */
184
    public function getTargetValue(): mixed
185
    {
186
        return $this->targetValue;
187
    }
188
189
    /**
190
     * Get the name of the attribute to be compared with.
191
     *
192
     * @return string|null Name of the attribute to be compared with or `null` if it was not configured.
193
     *
194
     * @see $targetAttribute
195
     */
196
    public function getTargetAttribute(): string|null
197
    {
198
        return $this->targetAttribute;
199
    }
200
201
    /**
202
     * Get the type of the values being compared.
203
     *
204
     * @return string The type of the values being compared. Either {@see CompareType::STRING}
205
     * or {@see CompareType::NUMBER}.
206
     *
207
     * @psalm-return CompareType::ORIGINAL | CompareType::STRING | CompareType::NUMBER $type
208
     *
209
     * @see $type
210
     */
211
    public function getType(): string
212
    {
213
        return $this->type;
214
    }
215
216
    /**
217
     * Get the operator for comparison.
218
     *
219
     * @return string The operator for comparison.
220
     *
221
     * @see $operator
222
     */
223
    public function getOperator(): string
224
    {
225
        return $this->operator;
226
    }
227
228
    /**
229
     * Get message used when the input is incorrect.
230
     *
231
     * @return string Error message.
232
     *
233
     * @see $incorrectInputMessage
234
     */
235
    public function getIncorrectInputMessage(): string
236
    {
237
        return $this->incorrectInputMessage;
238
    }
239
240
    /**
241
     * Get message used when the value returned from a custom
242
     * data set s not scalar.
243
     *
244
     * @return string Error message.
245
     *
246
     * @see $incorrectDataSetTypeMessage
247
     */
248
    public function getIncorrectDataSetTypeMessage(): string
249
    {
250
        return $this->incorrectDataSetTypeMessage;
251
    }
252
253
    /**
254
     * Get a message used when the value is not valid.
255
     *
256
     * @return string Error message.
257
     *
258
     * @see $message
259
     */
260
    public function getMessage(): string
261
    {
262
        return $this->message ?? match ($this->operator) {
263
            '==', => 'Value must be equal to "{targetValueOrAttribute}".',
264
            '===' => 'Value must be strictly equal to "{targetValueOrAttribute}".',
265
            '!=' => 'Value must not be equal to "{targetValueOrAttribute}".',
266
            '!==' => 'Value must not be strictly equal to "{targetValueOrAttribute}".',
267
            '>' => 'Value must be greater than "{targetValueOrAttribute}".',
268
            '>=' => 'Value must be greater than or equal to "{targetValueOrAttribute}".',
269
            '<' => 'Value must be less than "{targetValueOrAttribute}".',
270
            '<=' => 'Value must be less than or equal to "{targetValueOrAttribute}".',
271
        };
272
    }
273
274
    public function getOptions(): array
275
    {
276
        $isTargetValueSimple = $this->targetValue === null || is_scalar($this->targetValue);
277
278
        if (!$isTargetValueSimple) {
279
            $messageParameters = ['targetAttribute' => $this->targetAttribute];
280
        } else {
281
            $messageParameters = [
282
                'targetValue' => $this->targetValue,
283
                'targetAttribute' => $this->targetAttribute,
284
                'targetValueOrAttribute' => $this->targetAttribute ?? $this->targetValue,
285
            ];
286
        }
287
288
        $options = [
289
            'targetAttribute' => $this->targetAttribute,
290
            'incorrectInputMessage' => [
291
                'template' => $this->incorrectInputMessage,
292
                'parameters' => $messageParameters,
293
            ],
294
            'incorrectDataSetTypeMessage' => [
295
                'template' => $this->incorrectDataSetTypeMessage,
296
                'parameters' => $messageParameters,
297
            ],
298
            'message' => [
299
                'template' => $this->getMessage(),
300
                'parameters' => $messageParameters,
301
            ],
302
            'type' => $this->type,
303
            'operator' => $this->operator,
304
            'skipOnEmpty' => $this->getSkipOnEmptyOption(),
305
            'skipOnError' => $this->skipOnError,
306
        ];
307
        if (!$isTargetValueSimple) {
308
            return $options;
309
        }
310
311
        return array_merge(['targetValue' => $this->targetValue], $options);
312
    }
313
314
    public function getHandler(): string
315
    {
316
        return CompareHandler::class;
317
    }
318
319
    /**
320
     * Formats list of strings as a single string where items separated by the comma and each item is wrapped with
321
     * double quotes.
322
     *
323
     * For example, for `['item1', 'item2']` list, the output will be `"item1", "item2"`.
324
     *
325
     * @param string[] $items Initial list of strings to format.
326
     *
327
     * @return string Resulting formatted string.
328
     */
329
    private function getQuotedList(array $items): string
330
    {
331
        return '"' . implode('", "', $items) . '"';
332
    }
333
}
334