mirror of
				https://github.com/yiisoft/yii2.git
				synced 2025-11-04 14:46:19 +08:00 
			
		
		
		
	Merge pull request #13114 from yiisoft/fix-numbervalidator-comma-decimal-separator
Fixes incorrect behavior of `yii\validation\NumberValidator` when used with locales where decimal separator is comma
This commit is contained in:
		@ -9,6 +9,7 @@ Yii Framework 2 Change Log
 | 
				
			|||||||
- Bug #9305: Fixed MSSQL `Schema::TYPE_TIMESTAMP` to be 'datetime' instead of 'timestamp', which is just an incremental number (nkovacs)
 | 
					- Bug #9305: Fixed MSSQL `Schema::TYPE_TIMESTAMP` to be 'datetime' instead of 'timestamp', which is just an incremental number (nkovacs)
 | 
				
			||||||
- Bug #9616: Fixed mysql\Schema::loadColumnSchema to set enumValues attribute correctly if enum definition contains commas (fphammerle)
 | 
					- Bug #9616: Fixed mysql\Schema::loadColumnSchema to set enumValues attribute correctly if enum definition contains commas (fphammerle)
 | 
				
			||||||
- Bug #9796: Initialization of not existing `yii\grid\ActionColumn` default buttons (arogachev)
 | 
					- Bug #9796: Initialization of not existing `yii\grid\ActionColumn` default buttons (arogachev)
 | 
				
			||||||
 | 
					- Bug #10488: Fixed incorrect behavior of `yii\validation\NumberValidator` when used with locales where decimal separator is comma (quantum13, samdark, rob006)
 | 
				
			||||||
- Bug #11122: Fixed can not use `orderBy` with aggregate functions like `count` (Ni-san)
 | 
					- Bug #11122: Fixed can not use `orderBy` with aggregate functions like `count` (Ni-san)
 | 
				
			||||||
- Bug #11771: Fixed semantics of `yii\di\ServiceLocator::__isset()` to match the behavior of `__get()` which fixes inconsistent behavior on newer PHP versions (cebe)
 | 
					- Bug #11771: Fixed semantics of `yii\di\ServiceLocator::__isset()` to match the behavior of `__get()` which fixes inconsistent behavior on newer PHP versions (cebe)
 | 
				
			||||||
- Bug #12213: Fixed `yii\db\ActiveRecord::unlinkAll()` to respect `onCondition()` of the relational query (silverfire)
 | 
					- Bug #12213: Fixed `yii\db\ActiveRecord::unlinkAll()` to respect `onCondition()` of the relational query (silverfire)
 | 
				
			||||||
 | 
				
			|||||||
