Passed
Pull Request — master (#542)
by Alexander
04:53 queued 02:24
created

AbstractCompare::getQuotedList()   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 1
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 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 string[] List of valid types.
58
     *
59
     * @see CompareType
60
     */
61
    private static array $validTypes = [CompareType::STRING, CompareType::NUMBER];
62
    /**
63
     * @var array Map of valid operators.
64
     * It's used instead of a list for better performance.
65
     * @psalm-var array<string, 1>
66
     */
67
    private static array $validOperatorsMap = [
68
        '==' => 1,
69
        '===' => 1,
70
        '!=' => 1,
71
        '!==' => 1,
72
        '>' => 1,
73
        '>=' => 1,
74
        '<' => 1,
75
        '<=' => 1,
76
    ];
77
78
    /**
79
     * @param scalar|null $targetValue The constant value to be compared with. When both this property and
80
     * {@see $targetAttribute} are set, this property takes precedence.
81
     * @param string|null $targetAttribute The name of the attribute to be compared with. When both this property and
82
     * {@see $targetValue} are set, the {@see $targetValue} takes precedence.
83
     * @param string $incorrectInputMessage A message used when the input is incorrect.
84
     *
85
     * You may use the following placeholders in the message:
86
     *
87
     * - `{attribute}`: the translated label of the attribute being validated.
88
     * - `{type}`: the type of the value being validated.
89
     * @param string $incorrectDataSetTypeMessage A message used when the value returned from a custom
90
     * data set is not scalar.
91
     *
92
     * You may use the following placeholders in the message:
93
     *
94
     * - `{type}`: type of the value.
95
     * @param string|null $message A message used when the value is not valid.
96
     *
97
     * You may use the following placeholders in the message:
98
     *
99
     * - `{attribute}`: the translated label of the attribute being validated.
100
     * - `{targetValue}`: the constant value to be compared with.
101
     * - `{targetAttribute}`: the name of the attribute to be compared with.
102
     * - `{targetValueOrAttribute}`: the constant value to be compared with or, if it's absent, the name of
103
     *   the attribute to be compared with.
104
     * - `{value}`: the value of the attribute being validated.
105
     * @param string $type The type of the values being compared. Either {@see CompareType::STRING}
106
     * or {@see CompareType::NUMBER}.
107
     * @psalm-param CompareType::STRING | CompareType::NUMBER $type
108
     *
109
     * @param string $operator The operator for comparison. The following operators are supported:
110
     *
111
     * - `==`: check if two values are equal. The comparison is done in non-strict mode.
112
     * - `===`: check if two values are equal. The comparison is done in strict mode.
113
     * - `!=`: check if two values are NOT equal. The comparison is done in non-strict mode.
114
     * - `!==`: check if two values are NOT equal. The comparison is done in strict mode.
115
     * - `>`: check if value being validated is greater than the value being compared with.
116
     * - `>=`: check if value being validated is greater than or equal to the value being compared with.
117
     * - `<`: check if value being validated is less than the value being compared with.
118
     * - `<=`: check if value being validated is less than or equal to the value being compared with.
119
     *
120
     * When you want to compare numbers, make sure to also change {@see $type} to {@see TYPE_NUMBER}.
121
     * @param bool|callable|null $skipOnEmpty Whether to skip this rule if the value validated is empty.
122
     * See {@see SkipOnEmptyInterface}.
123
     * @param bool $skipOnError Whether to skip this rule if any of the previous rules gave an error.
124
     * See {@see SkipOnErrorInterface}.
125
     * @param Closure|null $when A callable to define a condition for applying the rule.
126
     * See {@see WhenInterface}.
127
     * @psalm-param WhenType $when
128
     */
129
    public function __construct(
130
        private int|float|string|bool|null $targetValue = null,
131
        private string|null $targetAttribute = null,
132
        private string $incorrectInputMessage = self::DEFAULT_INCORRECT_INPUT_MESSAGE,
133
        private string $incorrectDataSetTypeMessage = self::DEFAULT_INCORRECT_DATA_SET_TYPE_MESSAGE,
134
        private string|null $message = null,
135
        private string $type = CompareType::STRING,
136
        private string $operator = '==',
137
        private mixed $skipOnEmpty = null,
138
        private bool $skipOnError = false,
139
        private Closure|null $when = null,
140
    ) {
141
        if (!in_array($this->type, self::$validTypes, true)) {
142
            $validTypesString = $this->getQuotedList(self::$validTypes);
143
            $message = "Type \"$this->type\" is not supported. The valid types are: $validTypesString.";
144
145
            throw new InvalidArgumentException($message);
146
        }
147
148
        if (!isset(self::$validOperatorsMap[$this->operator])) {
149
            $validOperators = array_keys(self::$validOperatorsMap);
150
            $validOperatorsString = $this->getQuotedList($validOperators);
151
            $message = "Operator \"$operator\" is not supported. The valid operators are: $validOperatorsString.";
152
153
            throw new InvalidArgumentException($message);
154
        }
155
    }
156
157
    /**
158
     * Get the constant value to be compared with.
159
     *
160
     * @return scalar|null Value to be compared with or `null` if it was not configured.
161
     *
162
     * @see $targetValue
163
     */
164
    public function getTargetValue(): int|float|string|bool|null
165
    {
166
        return $this->targetValue;
167
    }
168
169
    /**
170
     * Get the name of the attribute to be compared with.
171
     *
172
     * @return string|null Name of the attribute to be compared with or `null` if it was not configured.
173
     *
174
     * @see $targetAttribute
175
     */
176
    public function getTargetAttribute(): string|null
177
    {
178
        return $this->targetAttribute;
179
    }
180
181
    /**
182
     * Get the type of the values being compared.
183
     *
184
     * @return string The type of the values being compared. Either {@see CompareType::STRING}
185
     * or {@see CompareType::NUMBER}.
186
     * @psalm-return CompareType::STRING | CompareType::NUMBER $type
187
     *
188
     * @see $type
189
     */
190
    public function getType(): string
191
    {
192
        return $this->type;
193
    }
194
195
    /**
196
     * Get the operator for comparison.
197
     *
198
     * @return string The operator for comparison.
199
     *
200
     * @see $operator
201
     */
202
    public function getOperator(): string
203
    {
204
        return $this->operator;
205
    }
206
207
    /**
208
     * Get message used when the input is incorrect.
209
     *
210
     * @return string Error message.
211
     *
212
     * @see $incorrectInputMessage
213
     */
214
    public function getIncorrectInputMessage(): string
215
    {
216
        return $this->incorrectInputMessage;
217
    }
218
219
    /**
220
     * Get message used when the value returned from a custom
221
     * data set s not scalar.
222
     *
223
     * @return string Error message.
224
     *
225
     * @see $incorrectDataSetTypeMessage
226
     */
227
    public function getIncorrectDataSetTypeMessage(): string
228
    {
229
        return $this->incorrectDataSetTypeMessage;
230
    }
231
232
    /**
233
     * Get a message used when the value is not valid.
234
     *
235
     * @return string Error message.
236
     *
237
     * @see $message
238
     */
239
    public function getMessage(): string
240
    {
241
        return $this->message ?? match ($this->operator) {
242
            '==', '===' => 'Value must be equal to "{targetValueOrAttribute}".',
243
            '!=', '!==' => 'Value must not be equal to "{targetValueOrAttribute}".',
244
            '>' => 'Value must be greater than "{targetValueOrAttribute}".',
245
            '>=' => 'Value must be greater than or equal to "{targetValueOrAttribute}".',
246
            '<' => 'Value must be less than "{targetValueOrAttribute}".',
247
            '<=' => 'Value must be less than or equal to "{targetValueOrAttribute}".',
248
        };
249
    }
250
251
    public function getOptions(): array
252
    {
253
        $messageParameters = [
254
            'targetValue' => $this->targetValue,
255
            'targetAttribute' => $this->targetAttribute,
256
            'targetValueOrAttribute' => $this->targetValue ?? $this->targetAttribute,
257
        ];
258
259
        return [
260
            'targetValue' => $this->targetValue,
261
            'targetAttribute' => $this->targetAttribute,
262
            'incorrectInputMessage' => [
263
                'template' => $this->incorrectInputMessage,
264
                'parameters' => $messageParameters,
265
            ],
266
            'incorrectDataSetTypeMessage' => [
267
                'template' => $this->incorrectDataSetTypeMessage,
268
                'parameters' => $messageParameters,
269
            ],
270
            'message' => [
271
                'template' => $this->getMessage(),
272
                'parameters' => $messageParameters,
273
            ],
274
            'type' => $this->type,
275
            'operator' => $this->operator,
276
            'skipOnEmpty' => $this->getSkipOnEmptyOption(),
277
            'skipOnError' => $this->skipOnError,
278
        ];
279
    }
280
281
    public function getHandler(): string
282
    {
283
        return CompareHandler::class;
284
    }
285
286
    /**
287
     * Formats list of strings as a single string where items separated by the comma and each item is wrapped with
288
     * double quotes.
289
     *
290
     * For example, for `['item1', 'item2']` list, the output will be `"item1", "item2"`.
291
     *
292
     * @param string[] $items Initial list of strings to format.
293
     *
294
     * @return string Resulting formatted string.
295
     */
296
    private function getQuotedList(array $items): string
297
    {
298
        return '"' . implode('", "', $items) . '"';
299
    }
300
}
301