Passed
Pull Request — master (#521)
by Alexander
05:04 queued 02:27
created

CompareTest.php$2 ➔ __toString()   A

Complexity

Conditions 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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