Passed
Pull Request — master (#521)
by Alexander
02:37
created

CompareTest.php$3 ➔ __toString()   A

Complexity

Conditions 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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