Issues (138)

tests/Rule/CompareTest.php (2 issues)

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Validator\Tests\Rule;
6
7
use DateTime;
8
use InvalidArgumentException;
9
use RuntimeException;
10
use stdClass;
11
use Stringable;
12
use Yiisoft\Validator\DataSetInterface;
13
use Yiisoft\Validator\DataWrapperInterface;
14
use Yiisoft\Validator\Rule\Compare;
15
use Yiisoft\Validator\Rule\CompareType;
16
use Yiisoft\Validator\RuleInterface;
17
use Yiisoft\Validator\Tests\Rule\Base\RuleTestCase;
18
use Yiisoft\Validator\Tests\Rule\Base\RuleWithOptionsTestTrait;
19
use Yiisoft\Validator\Tests\Rule\Base\SkipOnErrorTestTrait;
20
use Yiisoft\Validator\Tests\Rule\Base\WhenTestTrait;
0 ignored issues
show
Header blocks must not contain blank lines
Loading history...
21
22
use Yiisoft\Validator\Tests\Support\Data\CompareObject;
23
24
use function is_string;
25
26
final class CompareTest extends RuleTestCase
27
{
28
    use RuleWithOptionsTestTrait;
29
    use SkipOnErrorTestTrait;
30
    use WhenTestTrait;
31
32
    public function testInitWithWrongType(): void
33
    {
34
        $this->expectException(InvalidArgumentException::class);
35
        $message = 'Type "float" is not supported. The valid types are: "original", "string", "number".';
36
        $this->expectExceptionMessage($message);
37
38
        new Compare(type: 'float');
39
    }
40
41
    public function testInitWithWrongOperator(): void
42
    {
43
        $this->expectException(InvalidArgumentException::class);
44
        $message = 'Operator "=" is not supported. The valid operators are: "==", "===", "!=", "!==", ">", ">=", ' .
45
            '"<", "<=".';
46
        $this->expectExceptionMessage($message);
47
48
        new Compare(1, operator: '=');
49
    }
50
51
    public function testGetName(): void
52
    {
53
        $rule = new Compare();
54
        $this->assertSame(Compare::class, $rule->getName());
55
    }
56
57
    public function dataOptions(): array
58
    {
59
        return [
60
            [
61
                new Compare(1),
62
                [
63
                    'targetValue' => 1,
64
                    'targetAttribute' => null,
65
                    'incorrectInputMessage' => [
66
                        'template' => 'The allowed types are integer, float, string, boolean, null and object ' .
67
                            'implementing \Stringable interface or \DateTimeInterface.',
68
                        'parameters' => [
69
                            'targetValue' => 1,
70
                            'targetAttribute' => null,
71
                            'targetValueOrAttribute' => 1,
72
                        ],
73
                    ],
74
                    'incorrectDataSetTypeMessage' => [
75
                        'template' => 'The attribute value returned from a custom data set must have one of the ' .
76
                            'following types: integer, float, string, boolean, null or an object implementing ' .
77
                            '\Stringable interface or \DateTimeInterface.',
78
                        'parameters' => [
79
                            'targetValue' => 1,
80
                            'targetAttribute' => null,
81
                            'targetValueOrAttribute' => 1,
82
                        ],
83
                    ],
84
                    'message' => [
85
                        'template' => 'Value must be equal to "{targetValueOrAttribute}".',
86
                        'parameters' => [
87
                            'targetValue' => 1,
88
                            'targetAttribute' => null,
89
                            'targetValueOrAttribute' => 1,
90
                        ],
91
                    ],
92
                    'type' => 'number',
93
                    'operator' => '==',
94
                    'skipOnEmpty' => false,
95
                    'skipOnError' => false,
96
                ],
97
            ],
98
            [
99
                new Compare(
100
                    new DateTime('2023-02-07 12:57:12'),
101
                    targetAttribute: 'test',
102
                    incorrectInputMessage: 'Custom message 1.',
103
                    incorrectDataSetTypeMessage: 'Custom message 2.',
104
                    message: 'Custom message 3.',
105
                    type: CompareType::ORIGINAL,
106
                    operator: '>=',
107
                    skipOnEmpty: true,
108
                    skipOnError: true,
109
                    when: static fn (): bool => true,
110
                ),
111
                [
112
                    'targetAttribute' => 'test',
113
                    'incorrectInputMessage' => [
114
                        'template' => 'Custom message 1.',
115
                        'parameters' => [
116
                            'targetAttribute' => 'test',
117
                        ],
118
                    ],
119
                    'incorrectDataSetTypeMessage' => [
120
                        'template' => 'Custom message 2.',
121
                        'parameters' => [
122
                            'targetAttribute' => 'test',
123
                        ],
124
                    ],
125
                    'message' => [
126
                        'template' => 'Custom message 3.',
127
                        'parameters' => [
128
                            'targetAttribute' => 'test',
129
                        ],
130
                    ],
131
                    'type' => 'original',
132
                    'operator' => '>=',
133
                    'skipOnEmpty' => true,
134
                    'skipOnError' => true,
135
                ],
136
            ],
137
            'targetAttribute priority with simple targetValue' => [
138
                new Compare(
139
                    1,
140
                    targetAttribute: 'test',
141
                ),
142
                [
143
                    'targetValue' => 1,
144
                    'targetAttribute' => 'test',
145
                    'incorrectInputMessage' => [
146
                        'template' => 'The allowed types are integer, float, string, boolean, null and object ' .
147
                            'implementing \Stringable interface or \DateTimeInterface.',
148
                        'parameters' => [
149
                            'targetValue' => 1,
150
                            'targetAttribute' => 'test',
151
                            'targetValueOrAttribute' => 'test',
152
                        ],
153
                    ],
154
                    'incorrectDataSetTypeMessage' => [
155
                        'template' => 'The attribute value returned from a custom data set must have one of the ' .
156
                            'following types: integer, float, string, boolean, null or an object implementing ' .
157
                            '\Stringable interface or \DateTimeInterface.',
158
                        'parameters' => [
159
                            'targetValue' => 1,
160
                            'targetAttribute' => 'test',
161
                            'targetValueOrAttribute' => 'test',
162
                        ],
163
                    ],
164
                    'message' => [
165
                        'template' => 'Value must be equal to "{targetValueOrAttribute}".',
166
                        'parameters' => [
167
                            'targetValue' => 1,
168
                            'targetAttribute' => 'test',
169
                            'targetValueOrAttribute' => 'test',
170
                        ],
171
                    ],
172
                    'type' => 'number',
173
                    'operator' => '==',
174
                    'skipOnEmpty' => false,
175
                    'skipOnError' => false,
176
                ],
177
            ],
178
        ];
179
    }
180
181
    public function dataValidationPassed(): array
182
    {
183
        $targetStringableFloat = new class () implements Stringable {
184
            public function __toString(): string
185
            {
186
                return '100.5';
187
            }
188
        };
189
        $stringableFloat = new class () implements Stringable {
190
            public function __toString(): string
191
            {
192
                return '100.50';
193
            }
194
        };
195
        $targetStringableUuid = new class () implements Stringable {
196
            public function __toString(): string
197
            {
198
                return '3b98a689-7d49-48bb-8741-7e27f220b69a';
199
            }
200
        };
201
        $stringableUuid = new class () implements Stringable {
202
            public function __toString(): string
203
            {
204
                return 'd62f2b3f-707f-451a-8819-046ff8436a4f';
205
            }
206
        };
207
        $dateTime = new DateTime('2023-02-07 12:57:12');
208
        $object = new CompareObject(a: 1, b: 2);
209
        $objectWithDifferentPropertyType = new CompareObject(a: 1, b: '2');
210
        $array = [1, 2];
211
212
        return [
213
            // Number / string specific, expressions
214
215
            'target value: float, value: float with the same value as expression result, type: number, operator: ==' => [
216
                1 - 0.83,
217
                [new Compare(0.17)],
218
            ],
219
            'target value: float, value: float with the same value as expression result, type: number, operator: ===' => [
220
                1 - 0.83,
221
                [new Compare(0.17, operator: '===')],
222
            ],
223
            'target value: float, value: float with the same value as expression result, type: number, operator: >=' => [
224
                1 - 0.83,
225
                [new Compare(0.17, operator: '>=')],
226
            ],
227
            'target value: float as expression result, value: float with the same value, type: number, operator: >=' => [
228
                0.17,
229
                [new Compare(1 - 0.83, operator: '>=')],
230
            ],
231
            'target value: float, value: float with the same value as expression result, type: number, operator: <=' => [
232
                1 - 0.83,
233
                [new Compare(0.17, operator: '<=')],
234
            ],
235
            'target value: float as expression result, value: float with the same value, type: number, operator: <=' => [
236
                0.17,
237
                [new Compare(1 - 0.83, operator: '<=')],
238
            ],
239
            'target value: float, value: float with the same value as expression result, type: string, operator: ==' => [
240
                1 - 0.83,
241
                [new Compare(0.17, type: CompareType::STRING)],
242
            ],
243
244
            // Number / original specific, decimal places, directly provided values
245
246
            'target value: string float, value: string float with the same value, but extra decimal place (0), type: number, operator: ==' => [
247
                '100.50',
248
                [new Compare('100.5')],
249
            ],
250
            'target value: float, value: string float with the same value, but extra decimal place (0), type: number, operator: ==' => [
251
                '100.50',
252
                [new Compare(100.5)],
253
            ],
254
            'target value: string float, value: string float with the same value, but extra decimal place (0), type: number, operator: ===' => [
255
                '100.50',
256
                [new Compare('100.5', operator: '===')],
257
            ],
258
            'target value: string float, value: string float with the same value, but extra decimal place (0), type: original, operator: ==' => [
259
                '100.50',
260
                [new Compare('100.5', type: CompareType::ORIGINAL)],
261
            ],
262
263
            // Number / original specific, decimal places, values provided via stringable objects
264
265
            'target value: stringable float, value: stringable float with the same value, but extra decimal place (0), type: number, operator: ==' => [
266
                $stringableFloat,
267
                [new Compare($targetStringableFloat)],
268
            ],
269
            'target value: stringable float, value: stringable float with the same value, but extra decimal place (0), type: number, operator: >=' => [
270
                $stringableFloat,
271
                [new Compare($targetStringableFloat, operator: '>=')],
272
            ],
273
274
            // String / original specific, character order, directly provided values
275
276
            'target value: uuidv4, value: greater uuidv4, type: string, operator: >' => [
277
                'd62f2b3f-707f-451a-8819-046ff8436a4f',
278
                [new Compare('3b98a689-7d49-48bb-8741-7e27f220b69a', type: CompareType::STRING, operator: '>')],
279
            ],
280
            'target value: character, value: character located further within alphabet, type: string, operator: >' => [
281
                'b',
282
                [new Compare('a', type: CompareType::STRING, operator: '>')],
283
            ],
284
            'target value: character, value: character located further within alphabet, type: original, operator: >' => [
285
                'b',
286
                [new Compare('a', type: CompareType::ORIGINAL, operator: '>')],
287
            ],
288
289
            // String specific, character order, values provided via stringable objects
290
291
            'target value: stringable uuidv4, value: greater stringable uuidv4, type: string, operator: >' => [
292
                $stringableUuid,
293
                [new Compare($targetStringableUuid, type: CompareType::STRING, operator: '>')],
294
            ],
295
            'target value: stringable uuidv4, value: greater stringable uuidv4, type: string, operator: >=' => [
296
                $stringableUuid,
297
                [new Compare($targetStringableUuid, type: CompareType::STRING, operator: '>=')],
298
            ],
299
300
            // Original specific, datetime
301
302
            'target value: DateTime object, value: DateTime object with the same value, type: original, operator: ==' => [
303
                new DateTime('2023-02-07 12:57:12'),
304
                [new Compare(new DateTime('2023-02-07 12:57:12'), type: CompareType::ORIGINAL)],
305
            ],
306
            'target value: DateTime object, value: the same DateTime object, type: original, operator: ===' => [
307
                $dateTime,
308
                [new Compare($dateTime, type: CompareType::ORIGINAL)],
309
            ],
310
            'target value: DateTime object, value: DateTime object with the same value, type: original, operator: !==' => [
311
                new DateTime('2023-02-07 12:57:12'),
312
                [new Compare(new DateTime('2023-02-07 12:57:12'), type: CompareType::ORIGINAL, operator: '!==')],
313
            ],
314
            'target value: DateTime object, value: DateTime object with the same value, type: original, operator: >=' => [
315
                new DateTime('2023-02-07 12:57:12'),
316
                [new Compare(new DateTime('2023-02-07 12:57:12'), type: CompareType::ORIGINAL, operator: '>=')],
317
            ],
318
            'target value: human-readable DateTime object, value: greater DateTime object, type: original, operator: >' => [
319
                new DateTime('2022-06-03'),
320
                [new Compare(new DateTime('June 2nd, 2022'), type: CompareType::ORIGINAL, operator: '>')],
321
            ],
322
323
            // Number / string specific, DateTime object and Unix Timestamp
324
325
            'target value: Unix Timestamp string, value: DateTime object with the same value, type: number, operator: ==' => [
326
                $dateTime->format('U'),
327
                [new Compare($dateTime)],
328
            ],
329
            'target value: Unix Timestamp string, value: DateTime object with the same value, type: string, operator: ==' => [
330
                $dateTime->format('U'),
331
                [new Compare($dateTime, type: CompareType::STRING)],
332
            ],
333
            'target value: Unix Timestamp string, value: DateTime object with the same value, type: number, operator: >=' => [
334
                $dateTime->format('U'),
335
                [new Compare($dateTime, operator: '>=')],
336
            ],
337
            'target value: Unix Timestamp string, value: DateTime object with the same value, type: string, operator: >=' => [
338
                $dateTime->format('U'),
339
                [new Compare($dateTime, type: CompareType::STRING, operator: '>=')],
340
            ],
341
342
            // Original specific, objects
343
344
            'target value: object, value: similar object in a different instance, type: original, operator: ==' => [
345
                new stdClass(),
346
                [new Compare(new stdClass(), type: CompareType::ORIGINAL)],
347
            ],
348
            'target value: object, value: the same object, type: original, operator: ===' => [
349
                $object,
350
                [new Compare($object, type: CompareType::ORIGINAL, operator: '===')],
351
            ],
352
            'target value: object, value: similar object but with different property type, type: original, operator: ===' => [
353
                $objectWithDifferentPropertyType,
354
                [new Compare($object, type: CompareType::ORIGINAL)],
355
            ],
356
357
            // Original specific, arrays
358
359
            'target value: array, value: similar array declared separately, type: original, operator: ==' => [
360
                [1, 2],
361
                [new Compare([1, 2], type: CompareType::ORIGINAL)],
362
            ],
363
            'target value: array, value: similar array declared separately, type: original, operator: ===' => [
364
                [1, 2],
365
                [new Compare([1, 2], type: CompareType::ORIGINAL, operator: '===')],
366
            ],
367
            'target value: array, value: similar array but with different item type, type: original, operator: ==' => [
368
                [1, 2],
369
                [new Compare([1, '2'], type: CompareType::ORIGINAL)],
370
            ],
371
            'target value: array, value: the same array, type: original, operator: ===' => [
372
                $array,
373
                [new Compare($array, type: CompareType::ORIGINAL)],
374
            ],
375
        ];
376
    }
377
378
    public function dataValidationPassedWithDifferentTypes(): array
379
    {
380
        $customDataSet = new class () implements DataSetInterface {
381
            public function getAttributeValue(string $attribute): mixed
382
            {
383
                return 100;
384
            }
385
386
            public function getData(): ?array
387
            {
388
                return null;
389
            }
390
391
            public function hasAttribute(string $attribute): bool
392
            {
393
                return true;
394
            }
395
        };
396
        $subFloatFromInt = static fn(int $value1, float $value2): int => $value1 - (int) $value2;
397
        $initialData = [
398
            // Basic
399
400
            'target value: integer, value: integer with the same value, type: number, operator: ==' => [
401
                100,
402
                [new Compare(100)],
403
            ],
404
            'target value: integer, value: integer with the same value, type: number, operator: ===' => [
405
                100,
406
                [new Compare(100, operator: '===')],
407
            ],
408
            'target value: integer, value: lower integer, type: number, operator: !=' => [
409
                99,
410
                [new Compare(100, operator: '!=')],
411
            ],
412
            'target value: integer, value: greater integer, type: number, operator: !=' => [
413
                101,
414
                [new Compare(100, operator: '!=')],
415
            ],
416
            'target value: integer, value: lower integer, type: number, operator: !==' => [
417
                101,
418
                [new Compare(100, operator: '!==')],
419
            ],
420
            'target value: integer, value: greater integer, type: number, operator: !==' => [
421
                101,
422
                [new Compare(100, operator: '!==')],
423
            ],
424
            'target value: integer, value: greater integer, type: number, operator: >' => [
425
                101,
426
                [new Compare(100, operator: '>')],
427
            ],
428
            'target value: integer, value: integer with the same value, type: number, operator: >=' => [
429
                100,
430
                [new Compare(100, operator: '>=')],
431
            ],
432
            'target value: integer, value: greater integer, type: number, operator: >=' => [
433
                101,
434
                [new Compare(100, operator: '>=')],
435
            ],
436
            'target value: integer, value: lower integer, type: number, operator: <' => [
437
                99,
438
                [new Compare(100, operator: '<')],
439
            ],
440
            'target value: integer, value: integer with the same value, type: number, operator: <=' => [
441
                100,
442
                [new Compare(100, operator: '<=')],
443
            ],
444
            'target value: integer, value: lower integer, type: number, operator: <=' => [
445
                99,
446
                [new Compare(100, operator: '<=')],
447
            ],
448
449
            // Boolean
450
451
            'target value: boolean (false), value: boolean (true), type: number, operator: >=' => [
452
                true,
453
                [new Compare(false, operator: '>=')],
454
            ],
455
456
            // Different types for non-strict equality
457
458
            'target value: empty string, value: null, type: number, operator: ==' => [
459
                null,
460
                [new Compare('')],
461
            ],
462
            'target value: integer, value: string integer with the same value, type: number, operator: ==' => [
463
                '100',
464
                [new Compare(100)],
465
            ],
466
467
            // Different types for non-strict inequality
468
469
            'target value: integer, value: float, type: number, operator: !=' => [
470
                100.00001,
471
                [new Compare(100, operator: '!=')],
472
            ],
473
            'target value: integer, value: boolean, type: number, operator: !=' => [
474
                false,
475
                [new Compare(100, operator: '!=')],
476
            ],
477
478
            // Different types for strict inequality
479
480
            'target value: integer, value: boolean, type: number, operator: !==' => [
481
                false,
482
                [new Compare(100, operator: '!==')],
483
            ],
484
            'target value: integer, value: string integer with the same value, type: number, operator: !==' => [
485
                '100',
486
                [new Compare(100, operator: '!==')],
487
            ],
488
            'target value: integer, value: float with the same value, but extra decimal place (0), type: number, operator: !==' => [
489
                100.0,
490
                [new Compare(100, operator: '!==')],
491
            ],
492
493
            // Large integers
494
495
            'target value: string with large integer, value: string with the same integer, type: number, operator: ===' => [
496
                PHP_INT_MAX . '0',
497
                [new Compare(PHP_INT_MAX . '0', operator: '===')],
498
            ],
499
            'target value: string with large integer, value: string with greater integer, type: number, operator: >' => [
500
                PHP_INT_MAX . '0',
501
                [new Compare('-' . PHP_INT_MAX . '12', operator: '>')],
502
            ],
503
            'target value: large integer in scientific notation, value: greater integer, type: number, operator: ===' => [
504
                4.5e19,
505
                [new Compare(4.5e19, operator: '===')],
506
            ],
507
            'target value: large integer in scientific notation, value: greater integer, type: number, operator: >' => [
508
                4.5e20,
509
                [new Compare(-4.5e19, operator: '>')],
510
            ],
511
            'target value: integer, value: the same integer as expression result, type: number, operator: ===' => [
512
                $subFloatFromInt(1_234_567_890, 1_234_567_890),
0 ignored issues
show
A parse error occurred: Syntax error, unexpected T_STRING, expecting ',' or ')' on line 512 at column 34
Loading history...
513
                [new Compare(0, operator: '===')],
514
            ],
515
516
            // Target attribute
517
518
            'target attribute: array key, target attribute value: integer, attribute value: integer with the same value, type: number, operator: ==' => [
519
                ['attribute' => 100, 'number' => 100],
520
                ['number' => new Compare(targetAttribute: 'attribute')],
521
            ],
522
            'target attribute: array key, target attribute value: integer, attribute value: lower integer, type: number, operator: <=' => [
523
                ['attribute' => 100, 'number' => 99],
524
                ['number' => new Compare(targetAttribute: 'attribute', operator: '<=')],
525
            ],
526
            'target attribute: object property, target attribute value: integer, attribute value: integer with the same value, type: number, operator: ==' => [
527
                new class () {
528
                    public int $attribute = 100;
529
                    public int $number = 100;
530
                },
531
                ['number' => new Compare(targetAttribute: 'attribute', operator: '<=')],
532
            ],
533
            'target attribute: custom data set attribute, target attribute value: integer, attribute value: integer with the same value, type: number, operator: ==' => [
534
                $customDataSet,
535
                ['number' => new Compare(targetAttribute: 'attribute', operator: '<=')],
536
            ],
537
        ];
538
539
        return $this->extendDataWithDifferentTypes($initialData);
540
    }
541
542
    /**
543
     * @dataProvider dataValidationPassed
544
     * @dataProvider dataValidationPassedWithDifferentTypes
545
     */
546
    public function testValidationPassed(mixed $data, ?array $rules = null): void
547
    {
548
        parent::testValidationPassed($data, $rules);
549
    }
550
551
    public function dataValidationFailed(): array
552
    {
553
        $incorrectDataSet = new class () implements DataWrapperInterface {
554
            public function getAttributeValue(string $attribute): mixed
555
            {
556
                return new stdClass();
557
            }
558
559
            public function getData(): ?array
560
            {
561
                return null;
562
            }
563
564
            public function getSource(): mixed
565
            {
566
                return false;
567
            }
568
569
            public function hasAttribute(string $attribute): bool
570
            {
571
                return false;
572
            }
573
        };
574
        $targetStringableFloat = new class () implements Stringable {
575
            public function __toString(): string
576
            {
577
                return '100.5';
578
            }
579
        };
580
        $stringableFloat = new class () implements Stringable {
581
            public function __toString(): string
582
            {
583
                return '100.50';
584
            }
585
        };
586
        $targetStringableUuid = new class () implements Stringable {
587
            public function __toString(): string
588
            {
589
                return '3b98a689-7d49-48bb-8741-7e27f220b69a';
590
            }
591
        };
592
        $stringableUuid = new class () implements Stringable {
593
            public function __toString(): string
594
            {
595
                return 'd62f2b3f-707f-451a-8819-046ff8436a4f';
596
            }
597
        };
598
        $dateTime = new DateTime('2023-02-07 12:57:12');
599
        $object = new CompareObject(a: 1, b: 2);
600
        $objectWithDifferentPropertyValue = new CompareObject(a: 1, b: 3);
601
        $objectWithDifferentPropertyType = new CompareObject(a: 1, b: '2');
602
        $array = [1, 2];
603
        $reversedArray = [2, 1];
604
605
        return [
606
            // Incorrect input
607
608
            'incorrect input' => [
609
                [],
610
                [new Compare(false)],
611
                [
612
                    '' => [
613
                        'The allowed types are integer, float, string, boolean, null and object implementing ' .
614
                        '\Stringable interface or \DateTimeInterface.',
615
                    ],
616
                ],
617
            ],
618
            'custom incorrect input message' => [
619
                [],
620
                [new Compare(false, incorrectInputMessage: 'Custom incorrect input message.')],
621
                ['' => ['Custom incorrect input message.']],
622
            ],
623
            'custom incorrect input message with parameters' => [
624
                [],
625
                [new Compare(false, incorrectInputMessage: 'Attribute - {attribute}, type - {type}.')],
626
                ['' => ['Attribute - , type - array.']],
627
            ],
628
            'custom incorrect input message with parameters, attribute set' => [
629
                ['data' => []],
630
                ['data' => new Compare(false, incorrectInputMessage: 'Attribute - {attribute}, type - {type}.')],
631
                ['data' => ['Attribute - data, type - array.']],
632
            ],
633
634
            // Incorrect data set input
635
636
            'incorrect data set type' => [
637
                $incorrectDataSet,
638
                [new Compare(targetAttribute: 'test')],
639
                [
640
                    '' => [
641
                        'The attribute value returned from a custom data set must have one of the following types: ' .
642
                            'integer, float, string, boolean, null or an object implementing \Stringable interface ' .
643
                            'or \DateTimeInterface.',
644
                    ],
645
                ],
646
            ],
647
            'custom incorrect data set type message' => [
648
                $incorrectDataSet,
649
                [
650
                    new Compare(
651
                        targetAttribute: 'test',
652
                        incorrectDataSetTypeMessage: 'Custom incorrect data set type message.',
653
                    ),
654
                ],
655
                ['' => ['Custom incorrect data set type message.']],
656
            ],
657
            'custom incorrect data set type message with parameters' => [
658
                $incorrectDataSet,
659
                [
660
                    new Compare(
661
                        targetAttribute: 'test',
662
                        incorrectDataSetTypeMessage: 'Type - {type}.',
663
                    ),
664
                ],
665
                ['' => ['Type - stdClass.']],
666
            ],
667
668
            // Custom message
669
670
            'custom message' => [101, [new Compare(100, message: 'Custom message.')], ['' => ['Custom message.']]],
671
            'custom message with parameters, target value set' => [
672
                101,
673
                [
674
                    new Compare(
675
                        100,
676
                        message: 'Attribute - {attribute}, target value - {targetValue}, target attribute - ' .
677
                        '{targetAttribute}, target value or attribute - {targetValueOrAttribute}, value - {value}.',
678
                    ),
679
                ],
680
                [
681
                    '' => [
682
                        'Attribute - , target value - 100, target attribute - , target value or attribute - 100, ' .
683
                        'value - 101.',
684
                    ],
685
                ],
686
            ],
687
            'custom message with parameters, attribute and target attribute set' => [
688
                ['attribute' => 100, 'number' => 101],
689
                [
690
                    'number' => new Compare(
691
                        targetAttribute: 'attribute',
692
                        message: 'Attribute - {attribute}, target value - {targetValue}, target attribute - ' .
693
                        '{targetAttribute}, target attribute value - {targetAttributeValue}, target value or ' .
694
                        'attribute - {targetValueOrAttribute}, value - {value}.',
695
                        operator: '===',
696
                    ),
697
                ],
698
                [
699
                    'number' => [
700
                        'Attribute - number, target value - , target attribute - attribute, target attribute value ' .
701
                        '- 100, target value or attribute - attribute, value - 101.',
702
                    ],
703
                ],
704
            ],
705
706
            // String / original specific, falsy values
707
708
            'target value: integer (0), value: null, type: string, operator: ==' => [
709
                null,
710
                [new Compare(0, type: CompareType::STRING)],
711
                ['' => ['Value must be equal to "0".']],
712
            ],
713
714
            // Number / string specific, expressions
715
716
            'target value: float, value: float with the same value as expression result, type: original, operator: ==' => [
717
                1 - 0.83,
718
                [new Compare(0.17, type: CompareType::ORIGINAL)],
719
                ['' => ['Value must be equal to "0.17".']],
720
            ],
721
            'target value: float epsilon, value: doubled float epsilon, type: number, operator: ==' => [
722
                PHP_FLOAT_EPSILON * 2,
723
                [new Compare(PHP_FLOAT_EPSILON)],
724
                ['' => ['Value must be equal to "2.2204460492503E-16".']],
725
            ],
726
727
            // Number / original specific, decimal places, directly provided values
728
729
            'target value: string float, value: string float with the same value, but extra decimal place (0), type: string, operator: ==' => [
730
                '100.50', [new Compare('100.5', type: CompareType::STRING)], ['' => ['Value must be equal to "100.5".']],
731
            ],
732
            'target value: string float, value: string float with the same value, but extra decimal place (0), type: string, operator: ===' => [
733
                '100.50', [new Compare('100.5', type: CompareType::STRING, operator: '===')], ['' => ['Value must be strictly equal to "100.5".']],
734
            ],
735
            'target value: string float, value: string float with the same value, but extra decimal place (0), type: original, operator: ===' => [
736
                '100.50', [new Compare('100.5', type: CompareType::ORIGINAL, operator: '===')], ['' => ['Value must be strictly equal to "100.5".']],
737
            ],
738
739
            // Number / original specific, decimal places, values provided via stringable objects
740
741
            'target value: stringable float, value: stringable float with the same value, but extra decimal place (0), type: string, operator: ==' => [
742
                $stringableFloat,
743
                [new Compare($targetStringableFloat, type: CompareType::STRING)],
744
                ['' => ['Value must be equal to "100.5".']],
745
            ],
746
            'target value: stringable float, value: stringable float with the same value, but extra decimal place (0), type: string, operator: ===' => [
747
                $stringableFloat,
748
                [new Compare($targetStringableFloat, type: CompareType::STRING, operator: '===')],
749
                ['' => ['Value must be strictly equal to "100.5".']],
750
            ],
751
            'target value: stringable float, value: stringable float with the same value, but extra decimal place (0), type: original, operator: ==' => [
752
                $stringableFloat,
753
                [new Compare($targetStringableFloat, type: CompareType::ORIGINAL)],
754
                ['' => ['Value must be equal to "100.5".']],
755
            ],
756
            'target value: stringable float, value: stringable float with the same value, but extra decimal place (0), type: original, operator: ===' => [
757
                $stringableFloat,
758
                [new Compare($targetStringableFloat, type: CompareType::ORIGINAL, operator: '===')],
759
                ['' => ['Value must be strictly equal to "100.5".']],
760
            ],
761
762
            // String / original specific, character order, directly provided values
763
764
            'target value: character, value: character located further within alphabet, type: number, operator: >' => [
765
                'b',
766
                [new Compare('a', type: CompareType::NUMBER, operator: '>')],
767
                ['' => ['Value must be greater than "a".']],
768
            ],
769
770
            // String specific, character order, values provided via stringable objects
771
772
            'target value: stringable uuidv4, value: greater stringable uuidv4, type: number, operator: >' => [
773
                $stringableUuid,
774
                [new Compare($targetStringableUuid, type: CompareType::NUMBER, operator: '>')],
775
                ['' => ['Value must be greater than "3b98a689-7d49-48bb-8741-7e27f220b69a".']],
776
            ],
777
            'target value: stringable uuidv4, value: greater stringable uuidv4, type: original, operator: >' => [
778
                $stringableUuid,
779
                [new Compare($targetStringableUuid, type: CompareType::ORIGINAL, operator: '>')],
780
                ['' => ['Value must be greater than "3b98a689-7d49-48bb-8741-7e27f220b69a".']],
781
            ],
782
783
            // Original specific, datetime
784
785
            'target value: human-readable DateTime string, value: greater DateTime string, type: string, operator: >' => [
786
                '2022-06-03',
787
                [new Compare('June 2nd, 2022', type: CompareType::STRING, operator: '>')],
788
                ['' => ['Value must be greater than "June 2nd, 2022".']],
789
            ],
790
791
            // Number / string specific, DateTime object and Unix Timestamp
792
793
            'target value: Unix Timestamp string, value: DateTime object with the same value, type: original, operator: ==' => [
794
                $dateTime->format('U'),
795
                [new Compare($dateTime, type: CompareType::ORIGINAL)],
796
                ['' => ['Value must be equal to "1675774632".']],
797
            ],
798
            'target value: Unix Timestamp string, value: DateTime object with the same value, type: original, operator: >=' => [
799
                $dateTime->format('U'),
800
                [new Compare($dateTime, type: CompareType::ORIGINAL, operator: '>=')],
801
                ['' => ['Value must be greater than or equal to "1675774632".']],
802
            ],
803
804
            // Original specific, objects
805
806
            'target value: object, value: similar object in a different instance, type: original, operator: ===' => [
807
                new stdClass(),
808
                [new Compare(new stdClass(), type: CompareType::ORIGINAL, operator: '===')],
809
                ['' => ['Value must be strictly equal to "stdClass".']],
810
            ],
811
            'target value: object, value: similar object with different property value, type: original, operator: ==' => [
812
                $objectWithDifferentPropertyValue,
813
                [new Compare($object, type: CompareType::ORIGINAL)],
814
                ['' => [sprintf('Value must be equal to "%s".', CompareObject::class)]],
815
            ],
816
            'target value: object, value: similar object with different property value, type: original, operator: ===' => [
817
                $objectWithDifferentPropertyValue,
818
                [new Compare($object, type: CompareType::ORIGINAL, operator: '===')],
819
                ['' => [sprintf('Value must be strictly equal to "%s".', CompareObject::class)]],
820
            ],
821
            'target value: object, value: similar object but with different property type, type: original, operator: ===' => [
822
                $objectWithDifferentPropertyType,
823
                [new Compare($object, type: CompareType::ORIGINAL, operator: '===')],
824
                ['' => [sprintf('Value must be strictly equal to "%s".', CompareObject::class)]],
825
            ],
826
827
            // Original specific, arrays
828
829
            'target value: array, value: similar array but with different item type, type: original, operator: ===' => [
830
                [1, 2],
831
                [new Compare([1, '2'], type: CompareType::ORIGINAL, operator: '===')],
832
                ['' => ['Value must be strictly equal to "array".']],
833
            ],
834
            'target value: array, value: similar array but with different items order, type: original, operator: ==' => [
835
                $reversedArray,
836
                [new Compare($array, type: CompareType::ORIGINAL)],
837
                ['' => ['Value must be equal to "array".']],
838
            ],
839
            'target value: array, value: similar array but reversed, type: original, operator: ===' => [
840
                $reversedArray,
841
                [new Compare($array, type: CompareType::ORIGINAL, operator: '===')],
842
                ['' => ['Value must be strictly equal to "array".']],
843
            ],
844
        ];
845
    }
846
847
    public function dataValidationFailedWithDifferentTypes(): array
848
    {
849
        $messageEqual = 'Value must be equal to "100".';
850
        $messageStrictlyEqual = 'Value must be strictly equal to "100".';
851
        $messageNotEqual = 'Value must not be equal to "100".';
852
        $messageNotStrictlyEqual = 'Value must not be strictly equal to "100".';
853
        $messageGreaterThan = 'Value must be greater than "100".';
854
        $messageGreaterOrEqualThan = 'Value must be greater than or equal to "100".';
855
        $messageLessThan = 'Value must be less than "100".';
856
        $messageLessOrEqualThan = 'Value must be less than or equal to "100".';
857
        $initialData = [
858
            // Basic
859
860
            'target value: integer, value: lower integer, type: number, operator: ==' => [
861
                99,
862
                [new Compare(100)],
863
                ['' => [$messageEqual]],
864
            ],
865
            'target value: integer, value: greater integer, type: number, operator: ==' => [
866
                101,
867
                [new Compare(100)],
868
                ['' => [$messageEqual]],
869
            ],
870
            'target value: integer, value: lower integer, type: number, operator: ===' => [
871
                99,
872
                [new Compare(100, operator: '===')],
873
                ['' => [$messageStrictlyEqual]],
874
            ],
875
            'target value: integer, value: greater integer, type: number, operator: ===' => [
876
                101,
877
                [new Compare(100, operator: '===')],
878
                ['' => [$messageStrictlyEqual]],
879
            ],
880
            'target value: integer, value: integer with the same value, type: number, operator: !=' => [
881
                100,
882
                [new Compare(100, operator: '!=')],
883
                ['' => [$messageNotEqual]],
884
            ],
885
            'target value: integer, value: integer with the same value, type: number, operator: !==' => [
886
                100,
887
                [new Compare(100, operator: '!==')],
888
                ['' => [$messageNotStrictlyEqual]],
889
            ],
890
            'target value: integer, value: integer with the same value, type: number, operator: >' => [
891
                100,
892
                [new Compare(100, operator: '>')],
893
                ['' => [$messageGreaterThan]],
894
            ],
895
            'target value: integer, value: lower integer, type: number, operator: >' => [
896
                99,
897
                [new Compare(100, operator: '>')],
898
                ['' => [$messageGreaterThan]],
899
            ],
900
            'target value: integer, value: lower integer, type: number, operator: >=' => [
901
                99,
902
                [new Compare(100, operator: '>=')],
903
                ['' => [$messageGreaterOrEqualThan]],
904
            ],
905
            'target value: integer, value: integer with the same value, type: number, operator: <' => [
906
                100,
907
                [new Compare(100, operator: '<')],
908
                ['' => [$messageLessThan]],
909
            ],
910
            'target value: integer, value: greater integer, type: number, operator: <' => [
911
                101,
912
                [new Compare(100, operator: '<')],
913
                ['' => [$messageLessThan]],
914
            ],
915
            'target value: integer, value: greater integer, type: number, operator: <=' => [
916
                101,
917
                [new Compare(100, operator: '<=')],
918
                ['' => [$messageLessOrEqualThan]],
919
            ],
920
921
            // Different types for strict equality
922
923
            'target value: empty string, value: null, type: number, operator: ===' => [
924
                null,
925
                [new Compare('', operator: '===')],
926
                ['' => ['Value must be strictly equal to "".']],
927
            ],
928
            'target value: integer, value: string integer with the same value, type: number, operator: ===' => [
929
                '100',
930
                [new Compare(100, operator: '===')],
931
                ['' => [$messageStrictlyEqual]],
932
            ],
933
            'target value: integer, value: float with the same value, but extra decimal place (0), type: number, operator: ===' => [
934
                100.0,
935
                [new Compare(100, operator: '===')],
936
                ['' => [$messageStrictlyEqual]],
937
            ],
938
939
            // Different types for non-strict inequality
940
941
            'target value: integer, value: string integer with the same value, type: number, operator: !=' => [
942
                '100',
943
                [new Compare(100, operator: '!=')],
944
                ['' => [$messageNotEqual]],
945
            ],
946
            'target value: integer, value: float with the same value, but extra decimal place (0), type: number, operator: !=' => [
947
                100.0,
948
                [new Compare(100, operator: '!=')],
949
                ['' => [$messageNotEqual]],
950
            ],
951
952
            // Target attribute
953
954
            'target attribute: array key, target attribute value: string integer, attribute value: integer with the same value, type: number, operator: ===' => [
955
                ['attribute' => '100', 'number' => 100],
956
                ['number' => new Compare(targetAttribute: 'attribute', operator: '===')],
957
                ['number' => ['Value must be strictly equal to "attribute".']],
958
            ],
959
            'target attribute: array key, target attribute value: integer, attribute value: greater integer, type: number, operator: <=' => [
960
                ['attribute' => 100, 'number' => 101],
961
                ['number' => new Compare(targetAttribute: 'attribute', operator: '<=')],
962
                ['number' => ['Value must be less than or equal to "attribute".']],
963
            ],
964
        ];
965
966
        return $this->extendDataWithDifferentTypes($initialData);
967
    }
968
969
    /**
970
     * @dataProvider dataValidationFailed
971
     * @dataProvider dataValidationFailedWithDifferentTypes
972
     */
973
    public function testValidationFailed(
974
        mixed $data,
975
        array|RuleInterface|null $rules,
976
        array $errorMessagesIndexedByPath,
977
    ): void {
978
        parent::testValidationFailed($data, $rules, $errorMessagesIndexedByPath);
979
    }
980
981
    private function extendDataWithDifferentTypes(array $initialData): array
982
    {
983
        $dynamicData = [];
984
        $mainType = CompareType::NUMBER;
985
        $remainingTypes = [CompareType::ORIGINAL, CompareType::STRING];
986
        foreach ($remainingTypes as $type) {
987
            foreach ($initialData as $key => $item) {
988
                $rules = [];
989
                foreach ($item[1] as $attribute => $rule) {
990
                    if (!$rule instanceof Compare) {
991
                        throw new RuntimeException('Wrong format for rule.');
992
                    }
993
994
                    $rules[$attribute] = new Compare(
995
                        targetValue: $rule->getTargetValue(),
996
                        targetAttribute: $rule->getTargetAttribute(),
997
                        type: $type,
998
                        operator: $rule->getOperator(),
999
                    );
1000
                }
1001
1002
                if (!is_string($key)) {
1003
                    throw new RuntimeException('Data set must have a string name.');
1004
                }
1005
1006
                $newKey = str_replace(", type: $mainType,", ", type: $type,", $key);
1007
                if ($key === $newKey) {
1008
                    throw new RuntimeException('Wrong format for type.');
1009
                }
1010
1011
                $itemData = [$item[0], $rules];
1012
                if (isset($item[2])) {
1013
                    $itemData[] = $item[2];
1014
                }
1015
1016
                $dynamicData[$newKey] = $itemData;
1017
            }
1018
        }
1019
1020
        return array_merge($initialData, $dynamicData);
1021
    }
1022
1023
    public function testSkipOnError(): void
1024
    {
1025
        $this->testSkipOnErrorInternal(new Compare(), new Compare(skipOnError: true));
1026
    }
1027
1028
    public function testWhen(): void
1029
    {
1030
        $when = static fn (mixed $value): bool => $value !== null;
1031
        $this->testWhenInternal(new Compare(), new Compare(when: $when));
1032
    }
1033
}
1034