Passed
Push — master ( 4ba8b0...5a95eb )
by
unknown
10:52
created

CompareTest.php$10 ➔ __toString()   A

Complexity

Conditions 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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