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