Skip to content

Commit 0dfe7d6

Browse files
Fix CompareValidator client-side closure compareValue resolution to pass ($model, $attribute) and avoid mutating validator state. (#20814)
1 parent a887433 commit 0dfe7d6

5 files changed

Lines changed: 1476 additions & 348 deletions

File tree

framework/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ Yii Framework 2 Change Log
3737
- Chg #20808: Rename `ApcCache` to `ApcuCache` and remove legacy APC extension support (terabytesoftw)
3838
- Bug: Fix `ActiveField::generateLabel()` to strip `label`/`for` attributes for custom tags and preserve prior label in `radio()`/`checkbox()` (terabytesoftw)
3939
- Bug: Fix `clientScript` not being instantiated in validators when a custom array config is set and `useJquery` is `false` (terabytesoftw)
40+
- Bug: Fix `CompareValidator` client-side closure `compareValue` resolution to pass `($model, $attribute)` and avoid mutating validator state (terabytesoftw)
4041

4142

4243
2.0.55 under development

framework/jquery/validators/CompareValidatorJqueryClientScript.php

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
<?php
22

3+
declare(strict_types=1);
4+
35
/**
46
* @link https://www.yiiframework.com/
57
* @copyright Copyright (c) 2008 Yii Software LLC
68
* @license https://www.yiiframework.com/license/
79
*/
810

9-
declare(strict_types=1);
10-
1111
namespace yii\jquery\validators;
1212

13+
use Closure;
1314
use yii\base\Model;
1415
use yii\helpers\Html;
1516
use yii\helpers\Json;
@@ -19,8 +20,10 @@
1920
use yii\validators\Validator;
2021
use yii\web\View;
2122

23+
use function call_user_func;
24+
2225
/**
23-
* CompareValidatorJqueryClientScript provides client-side validation script generation for attribute comparison.
26+
* Provides client-side validation script generation for attribute comparison.
2427
*
2528
* This class implements {@see ClientValidatorScriptInterface} to supply client-side validation options and register the
2629
* corresponding JavaScript code for comparison validation in Yii2 forms using jQuery.
@@ -29,20 +32,26 @@
2932
* @implements ClientValidatorScriptInterface<T>
3033
*
3134
* @author Wilmer Arambula <terabytesoftw@gmail.com>
32-
* @since 2.2.0
35+
* @since 2.2
3336
*/
3437
class CompareValidatorJqueryClientScript implements ClientValidatorScriptInterface
3538
{
3639
public function getClientOptions(Validator $validator, Model $model, string $attribute): array
3740
{
41+
$resolvedCompareValue = $validator->compareValue;
42+
43+
if ($resolvedCompareValue instanceof Closure) {
44+
$resolvedCompareValue = call_user_func($resolvedCompareValue, $model, $attribute);
45+
}
46+
3847
$options = [
3948
'operator' => $validator->operator,
4049
'type' => $validator->type,
4150
];
4251

43-
if ($validator->compareValue !== null) {
44-
$options['compareValue'] = $validator->compareValue;
45-
$compareLabel = $compareValue = $compareValueOrAttribute = $validator->compareValue;
52+
if ($resolvedCompareValue !== null) {
53+
$options['compareValue'] = $resolvedCompareValue;
54+
$compareLabel = $compareValue = $compareValueOrAttribute = $resolvedCompareValue;
4655
} else {
4756
$compareAttribute = $validator->compareAttribute === null ? $attribute . '_repeat' : $validator->compareAttribute;
4857

@@ -71,10 +80,6 @@ public function getClientOptions(Validator $validator, Model $model, string $att
7180

7281
public function register(Validator $validator, Model $model, string $attribute, View $view): string
7382
{
74-
if ($validator->compareValue != null && $validator->compareValue instanceof \Closure) {
75-
$validator->compareValue = call_user_func($validator->compareValue);
76-
}
77-
7883
ValidationAsset::register($view);
7984

8085
$options = $this->getClientOptions($validator, $model, $attribute);

tests/framework/jquery/validators/CompareValidatorJqueryClientScriptTest.php

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
namespace yiiunit\framework\jquery\validators;
1212

13+
use Closure;
1314
use PHPUnit\Framework\Attributes\Group;
1415
use Yii;
1516
use yii\validators\CompareValidator;
@@ -81,23 +82,42 @@ public function testClientValidateAttribute(): void
8182

8283
public function testClientValidateAttributeWithClosureCompareValue(): void
8384
{
85+
$closureConfig = [
86+
'compareValue' => static fn(FakedValidationModel $model, string $attribute): string => 'closure_value',
87+
'operator' => '==',
88+
'type' => CompareValidator::TYPE_STRING,
89+
];
90+
91+
$validator = new CompareValidator($closureConfig);
92+
8493
$modelValidator = new FakedValidationModel();
8594

86-
$validator = new CompareValidator(
87-
[
88-
'compareValue' => static fn(): string => 'closure_value',
89-
'operator' => '==',
90-
'type' => CompareValidator::TYPE_STRING,
91-
],
95+
self::assertInstanceOf(
96+
Closure::class,
97+
$validator->compareValue,
98+
'Should start as Closure before client validation.',
9299
);
93-
94100
self::assertSame(
95101
<<<JS
96102
yii.validation.compare(value, messages, {"operator":"==","type":"string","compareValue":"closure_value","skipOnEmpty":1,"message":"attrA must be equal to \u0022closure_value\u0022."}, \$form);
97103
JS,
98104
$validator->clientValidateAttribute($modelValidator, 'attrA', Yii::$app->view),
99105
'Should return correct validation script.',
100106
);
107+
self::assertInstanceOf(
108+
Closure::class,
109+
$validator->compareValue,
110+
'Should not resolve compareValue in-place.',
111+
);
112+
113+
$validator = new CompareValidator($closureConfig);
114+
115+
self::assertInstanceOf(
116+
Closure::class,
117+
$validator->compareValue,
118+
'Should start as Closure before client validation.',
119+
);
120+
101121
self::assertSame(
102122
[
103123
'operator' => '==',
@@ -109,6 +129,19 @@ public function testClientValidateAttributeWithClosureCompareValue(): void
109129
$validator->getClientOptions($modelValidator, 'attrA'),
110130
'Should return correct options array.',
111131
);
132+
self::assertInstanceOf(
133+
Closure::class,
134+
$validator->compareValue,
135+
'Should not resolve compareValue in-place.',
136+
);
137+
138+
$validator = new CompareValidator(
139+
[
140+
'compareValue' => static fn(): string => 'closure_value',
141+
'operator' => '==',
142+
'type' => CompareValidator::TYPE_STRING,
143+
],
144+
);
112145

113146
$validator->validate('someIncorrectValue', $errorMessage);
114147

@@ -128,7 +161,6 @@ public function testClientValidateAttributeWithNullCompareAttribute(): void
128161

129162
$validator = new CompareValidator(
130163
[
131-
'compareAttribute' => 'attrA_repeat',
132164
'operator' => '==',
133165
'type' => CompareValidator::TYPE_STRING,
134166
],

0 commit comments

Comments
 (0)