@ -284,4 +284,25 @@ class BaseStringHelper
 | 
				
			|||||||
    {
 | 
					    {
 | 
				
			||||||
        return count(preg_split('/\s+/u', $string, null, PREG_SPLIT_NO_EMPTY));
 | 
					        return count(preg_split('/\s+/u', $string, null, PREG_SPLIT_NO_EMPTY));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Returns string represenation of number value with replaced commas to dots, if decimal point
 | 
				
			||||||
 | 
					     * of current locale is comma
 | 
				
			||||||
 | 
					     * @param int|float|string $value
 | 
				
			||||||
 | 
					     * @return string
 | 
				
			||||||
 | 
					     * @since 2.0.11
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public static function normalizeNumber($value)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $value = "$value";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $localeInfo = localeconv();
 | 
				
			||||||
 | 
					        $decimalSeparator = isset($localeInfo['decimal_point']) ? $localeInfo['decimal_point'] : null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if ($decimalSeparator !== null && $decimalSeparator !== '.') {
 | 
				
			||||||
 | 
					            $value = str_replace($decimalSeparator, '.', $value);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return $value;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -8,6 +8,7 @@
 | 
				
			|||||||
namespace yii\validators;
 | 
					namespace yii\validators;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use Yii;
 | 
					use Yii;
 | 
				
			||||||
 | 
					use yii\helpers\StringHelper;
 | 
				
			||||||
use yii\web\JsExpression;
 | 
					use yii\web\JsExpression;
 | 
				
			||||||
use yii\helpers\Json;
 | 
					use yii\helpers\Json;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -85,7 +86,8 @@ class NumberValidator extends Validator
 | 
				
			|||||||
            return;
 | 
					            return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        $pattern = $this->integerOnly ? $this->integerPattern : $this->numberPattern;
 | 
					        $pattern = $this->integerOnly ? $this->integerPattern : $this->numberPattern;
 | 
				
			||||||
        if (!preg_match($pattern, "$value")) {
 | 
					
 | 
				
			||||||
 | 
					        if (!preg_match($pattern, StringHelper::normalizeNumber($value))) {
 | 
				
			||||||
            $this->addError($model, $attribute, $this->message);
 | 
					            $this->addError($model, $attribute, $this->message);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        if ($this->min !== null && $value < $this->min) {
 | 
					        if ($this->min !== null && $value < $this->min) {
 | 
				
			||||||
@ -105,7 +107,7 @@ class NumberValidator extends Validator
 | 
				
			|||||||
            return [Yii::t('yii', '{attribute} is invalid.'), []];
 | 
					            return [Yii::t('yii', '{attribute} is invalid.'), []];
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        $pattern = $this->integerOnly ? $this->integerPattern : $this->numberPattern;
 | 
					        $pattern = $this->integerOnly ? $this->integerPattern : $this->numberPattern;
 | 
				
			||||||
        if (!preg_match($pattern, "$value")) {
 | 
					        if (!preg_match($pattern, StringHelper::normalizeNumber($value))) {
 | 
				
			||||||
            return [$this->message, []];
 | 
					            return [$this->message, []];
 | 
				
			||||||
        } elseif ($this->min !== null && $value < $this->min) {
 | 
					        } elseif ($this->min !== null && $value < $this->min) {
 | 
				
			||||||
            return [$this->tooSmall, ['min' => $this->min]];
 | 
					            return [$this->tooSmall, ['min' => $this->min]];
 | 
				
			||||||
 | 
				
			|||||||
@ -12,10 +12,42 @@ use yiiunit\TestCase;
 | 
				
			|||||||
 */
 | 
					 */
 | 
				
			||||||
class NumberValidatorTest extends TestCase
 | 
					class NumberValidatorTest extends TestCase
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
 | 
					    private $commaDecimalLocales = ['fr_FR.UTF-8', 'fr_FR.UTF8', 'fr_FR.utf-8', 'fr_FR.utf8', 'French_France.1252'];
 | 
				
			||||||
 | 
					    private $pointDecimalLocales = ['en_US.UTF-8', 'en_US.UTF8', 'en_US.utf-8', 'en_US.utf8', 'English_United States.1252'];
 | 
				
			||||||
 | 
					    private $oldLocale;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private function setCommaDecimalLocale()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        if ($this->oldLocale === false) {
 | 
				
			||||||
 | 
					            $this->markTestSkipped('Your platform does not support locales.');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (setlocale(LC_NUMERIC, $this->commaDecimalLocales) === false) {
 | 
				
			||||||
 | 
					            $this->markTestSkipped('Could not set any of required locales: ' . implode(', ', $this->commaDecimalLocales));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private function setPointDecimalLocale()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        if ($this->oldLocale === false) {
 | 
				
			||||||
 | 
					            $this->markTestSkipped('Your platform does not support locales.');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (setlocale(LC_NUMERIC, $this->pointDecimalLocales) === false) {
 | 
				
			||||||
 | 
					            $this->markTestSkipped('Could not set any of required locales: ' . implode(', ', $this->pointDecimalLocales));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private function restoreLocale()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        setlocale(LC_NUMERIC, $this->oldLocale);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    protected function setUp()
 | 
					    protected function setUp()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        parent::setUp();
 | 
					        parent::setUp();
 | 
				
			||||||
        $this->mockApplication();
 | 
					        $this->mockApplication();
 | 
				
			||||||
 | 
					        $this->oldLocale = setlocale(LC_NUMERIC, 0);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function testEnsureMessageOnInit()
 | 
					    public function testEnsureMessageOnInit()
 | 
				
			||||||
@ -37,7 +69,13 @@ class NumberValidatorTest extends TestCase
 | 
				
			|||||||
        $this->assertTrue($val->validate(-20));
 | 
					        $this->assertTrue($val->validate(-20));
 | 
				
			||||||
        $this->assertTrue($val->validate('20'));
 | 
					        $this->assertTrue($val->validate('20'));
 | 
				
			||||||
        $this->assertTrue($val->validate(25.45));
 | 
					        $this->assertTrue($val->validate(25.45));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $this->setPointDecimalLocale();
 | 
				
			||||||
        $this->assertFalse($val->validate('25,45'));
 | 
					        $this->assertFalse($val->validate('25,45'));
 | 
				
			||||||
 | 
					        $this->setCommaDecimalLocale();
 | 
				
			||||||
 | 
					        $this->assertTrue($val->validate('25,45'));
 | 
				
			||||||
 | 
					        $this->restoreLocale();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $this->assertFalse($val->validate('12:45'));
 | 
					        $this->assertFalse($val->validate('12:45'));
 | 
				
			||||||
        $val = new NumberValidator(['integerOnly' => true]);
 | 
					        $val = new NumberValidator(['integerOnly' => true]);
 | 
				
			||||||
        $this->assertTrue($val->validate(20));
 | 
					        $this->assertTrue($val->validate(20));
 | 
				
			||||||
@ -70,6 +108,19 @@ class NumberValidatorTest extends TestCase
 | 
				
			|||||||
        $this->assertFalse($val->validate('12.23^4'));
 | 
					        $this->assertFalse($val->validate('12.23^4'));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function testValidateValueWithLocaleWhereDecimalPointIsComma()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $val = new NumberValidator();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $this->setPointDecimalLocale();
 | 
				
			||||||
 | 
					        $this->assertTrue($val->validate(.5));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $this->setCommaDecimalLocale();
 | 
				
			||||||
 | 
					        $this->assertTrue($val->validate(.5));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $this->restoreLocale();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function testValidateValueMin()
 | 
					    public function testValidateValueMin()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $val = new NumberValidator(['min' => 1]);
 | 
					        $val = new NumberValidator(['min' => 1]);
 | 
				
			||||||
@ -159,6 +210,23 @@ class NumberValidatorTest extends TestCase
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function testValidateAttributeWithLocaleWhereDecimalPointIsComma()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $val = new NumberValidator();
 | 
				
			||||||
 | 
					        $model = new FakedValidationModel();
 | 
				
			||||||
 | 
					        $model->attr_number = 0.5;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $this->setPointDecimalLocale();
 | 
				
			||||||
 | 
					        $val->validateAttribute($model, 'attr_number');
 | 
				
			||||||
 | 
					        $this->assertFalse($model->hasErrors('attr_number'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $this->setCommaDecimalLocale();
 | 
				
			||||||
 | 
					        $val->validateAttribute($model, 'attr_number');
 | 
				
			||||||
 | 
					        $this->assertFalse($model->hasErrors('attr_number'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $this->restoreLocale();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function testEnsureCustomMessageIsSetOnValidateAttribute()
 | 
					    public function testEnsureCustomMessageIsSetOnValidateAttribute()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $val = new NumberValidator([
 | 
					        $val = new NumberValidator([
 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user