diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 4e3c1787ba..9b4978e4b1 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -15,6 +15,7 @@ Yii Framework 2 Change Log - Chg #19696: Change visibility of `yii\web\View::isPageEnded` to `protected` (lubosdz, samdark) - Bug #19712: Cast shell_exec() output to string for jsCompressor (impayru) - Bug #19731: Fix `yii\data\Sort` to generate proper link when multisort is on and attribute has a default sort order set (bizley) +- Bug #19735: Fix `yii\validators\NumberValidator` to use programmable message for the value validation (bizley) 2.0.47 November 18, 2022 ------------------------ diff --git a/framework/validators/NumberValidator.php b/framework/validators/NumberValidator.php index 714288a27f..074af8cf45 100644 --- a/framework/validators/NumberValidator.php +++ b/framework/validators/NumberValidator.php @@ -116,19 +116,19 @@ class NumberValidator extends Validator protected function validateValue($value) { if (is_array($value) && !$this->allowArray) { - return [Yii::t('yii', '{attribute} is invalid.'), []]; + return [$this->message, []]; } $values = !is_array($value) ? [$value] : $value; - foreach ($values as $value) { - if ($this->isNotNumber($value)) { - return [Yii::t('yii', '{attribute} is invalid.'), []]; + foreach ($values as $sample) { + if ($this->isNotNumber($sample)) { + return [$this->message, []]; } $pattern = $this->integerOnly ? $this->integerPattern : $this->numberPattern; - if (!preg_match($pattern, StringHelper::normalizeNumber($value))) { + if (!preg_match($pattern, StringHelper::normalizeNumber($sample))) { return [$this->message, []]; - } elseif ($this->min !== null && $value < $this->min) { + } elseif ($this->min !== null && $sample < $this->min) { return [$this->tooSmall, ['min' => $this->min]]; - } elseif ($this->max !== null && $value > $this->max) { + } elseif ($this->max !== null && $sample > $this->max) { return [$this->tooBig, ['max' => $this->max]]; } } diff --git a/tests/framework/validators/NumberValidatorTest.php b/tests/framework/validators/NumberValidatorTest.php index 82f643f83d..826eb9cf91 100644 --- a/tests/framework/validators/NumberValidatorTest.php +++ b/tests/framework/validators/NumberValidatorTest.php @@ -61,12 +61,15 @@ class NumberValidatorTest extends TestCase public function testEnsureMessageOnInit() { $val = new NumberValidator(); - $this->assertInternalType('string', $val->message); - $this->assertTrue($val->max === null); + $this->assertSame('{attribute} must be a number.', $val->message); + $this->assertNull($val->max); + $this->assertNull($val->min); + $this->assertNull($val->tooSmall); + $this->assertNull($val->tooBig); $val = new NumberValidator(['min' => -1, 'max' => 20, 'integerOnly' => true]); - $this->assertInternalType('string', $val->message); - $this->assertInternalType('string', $val->tooSmall); - $this->assertInternalType('string', $val->tooBig); + $this->assertSame('{attribute} must be an integer.', $val->message); + $this->assertSame('{attribute} must be no less than {min}.', $val->tooSmall); + $this->assertSame('{attribute} must be no greater than {max}.', $val->tooBig); } public function testValidateValueSimple() @@ -77,39 +80,69 @@ class NumberValidatorTest extends TestCase $this->assertTrue($val->validate(-20)); $this->assertTrue($val->validate('20')); $this->assertTrue($val->validate(25.45)); - $this->assertFalse($val->validate(false)); - $this->assertFalse($val->validate(true)); + $this->assertFalse($val->validate(false, $error)); + $this->assertSame('the input value must be a number.', $error); + $this->assertFalse($val->validate(true, $error)); + $this->assertSame('the input value must be a number.', $error); + $this->assertFalse($val->validate('0x14', $error)); + $this->assertSame('the input value must be a number.', $error); + $this->assertTrue($val->validate(0x14)); + $this->assertTrue($val->validate('0123')); + $this->assertTrue($val->validate(0123)); + $this->assertFalse($val->validate('0b111', $error)); + $this->assertSame('the input value must be a number.', $error); + $this->assertTrue($val->validate(0b111)); $this->setPointDecimalLocale(); - $this->assertFalse($val->validate('25,45')); + $this->assertFalse($val->validate('25,45', $error)); + $this->assertSame('the input value must be a number.', $error); $this->setCommaDecimalLocale(); $this->assertTrue($val->validate('25,45')); $this->restoreLocale(); - $this->assertFalse($val->validate('12:45')); + $this->assertFalse($val->validate('12:45', $error)); + $this->assertSame('the input value must be a number.', $error); + $val = new NumberValidator(['integerOnly' => true]); $this->assertTrue($val->validate(20)); $this->assertTrue($val->validate(0)); - $this->assertFalse($val->validate(25.45)); + $this->assertFalse($val->validate(25.45, $error)); + $this->assertSame('the input value must be an integer.', $error); $this->assertTrue($val->validate('20')); - $this->assertFalse($val->validate('25,45')); + $this->assertFalse($val->validate('25,45', $error)); + $this->assertSame('the input value must be an integer.', $error); $this->assertTrue($val->validate('020')); + $this->assertFalse($val->validate('0x14', $error)); + $this->assertSame('the input value must be an integer.', $error); $this->assertTrue($val->validate(0x14)); - $this->assertFalse($val->validate('0x14')); // todo check this - $this->assertFalse($val->validate(false)); - $this->assertFalse($val->validate(true)); + $this->assertTrue($val->validate('0123')); + $this->assertTrue($val->validate(0123)); + $this->assertFalse($val->validate('0b111', $error)); + $this->assertSame('the input value must be an integer.', $error); + $this->assertTrue($val->validate(0b111)); + $this->assertFalse($val->validate(false, $error)); + $this->assertSame('the input value must be an integer.', $error); + $this->assertFalse($val->validate(true, $error)); + $this->assertSame('the input value must be an integer.', $error); } public function testValidateValueArraySimple() { $val = new NumberValidator(); - $this->assertFalse($val->validate([20])); - $this->assertFalse($val->validate([0])); - $this->assertFalse($val->validate([-20])); - $this->assertFalse($val->validate(['20'])); - $this->assertFalse($val->validate([25.45])); - $this->assertFalse($val->validate([false])); - $this->assertFalse($val->validate([true])); + $this->assertFalse($val->validate([20], $error)); + $this->assertSame('the input value must be a number.', $error); + $this->assertFalse($val->validate([0], $error)); + $this->assertSame('the input value must be a number.', $error); + $this->assertFalse($val->validate([-20], $error)); + $this->assertSame('the input value must be a number.', $error); + $this->assertFalse($val->validate(['20'], $error)); + $this->assertSame('the input value must be a number.', $error); + $this->assertFalse($val->validate([25.45], $error)); + $this->assertSame('the input value must be a number.', $error); + $this->assertFalse($val->validate([false], $error)); + $this->assertSame('the input value must be a number.', $error); + $this->assertFalse($val->validate([true], $error)); + $this->assertSame('the input value must be a number.', $error); $val = new NumberValidator(); $val->allowArray = true; @@ -118,28 +151,38 @@ class NumberValidatorTest extends TestCase $this->assertTrue($val->validate([-20])); $this->assertTrue($val->validate(['20'])); $this->assertTrue($val->validate([25.45])); - $this->assertFalse($val->validate([false])); - $this->assertFalse($val->validate([true])); + $this->assertFalse($val->validate([false], $error)); + $this->assertSame('the input value must be a number.', $error); + $this->assertFalse($val->validate([true], $error)); + $this->assertSame('the input value must be a number.', $error); $this->setPointDecimalLocale(); - $this->assertFalse($val->validate(['25,45'])); + $this->assertFalse($val->validate(['25,45'], $error)); + $this->assertSame('the input value must be a number.', $error); $this->setCommaDecimalLocale(); $this->assertTrue($val->validate(['25,45'])); $this->restoreLocale(); - $this->assertFalse($val->validate(['12:45'])); + $this->assertFalse($val->validate(['12:45'], $error)); + $this->assertSame('the input value must be a number.', $error); + $val = new NumberValidator(['integerOnly' => true]); $val->allowArray = true; $this->assertTrue($val->validate([20])); $this->assertTrue($val->validate([0])); - $this->assertFalse($val->validate([25.45])); + $this->assertFalse($val->validate([25.45], $error)); + $this->assertSame('the input value must be an integer.', $error); $this->assertTrue($val->validate(['20'])); - $this->assertFalse($val->validate(['25,45'])); + $this->assertFalse($val->validate(['25,45'], $error)); + $this->assertSame('the input value must be an integer.', $error); $this->assertTrue($val->validate(['020'])); $this->assertTrue($val->validate([0x14])); - $this->assertFalse($val->validate(['0x14'])); // todo check this - $this->assertFalse($val->validate([false])); - $this->assertFalse($val->validate([true])); + $this->assertFalse($val->validate(['0x14'], $error)); + $this->assertSame('the input value must be an integer.', $error); + $this->assertFalse($val->validate([false], $error)); + $this->assertSame('the input value must be an integer.', $error); + $this->assertFalse($val->validate([true], $error)); + $this->assertSame('the input value must be an integer.', $error); } public function testValidateValueAdvanced() @@ -148,18 +191,30 @@ class NumberValidatorTest extends TestCase $this->assertTrue($val->validate('-1.23')); // signed float $this->assertTrue($val->validate('-4.423e-12')); // signed float + exponent $this->assertTrue($val->validate('12E3')); // integer + exponent - $this->assertFalse($val->validate('e12')); // just exponent - $this->assertFalse($val->validate('-e3')); - $this->assertFalse($val->validate('-4.534-e-12')); // 'signed' exponent - $this->assertFalse($val->validate('12.23^4')); // expression instead of value + $this->assertFalse($val->validate('e12', $error)); // just exponent + $this->assertSame('the input value must be a number.', $error); + $this->assertFalse($val->validate('-e3', $error)); + $this->assertSame('the input value must be a number.', $error); + $this->assertFalse($val->validate('-4.534-e-12', $error)); // 'signed' exponent + $this->assertSame('the input value must be a number.', $error); + $this->assertFalse($val->validate('12.23^4', $error)); // expression instead of value + $this->assertSame('the input value must be a number.', $error); + $val = new NumberValidator(['integerOnly' => true]); - $this->assertFalse($val->validate('-1.23')); - $this->assertFalse($val->validate('-4.423e-12')); - $this->assertFalse($val->validate('12E3')); - $this->assertFalse($val->validate('e12')); - $this->assertFalse($val->validate('-e3')); - $this->assertFalse($val->validate('-4.534-e-12')); - $this->assertFalse($val->validate('12.23^4')); + $this->assertFalse($val->validate('-1.23', $error)); + $this->assertSame('the input value must be an integer.', $error); + $this->assertFalse($val->validate('-4.423e-12', $error)); + $this->assertSame('the input value must be an integer.', $error); + $this->assertFalse($val->validate('12E3', $error)); + $this->assertSame('the input value must be an integer.', $error); + $this->assertFalse($val->validate('e12', $error)); + $this->assertSame('the input value must be an integer.', $error); + $this->assertFalse($val->validate('-e3', $error)); + $this->assertSame('the input value must be an integer.', $error); + $this->assertFalse($val->validate('-4.534-e-12', $error)); + $this->assertSame('the input value must be an integer.', $error); + $this->assertFalse($val->validate('12.23^4', $error)); + $this->assertSame('the input value must be an integer.', $error); } public function testValidateValueWithLocaleWhereDecimalPointIsComma() @@ -180,28 +235,37 @@ class NumberValidatorTest extends TestCase $val = new NumberValidator(['min' => 1]); $this->assertTrue($val->validate(1)); $this->assertFalse($val->validate(-1, $error)); - $this->assertContains('the input value must be no less than 1.', $error); - $this->assertFalse($val->validate('22e-12')); + $this->assertSame('the input value must be no less than 1.', $error); + $this->assertFalse($val->validate('22e-12', $error)); + $this->assertSame('the input value must be no less than 1.', $error); $this->assertTrue($val->validate(PHP_INT_MAX + 1)); - $val = new NumberValidator(['min' => 1], ['integerOnly' => true]); + + $val = new NumberValidator(['min' => 1, 'integerOnly' => true]); $this->assertTrue($val->validate(1)); - $this->assertFalse($val->validate(-1)); - $this->assertFalse($val->validate('22e-12')); - $this->assertTrue($val->validate(PHP_INT_MAX + 1)); + $this->assertFalse($val->validate(-1, $error)); + $this->assertSame('the input value must be no less than 1.', $error); + $this->assertFalse($val->validate('22e-12', $error)); + $this->assertSame('the input value must be an integer.', $error); + $this->assertFalse($val->validate(PHP_INT_MAX + 1, $error)); + $this->assertSame('the input value must be an integer.', $error); } public function testValidateValueMax() { $val = new NumberValidator(['max' => 1.25]); $this->assertTrue($val->validate(1)); - $this->assertFalse($val->validate(1.5)); + $this->assertFalse($val->validate(1.5, $error)); + $this->assertSame('the input value must be no greater than 1.25.', $error); $this->assertTrue($val->validate('22e-12')); $this->assertTrue($val->validate('125e-2')); $val = new NumberValidator(['max' => 1.25, 'integerOnly' => true]); $this->assertTrue($val->validate(1)); - $this->assertFalse($val->validate(1.5)); - $this->assertFalse($val->validate('22e-12')); - $this->assertFalse($val->validate('125e-2')); + $this->assertFalse($val->validate(1.5, $error)); + $this->assertSame('the input value must be an integer.', $error); + $this->assertFalse($val->validate('22e-12', $error)); + $this->assertSame('the input value must be an integer.', $error); + $this->assertFalse($val->validate('125e-2', $error)); + $this->assertSame('the input value must be an integer.', $error); } public function testValidateValueRange() @@ -209,13 +273,19 @@ class NumberValidatorTest extends TestCase $val = new NumberValidator(['min' => -10, 'max' => 20]); $this->assertTrue($val->validate(0)); $this->assertTrue($val->validate(-10)); - $this->assertFalse($val->validate(-11)); - $this->assertFalse($val->validate(21)); + $this->assertFalse($val->validate(-11, $error)); + $this->assertSame('the input value must be no less than -10.', $error); + $this->assertFalse($val->validate(21, $error)); + $this->assertSame('the input value must be no greater than 20.', $error); + $val = new NumberValidator(['min' => -10, 'max' => 20, 'integerOnly' => true]); $this->assertTrue($val->validate(0)); - $this->assertFalse($val->validate(-11)); - $this->assertFalse($val->validate(22)); - $this->assertFalse($val->validate('20e-1')); + $this->assertFalse($val->validate(-11, $error)); + $this->assertSame('the input value must be no less than -10.', $error); + $this->assertFalse($val->validate(22, $error)); + $this->assertSame('the input value must be no greater than 20.', $error); + $this->assertFalse($val->validate('20e-1', $error)); + $this->assertSame('the input value must be an integer.', $error); } public function testValidateAttribute() @@ -228,6 +298,7 @@ class NumberValidatorTest extends TestCase $model->attr_number = '43^32'; //expression $val->validateAttribute($model, 'attr_number'); $this->assertTrue($model->hasErrors('attr_number')); + $this->assertSame('attr_number must be a number.', $model->getFirstError('attr_number')); $val = new NumberValidator(['min' => 10]); $model = new FakedValidationModel(); $model->attr_number = 10; @@ -236,6 +307,7 @@ class NumberValidatorTest extends TestCase $model->attr_number = 5; $val->validateAttribute($model, 'attr_number'); $this->assertTrue($model->hasErrors('attr_number')); + $this->assertSame('attr_number must be no less than 10.', $model->getFirstError('attr_number')); $val = new NumberValidator(['max' => 10]); $model = new FakedValidationModel(); $model->attr_number = 10; @@ -244,6 +316,7 @@ class NumberValidatorTest extends TestCase $model->attr_number = 15; $val->validateAttribute($model, 'attr_number'); $this->assertTrue($model->hasErrors('attr_number')); + $this->assertSame('attr_number must be no greater than 10.', $model->getFirstError('attr_number')); $val = new NumberValidator(['max' => 10, 'integerOnly' => true]); $model = new FakedValidationModel(); $model->attr_number = 10; @@ -252,10 +325,12 @@ class NumberValidatorTest extends TestCase $model->attr_number = 3.43; $val->validateAttribute($model, 'attr_number'); $this->assertTrue($model->hasErrors('attr_number')); + $this->assertSame('attr_number must be an integer.', $model->getFirstError('attr_number')); $val = new NumberValidator(['min' => 1]); $model = FakedValidationModel::createWithAttributes(['attr_num' => [1, 2, 3]]); $val->validateAttribute($model, 'attr_num'); $this->assertTrue($model->hasErrors('attr_num')); + $this->assertSame('attr_num must be a number.', $model->getFirstError('attr_num')); // @see https://github.com/yiisoft/yii2/issues/11672 $model = new FakedValidationModel(); @@ -275,6 +350,7 @@ class NumberValidatorTest extends TestCase $model->attr_number = ['43^32']; //expression $val->validateAttribute($model, 'attr_number'); $this->assertTrue($model->hasErrors('attr_number')); + $this->assertSame('attr_number must be a number.', $model->getFirstError('attr_number')); $val = new NumberValidator(['min' => 10]); $val->allowArray = true; $model = new FakedValidationModel(); @@ -284,6 +360,7 @@ class NumberValidatorTest extends TestCase $model->attr_number = [5]; $val->validateAttribute($model, 'attr_number'); $this->assertTrue($model->hasErrors('attr_number')); + $this->assertSame('attr_number must be no less than 10.', $model->getFirstError('attr_number')); $val = new NumberValidator(['max' => 10]); $val->allowArray = true; $model = new FakedValidationModel(); @@ -293,6 +370,7 @@ class NumberValidatorTest extends TestCase $model->attr_number = [15]; $val->validateAttribute($model, 'attr_number'); $this->assertTrue($model->hasErrors('attr_number')); + $this->assertSame('attr_number must be no greater than 10.', $model->getFirstError('attr_number')); $val = new NumberValidator(['max' => 10, 'integerOnly' => true]); $val->allowArray = true; $model = new FakedValidationModel(); @@ -302,61 +380,73 @@ class NumberValidatorTest extends TestCase $model->attr_number = [3.43]; $val->validateAttribute($model, 'attr_number'); $this->assertTrue($model->hasErrors('attr_number')); + $this->assertSame('attr_number must be an integer.', $model->getFirstError('attr_number')); $val = new NumberValidator(['min' => 1]); $val->allowArray = true; $model = FakedValidationModel::createWithAttributes(['attr_num' => [[1], [2], [3]]]); $val->validateAttribute($model, 'attr_num'); $this->assertTrue($model->hasErrors('attr_num')); + $this->assertSame('attr_num must be a number.', $model->getFirstError('attr_num')); // @see https://github.com/yiisoft/yii2/issues/11672 $model = new FakedValidationModel(); $model->attr_number = new \stdClass(); $val->validateAttribute($model, 'attr_number'); $this->assertTrue($model->hasErrors('attr_number')); - + $this->assertSame('attr_number must be a number.', $model->getFirstError('attr_number')); $val = new NumberValidator(); $model = new FakedValidationModel(); $model->attr_number = ['5.5e1']; $val->validateAttribute($model, 'attr_number'); $this->assertTrue($model->hasErrors('attr_number')); + $this->assertSame('attr_number must be a number.', $model->getFirstError('attr_number')); $model->attr_number = ['43^32']; //expression $val->validateAttribute($model, 'attr_number'); $this->assertTrue($model->hasErrors('attr_number')); + $this->assertSame('attr_number must be a number.', $model->getFirstError('attr_number')); $val = new NumberValidator(['min' => 10]); $model = new FakedValidationModel(); $model->attr_number = [10]; $val->validateAttribute($model, 'attr_number'); $this->assertTrue($model->hasErrors('attr_number')); + $this->assertSame('attr_number must be a number.', $model->getFirstError('attr_number')); $model->attr_number = [5]; $val->validateAttribute($model, 'attr_number'); $this->assertTrue($model->hasErrors('attr_number')); + $this->assertSame('attr_number must be a number.', $model->getFirstError('attr_number')); $val = new NumberValidator(['max' => 10]); $model = new FakedValidationModel(); $model->attr_number = [10]; $val->validateAttribute($model, 'attr_number'); $this->assertTrue($model->hasErrors('attr_number')); + $this->assertSame('attr_number must be a number.', $model->getFirstError('attr_number')); $model->attr_number = [15]; $val->validateAttribute($model, 'attr_number'); $this->assertTrue($model->hasErrors('attr_number')); + $this->assertSame('attr_number must be a number.', $model->getFirstError('attr_number')); $val = new NumberValidator(['max' => 10, 'integerOnly' => true]); $model = new FakedValidationModel(); $model->attr_number = [10]; $val->validateAttribute($model, 'attr_number'); $this->assertTrue($model->hasErrors('attr_number')); + $this->assertSame('attr_number must be an integer.', $model->getFirstError('attr_number')); $model->attr_number = [3.43]; $val->validateAttribute($model, 'attr_number'); $this->assertTrue($model->hasErrors('attr_number')); + $this->assertSame('attr_number must be an integer.', $model->getFirstError('attr_number')); $val = new NumberValidator(['min' => 1]); $model = FakedValidationModel::createWithAttributes(['attr_num' => [[1], [2], [3]]]); $val->validateAttribute($model, 'attr_num'); $this->assertTrue($model->hasErrors('attr_num')); + $this->assertSame('attr_num must be a number.', $model->getFirstError('attr_num')); // @see https://github.com/yiisoft/yii2/issues/11672 $model = new FakedValidationModel(); $model->attr_number = new \stdClass(); $val->validateAttribute($model, 'attr_number'); $this->assertTrue($model->hasErrors('attr_number')); + $this->assertSame('attr_number must be a number.', $model->getFirstError('attr_number')); } public function testValidateAttributeWithLocaleWhereDecimalPointIsComma() @@ -376,10 +466,22 @@ class NumberValidatorTest extends TestCase $this->restoreLocale(); } - public function testEnsureCustomMessageIsSetOnValidateAttribute() + public function testEnsureCustomMessageIsSetOnValidateAttributeGeneral() + { + $val = new NumberValidator(['message' => '{attribute} is not integer.']); + $model = new FakedValidationModel(); + $model->attr_number = 'as'; + $val->validateAttribute($model, 'attr_number'); + $this->assertTrue($model->hasErrors('attr_number')); + $this->assertCount(1, $model->getErrors('attr_number')); + $msgs = $model->getErrors('attr_number'); + $this->assertSame('attr_number is not integer.', $msgs[0]); + } + + public function testEnsureCustomMessageIsSetOnValidateAttributeMin() { $val = new NumberValidator([ - 'tooSmall' => '{attribute} is to small.', + 'tooSmall' => '{attribute} is too small.', 'min' => 5, ]); $model = new FakedValidationModel(); @@ -388,7 +490,22 @@ class NumberValidatorTest extends TestCase $this->assertTrue($model->hasErrors('attr_number')); $this->assertCount(1, $model->getErrors('attr_number')); $msgs = $model->getErrors('attr_number'); - $this->assertSame('attr_number is to small.', $msgs[0]); + $this->assertSame('attr_number is too small.', $msgs[0]); + } + + public function testEnsureCustomMessageIsSetOnValidateAttributeMax() + { + $val = new NumberValidator([ + 'tooBig' => '{attribute} is too big.', + 'max' => 5, + ]); + $model = new FakedValidationModel(); + $model->attr_number = 6; + $val->validateAttribute($model, 'attr_number'); + $this->assertTrue($model->hasErrors('attr_number')); + $this->assertCount(1, $model->getErrors('attr_number')); + $msgs = $model->getErrors('attr_number'); + $this->assertSame('attr_number is too big.', $msgs[0]); } /**