From c8e4e0727a122fddcd021f4419b939dc841d769c Mon Sep 17 00:00:00 2001 From: Bizley Date: Mon, 9 Aug 2021 09:26:10 +0200 Subject: [PATCH] Fix #18674: Added more user-friendly exception messages for `yii\i18n\Formatter` --- framework/CHANGELOG.md | 1 + framework/i18n/Formatter.php | 100 +++++++++++++++++++++--- tests/framework/i18n/FormatterNumberTest.php | 113 +++++++++++++++++++++++++++ 3 files changed, 203 insertions(+), 11 deletions(-) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index f3df9a4..1752bfa 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -26,6 +26,7 @@ Yii Framework 2 Change Log - Enh #18789: Added JSONP support in `yii\web\JsonParser::parse()` (WinterSilence) - Bug #18274: Fix `yii\log\Logger` to calculate profile timings no matter the value of the flush interval (bizley) - Bug #18807: Fix replacing source whitespaces and optimize code of `yii\helpers\BaseStringHelper::mb_ucwords()` (WinterSilence) +- Enh #18674: Added more user-friendly exception messages for `yii\i18n\Formatter` (bizley) 2.0.42.1 May 06, 2021 diff --git a/framework/i18n/Formatter.php b/framework/i18n/Formatter.php index c5a13b7..27bad52 100644 --- a/framework/i18n/Formatter.php +++ b/framework/i18n/Formatter.php @@ -1732,7 +1732,7 @@ class Formatter extends Component $oldThousandSeparator = $this->thousandSeparator; $this->thousandSeparator = ''; if ($this->_intlLoaded && !isset($options[NumberFormatter::GROUPING_USED])) { - $options[NumberFormatter::GROUPING_USED] = false; + $options[NumberFormatter::GROUPING_USED] = 0; } // format the size value $params = [ @@ -1793,19 +1793,19 @@ class Formatter extends Component $formatter = new NumberFormatter($this->locale, $style); // set text attributes - foreach ($this->numberFormatterTextOptions as $name => $attribute) { - $formatter->setTextAttribute($name, $attribute); + foreach ($this->numberFormatterTextOptions as $attribute => $value) { + $this->setFormatterTextAttribute($formatter, $attribute, $value, 'numberFormatterTextOptions', 'numberFormatterOptions'); } - foreach ($textOptions as $name => $attribute) { - $formatter->setTextAttribute($name, $attribute); + foreach ($textOptions as $attribute => $value) { + $this->setFormatterTextAttribute($formatter, $attribute, $value, '$textOptions', '$options'); } // set attributes - foreach ($this->numberFormatterOptions as $name => $value) { - $formatter->setAttribute($name, $value); + foreach ($this->numberFormatterOptions as $attribute => $value) { + $this->setFormatterIntAttribute($formatter, $attribute, $value, 'numberFormatterOptions', 'numberFormatterTextOptions'); } - foreach ($options as $name => $value) { - $formatter->setAttribute($name, $value); + foreach ($options as $attribute => $value) { + $this->setFormatterIntAttribute($formatter, $attribute, $value, '$options', '$textOptions'); } if ($decimals !== null) { $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, $decimals); @@ -1823,14 +1823,92 @@ class Formatter extends Component $formatter->setSymbol(NumberFormatter::GROUPING_SEPARATOR_SYMBOL, $this->thousandSeparator); $formatter->setSymbol(NumberFormatter::MONETARY_GROUPING_SEPARATOR_SYMBOL, $this->thousandSeparator); } - foreach ($this->numberFormatterSymbols as $name => $symbol) { - $formatter->setSymbol($name, $symbol); + foreach ($this->numberFormatterSymbols as $symbol => $value) { + $this->setFormatterSymbol($formatter, $symbol, $value, 'numberFormatterSymbols'); } return $formatter; } /** + * @param NumberFormatter $formatter + * @param mixed $attribute + * @param mixed $value + * @param string $source + * @param string $alternative + */ + private function setFormatterTextAttribute($formatter, $attribute, $value, $source, $alternative) + { + if (!is_int($attribute)) { + throw new InvalidArgumentException( + "The $source array keys must be integers recognizable by NumberFormatter::setTextAttribute(). \"" + . gettype($attribute) . '" provided instead.' + ); + } + if (!is_string($value)) { + if (is_int($value)) { + throw new InvalidArgumentException( + "The $source array values must be strings. Did you mean to use $alternative?" + ); + } + throw new InvalidArgumentException( + "The $source array values must be strings. \"" . gettype($value) . '" provided instead.' + ); + } + $formatter->setTextAttribute($attribute, $value); + } + + /** + * @param NumberFormatter $formatter + * @param mixed $symbol + * @param mixed $value + * @param string $source + */ + private function setFormatterSymbol($formatter, $symbol, $value, $source) + { + if (!is_int($symbol)) { + throw new InvalidArgumentException( + "The $source array keys must be integers recognizable by NumberFormatter::setSymbol(). \"" + . gettype($symbol) . '" provided instead.' + ); + } + if (!is_string($value)) { + throw new InvalidArgumentException( + "The $source array values must be strings. \"" . gettype($value) . '" provided instead.' + ); + } + $formatter->setSymbol($symbol, $value); + } + + /** + * @param NumberFormatter $formatter + * @param mixed $attribute + * @param mixed $value + * @param string $source + * @param string $alternative + */ + private function setFormatterIntAttribute($formatter, $attribute, $value, $source, $alternative) + { + if (!is_int($attribute)) { + throw new InvalidArgumentException( + "The $source array keys must be integers recognizable by NumberFormatter::setAttribute(). \"" + . gettype($attribute) . '" provided instead.' + ); + } + if (!is_int($value)) { + if (is_string($value)) { + throw new InvalidArgumentException( + "The $source array values must be integers. Did you mean to use $alternative?" + ); + } + throw new InvalidArgumentException( + "The $source array values must be integers. \"" . gettype($value) . '" provided instead.' + ); + } + $formatter->setAttribute($attribute, $value); + } + + /** * Checks if string representations of given value and its normalized version are different. * @param string|float|int $value * @param float|int $normalizedValue diff --git a/tests/framework/i18n/FormatterNumberTest.php b/tests/framework/i18n/FormatterNumberTest.php index d475a98..635917e 100755 --- a/tests/framework/i18n/FormatterNumberTest.php +++ b/tests/framework/i18n/FormatterNumberTest.php @@ -823,4 +823,117 @@ class FormatterNumberTest extends TestCase $this->assertSame('1023 bytes', $this->formatter->asSize(1023)); $this->assertSame('1023 B', $this->formatter->asShortSize(1023)); } + + public function providerForDirectWrongTypeAttributes() + { + return [ + 'not-int key for int options' => [ + ['a' => 1], + [], + 'The $options array keys must be integers recognizable by NumberFormatter::setAttribute(). "string" provided instead.' + ], + 'string value for int options' => [ + [1 => 'a'], + [], + 'The $options array values must be integers. Did you mean to use $textOptions?' + ], + 'non-string-int value for int options' => [ + [1 => 1.1], + [], + 'The $options array values must be integers. "double" provided instead.' + ], + 'not-int key for text options' => [ + [], + ['a' => 1], + 'The $textOptions array keys must be integers recognizable by NumberFormatter::setTextAttribute(). "string" provided instead.' + ], + 'int value for text options' => [ + [], + [1 => 1], + 'The $textOptions array values must be strings. Did you mean to use $options?' + ], + 'non-string-int value for text options' => [ + [], + [1 => 1.1], + 'The $textOptions array values must be strings. "double" provided instead.' + ], + ]; + } + + /** + * @dataProvider providerForDirectWrongTypeAttributes + */ + public function testIntlAsIntegerDirectWrongTypeAttributes($intOptions, $textOptions, $message) + { + $this->expectException('yii\base\InvalidArgumentException'); + $this->expectExceptionMessage($message); + $this->formatter->asInteger(1, $intOptions, $textOptions); + } + + public function providerForConfiguredWrongTypeAttributes() + { + return [ + 'not-int key for int options' => [ + ['a' => 1], + [], + [], + 'The numberFormatterOptions array keys must be integers recognizable by NumberFormatter::setAttribute(). "string" provided instead.' + ], + 'string value for int options' => [ + [1 => 'a'], + [], + [], + 'The numberFormatterOptions array values must be integers. Did you mean to use numberFormatterTextOptions?' + ], + 'non-string-int value for int options' => [ + [1 => 1.1], + [], + [], + 'The numberFormatterOptions array values must be integers. "double" provided instead.' + ], + 'not-int key for text options' => [ + [], + ['a' => 1], + [], + 'The numberFormatterTextOptions array keys must be integers recognizable by NumberFormatter::setTextAttribute(). "string" provided instead.' + ], + 'int value for text options' => [ + [], + [1 => 1], + [], + 'The numberFormatterTextOptions array values must be strings. Did you mean to use numberFormatterOptions?' + ], + 'non-string-int value for text options' => [ + [], + [1 => 1.1], + [], + 'The numberFormatterTextOptions array values must be strings. "double" provided instead.' + ], + 'non-int key for symbol' => [ + [], + [], + ['a' => 2], + 'The numberFormatterSymbols array keys must be integers recognizable by NumberFormatter::setSymbol(). "string" provided instead.' + ], + 'non-string value for symbol' => [ + [], + [], + [1 => 3], + 'The numberFormatterSymbols array values must be strings. "integer" provided instead.' + ], + ]; + } + + /** + * @dataProvider providerForConfiguredWrongTypeAttributes + */ + public function testIntlAsIntegerConfiguredWrongTypeAttributes($intOptions, $textOptions, $symbols, $message) + { + $this->expectException('yii\base\InvalidArgumentException'); + $this->expectExceptionMessage($message); + $this->formatter->numberFormatterTextOptions = $textOptions; + $this->formatter->numberFormatterOptions = $intOptions; + $this->formatter->numberFormatterSymbols = $symbols; + $this->formatter->asInteger(1); + } }