From d36a8779d794909c3e138ee12cc43e79e808c375 Mon Sep 17 00:00:00 2001 From: John Was Date: Thu, 17 Sep 2015 08:26:17 +0200 Subject: [PATCH 001/134] added length and weight formatters --- framework/i18n/Formatter.php | 126 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 118 insertions(+), 8 deletions(-) diff --git a/framework/i18n/Formatter.php b/framework/i18n/Formatter.php index 23a7d70..77cfe1c 100644 --- a/framework/i18n/Formatter.php +++ b/framework/i18n/Formatter.php @@ -1036,7 +1036,7 @@ class Formatter extends Component * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]]. * @return string the formatted result. * @throws InvalidParamException if the input value is not numeric or the formatting failed. - * @see sizeFormat + * @see sizeFormatBase * @see asSize */ public function asShortSize($value, $decimals = null, $options = [], $textOptions = []) @@ -1045,7 +1045,7 @@ class Formatter extends Component return $this->nullDisplay; } - list($params, $position) = $this->formatSizeNumber($value, $decimals, $options, $textOptions); + list($params, $position) = $this->formatNumber($value, $decimals, 4, $this->sizeFormatBase, $options, $textOptions); if ($this->sizeFormatBase == 1024) { switch ($position) { @@ -1080,7 +1080,7 @@ class Formatter extends Component * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]]. * @return string the formatted result. * @throws InvalidParamException if the input value is not numeric or the formatting failed. - * @see sizeFormat + * @see sizeFormatBase * @see asShortSize */ public function asSize($value, $decimals = null, $options = [], $textOptions = []) @@ -1089,7 +1089,7 @@ class Formatter extends Component return $this->nullDisplay; } - list($params, $position) = $this->formatSizeNumber($value, $decimals, $options, $textOptions); + list($params, $position) = $this->formatNumber($value, $decimals, 4, $this->sizeFormatBase, $options, $textOptions); if ($this->sizeFormatBase == 1024) { switch ($position) { @@ -1112,18 +1112,128 @@ class Formatter extends Component } } + /** + * Formats the value in millimeters as a length in human readable form for example `12 meters`. + * + * @param integer $value value in millimeters to be formatted. + * @param integer $decimals the number of digits after the decimal point. + * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]]. + * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]]. + * @return string the formatted result. + * @throws InvalidParamException if the input value is not numeric or the formatting failed. + * @see asLength + */ + public function asLength($value, $decimals = null, $options = [], $textOptions = []) + { + if ($value === null) { + return $this->nullDisplay; + } + + list($params, $position) = $this->formatNumber($value, $decimals, 2, 1000, $options, $textOptions); + + switch ($position) { + case 0: return Yii::t('yii', '{nFormatted} {n, plural, =1{millimeter} other{millimeters}}', $params, $this->locale); + case 1: return Yii::t('yii', '{nFormatted} {n, plural, =1{meters} meters}}', $params, $this->locale); + default: return Yii::t('yii', '{nFormatted} {n, plural, =1{kilometers} kilometers}}', $params, $this->locale); + } + } + + /** + * Formats the value in millimeters as a length in human readable form for example `12 m`. + * + * This is the short form of [[asLength]]. + * + * @param integer $value value in millimeters to be formatted. + * @param integer $decimals the number of digits after the decimal point. + * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]]. + * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]]. + * @return string the formatted result. + * @throws InvalidParamException if the input value is not numeric or the formatting failed. + * @see asLength + */ + public function asShortLength($value, $decimals = null, $options = [], $textOptions = []) + { + if ($value === null) { + return $this->nullDisplay; + } + + list($params, $position) = $this->formatNumber($value, $decimals, 2, 1000, $options, $textOptions); + + switch ($position) { + case 0: return Yii::t('yii', '{nFormatted} mm', $params, $this->locale); + case 1: return Yii::t('yii', '{nFormatted} m', $params, $this->locale); + default: return Yii::t('yii', '{nFormatted} km', $params, $this->locale); + } + } + + /** + * Formats the value in grams as a weight in human readable form for example `12 kilograms`. + * + * @param integer $value value in grams to be formatted. + * @param integer $decimals the number of digits after the decimal point. + * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]]. + * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]]. + * @return string the formatted result. + * @throws InvalidParamException if the input value is not numeric or the formatting failed. + * @see asWeight + */ + public function asWeight($value, $decimals = null, $options = [], $textOptions = []) + { + if ($value === null) { + return $this->nullDisplay; + } + + list($params, $position) = $this->formatNumber($value, $decimals, 1, 1000, $options, $textOptions); + + switch ($position) { + case 0: return Yii::t('yii', '{nFormatted} {n, plural, =1{gram} other{grams}}', $params, $this->locale); + case 1: return Yii::t('yii', '{nFormatted} {n, plural, =1{kilogram} other{kilograms}}', $params, $this->locale); + default: return Yii::t('yii', '{nFormatted} {n, plural, =1{ton} other{tons}}', $params, $this->locale); + } + } + + /** + * Formats the value in grams as a weight in human readable form for example `12 kg`. + * + * This is the short form of [[asWeight]]. + * + * @param integer $value value in grams to be formatted. + * @param integer $decimals the number of digits after the decimal point. + * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]]. + * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]]. + * @return string the formatted result. + * @throws InvalidParamException if the input value is not numeric or the formatting failed. + * @see asWeight + */ + public function asShortWeight($value, $decimals = null, $options = [], $textOptions = []) + { + if ($value === null) { + return $this->nullDisplay; + } + + list($params, $position) = $this->formatNumber($value, $decimals, 1, 1000, $options, $textOptions); + + switch ($position) { + case 0: return Yii::t('yii', '{nFormatted} g', $params, $this->locale); + case 1: return Yii::t('yii', '{nFormatted} kg', $params, $this->locale); + default: return Yii::t('yii', '{nFormatted} t', $params, $this->locale); + } + } + /** * Given the value in bytes formats number part of the human readable form. * * @param string|integer|float $value value in bytes to be formatted. * @param integer $decimals the number of digits after the decimal point + * @param integer $maxPosition maximum internal position of size unit + * @param integer $formatBase the base at which each next unit is calculated, either 1000 or 1024 * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]]. * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]]. * @return array [parameters for Yii::t containing formatted number, internal position of size unit] * @throws InvalidParamException if the input value is not numeric or the formatting failed. */ - private function formatSizeNumber($value, $decimals, $options, $textOptions) + private function formatNumber($value, $decimals, $maxPosition, $formatBase, $options, $textOptions) { if (is_string($value) && is_numeric($value)) { $value = (int) $value; @@ -1134,12 +1244,12 @@ class Formatter extends Component $position = 0; do { - if (abs($value) < $this->sizeFormatBase) { + if (abs($value) < $formatBase) { break; } - $value = $value / $this->sizeFormatBase; + $value = $value / $formatBase; $position++; - } while ($position < 5); + } while ($position < $maxPosition + 1); // no decimals for bytes if ($position === 0) { From 705f544045dc7fb908b6c47623b16d9ec290deb0 Mon Sep 17 00:00:00 2001 From: John Was Date: Sun, 20 Sep 2015 13:37:21 +0200 Subject: [PATCH 002/134] add imperial system to length and weight formatters and switch base unit to SI units --- framework/i18n/Formatter.php | 245 +++++++++++++++++++++++++++++++++---------- 1 file changed, 192 insertions(+), 53 deletions(-) diff --git a/framework/i18n/Formatter.php b/framework/i18n/Formatter.php index 77cfe1c..2772fc7 100644 --- a/framework/i18n/Formatter.php +++ b/framework/i18n/Formatter.php @@ -46,6 +46,9 @@ use yii\helpers\Html; */ class Formatter extends Component { + const UNIT_SYSTEM_METRIC = 'metric'; + const UNIT_SYSTEM_IMPERIAL = 'imperial'; + /** * @var string the text to be displayed when formatting a `null` value. * Defaults to `'(not set)'`, where `(not set)` @@ -219,6 +222,10 @@ class Formatter extends Component * Defaults to 1024. */ public $sizeFormatBase = 1024; + /** + * @var string default system of measure units. + */ + public $systemOfUnits = self::UNIT_SYSTEM_METRIC; /** * @var boolean whether the [PHP intl extension](http://php.net/manual/en/book.intl.php) is loaded. @@ -1115,7 +1122,9 @@ class Formatter extends Component /** * Formats the value in millimeters as a length in human readable form for example `12 meters`. * - * @param integer $value value in millimeters to be formatted. + * @param integer $value value to be formatted. + * @param double $baseUnit unit of value as the multiplier of the smallest unit + * @param string $system either self::UNIT_SYSTEM_METRIC or self::UNIT_SYSTEM_IMPERIAL, if null defaults to metric * @param integer $decimals the number of digits after the decimal point. * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]]. * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]]. @@ -1123,19 +1132,9 @@ class Formatter extends Component * @throws InvalidParamException if the input value is not numeric or the formatting failed. * @see asLength */ - public function asLength($value, $decimals = null, $options = [], $textOptions = []) + public function asLength($value, $baseUnit = null, $system = null, $decimals = null, $options = [], $textOptions = []) { - if ($value === null) { - return $this->nullDisplay; - } - - list($params, $position) = $this->formatNumber($value, $decimals, 2, 1000, $options, $textOptions); - - switch ($position) { - case 0: return Yii::t('yii', '{nFormatted} {n, plural, =1{millimeter} other{millimeters}}', $params, $this->locale); - case 1: return Yii::t('yii', '{nFormatted} {n, plural, =1{meters} meters}}', $params, $this->locale); - default: return Yii::t('yii', '{nFormatted} {n, plural, =1{kilometers} kilometers}}', $params, $this->locale); - } + return $this->formatUnit('length', 'long', $value, $baseUnit, $system, $decimals, $options, $textOptions); } /** @@ -1143,7 +1142,9 @@ class Formatter extends Component * * This is the short form of [[asLength]]. * - * @param integer $value value in millimeters to be formatted. + * @param integer $value value to be formatted. + * @param double $baseUnit unit of value as the multiplier of the smallest unit + * @param string $system either self::UNIT_SYSTEM_METRIC or self::UNIT_SYSTEM_IMPERIAL, if null defaults to metric * @param integer $decimals the number of digits after the decimal point. * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]]. * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]]. @@ -1151,25 +1152,17 @@ class Formatter extends Component * @throws InvalidParamException if the input value is not numeric or the formatting failed. * @see asLength */ - public function asShortLength($value, $decimals = null, $options = [], $textOptions = []) + public function asShortLength($value, $baseUnit = null, $system = null, $decimals = null, $options = [], $textOptions = []) { - if ($value === null) { - return $this->nullDisplay; - } - - list($params, $position) = $this->formatNumber($value, $decimals, 2, 1000, $options, $textOptions); - - switch ($position) { - case 0: return Yii::t('yii', '{nFormatted} mm', $params, $this->locale); - case 1: return Yii::t('yii', '{nFormatted} m', $params, $this->locale); - default: return Yii::t('yii', '{nFormatted} km', $params, $this->locale); - } + return $this->formatUnit('length', 'short', $value, $baseUnit, $system, $decimals, $options, $textOptions); } /** * Formats the value in grams as a weight in human readable form for example `12 kilograms`. * - * @param integer $value value in grams to be formatted. + * @param integer $value value to be formatted. + * @param double $baseUnit unit of value as the multiplier of the smallest unit + * @param string $system either self::UNIT_SYSTEM_METRIC or self::UNIT_SYSTEM_IMPERIAL, if null defaults to metric * @param integer $decimals the number of digits after the decimal point. * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]]. * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]]. @@ -1177,19 +1170,9 @@ class Formatter extends Component * @throws InvalidParamException if the input value is not numeric or the formatting failed. * @see asWeight */ - public function asWeight($value, $decimals = null, $options = [], $textOptions = []) + public function asWeight($value, $baseUnit = null, $system = null, $decimals = null, $options = [], $textOptions = []) { - if ($value === null) { - return $this->nullDisplay; - } - - list($params, $position) = $this->formatNumber($value, $decimals, 1, 1000, $options, $textOptions); - - switch ($position) { - case 0: return Yii::t('yii', '{nFormatted} {n, plural, =1{gram} other{grams}}', $params, $this->locale); - case 1: return Yii::t('yii', '{nFormatted} {n, plural, =1{kilogram} other{kilograms}}', $params, $this->locale); - default: return Yii::t('yii', '{nFormatted} {n, plural, =1{ton} other{tons}}', $params, $this->locale); - } + return $this->formatUnit('weight', 'long', $value, $baseUnit, $system, $decimals, $options, $textOptions); } /** @@ -1197,7 +1180,9 @@ class Formatter extends Component * * This is the short form of [[asWeight]]. * - * @param integer $value value in grams to be formatted. + * @param double $value value to be formatted. + * @param double $baseUnit unit of value as the multiplier of the smallest unit + * @param string $system either self::UNIT_SYSTEM_METRIC or self::UNIT_SYSTEM_IMPERIAL, if null defaults to metric * @param integer $decimals the number of digits after the decimal point. * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]]. * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]]. @@ -1205,29 +1190,47 @@ class Formatter extends Component * @throws InvalidParamException if the input value is not numeric or the formatting failed. * @see asWeight */ - public function asShortWeight($value, $decimals = null, $options = [], $textOptions = []) + public function asShortWeight($value, $baseUnit = null, $system = null, $decimals = null, $options = [], $textOptions = []) + { + return $this->formatUnit('weight', 'short', $value, $baseUnit, $system, $decimals, $options, $textOptions); + } + + /** + * @param string $unit one of: weight, length + * @param string $type one of: short, long + * @param double $value + * @param double $baseUnit + * @param string $system + * @param integer $decimals + * @param $options + * @param $textOptions + * @return string + */ + private function formatUnit($unit, $type, $value, $baseUnit, $system, $decimals, $options, $textOptions) { if ($value === null) { return $this->nullDisplay; } + if ($system === null) { + $system = $this->systemOfUnits; + } + if ($baseUnit === null) { + $baseUnit = $this->getBaseMeasureUnit($unit, $system); + } + $multipliers = $this->getUnitMultipliers($unit, $system); - list($params, $position) = $this->formatNumber($value, $decimals, 1, 1000, $options, $textOptions); + list($params, $position) = $this->formatNumber($value / $baseUnit, $decimals, null, $multipliers, $options, $textOptions); - switch ($position) { - case 0: return Yii::t('yii', '{nFormatted} g', $params, $this->locale); - case 1: return Yii::t('yii', '{nFormatted} kg', $params, $this->locale); - default: return Yii::t('yii', '{nFormatted} t', $params, $this->locale); - } + return $this->getMeasureUnit($unit, $system, 'short', $multipliers[$position], $params); } - /** * Given the value in bytes formats number part of the human readable form. * * @param string|integer|float $value value in bytes to be formatted. * @param integer $decimals the number of digits after the decimal point - * @param integer $maxPosition maximum internal position of size unit - * @param integer $formatBase the base at which each next unit is calculated, either 1000 or 1024 + * @param integer $maxPosition maximum internal position of size unit, ignored if $formatBase is an array + * @param array|integer $formatBase the base at which each next unit is calculated, either 1000 or 1024, or an array * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]]. * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]]. * @return array [parameters for Yii::t containing formatted number, internal position of size unit] @@ -1243,11 +1246,20 @@ class Formatter extends Component } $position = 0; + if (is_array($formatBase)) { + $maxPosition = count($formatBase) - 1; + } do { - if (abs($value) < $formatBase) { - break; + if (is_array($formatBase)) { + if (abs($value) < $formatBase[$position]) { + break; + } + } else { + if (abs($value) < $formatBase) { + break; + } + $value = $value / $formatBase; } - $value = $value / $formatBase; $position++; } while ($position < $maxPosition + 1); @@ -1277,6 +1289,133 @@ class Formatter extends Component return [$params, $position]; } + protected function getBaseMeasureUnit($unit, $system) + { + switch ($unit) { + case 'length': + switch ($system) { + case self::UNIT_SYSTEM_IMPERIAL: + return 12; // 1 feet = 12 inches + case self::UNIT_SYSTEM_METRIC: + return 1000; // 1 meter = 1000 millimeters + } + case 'weight': + switch ($system) { + case self::UNIT_SYSTEM_IMPERIAL: + return 7000; // 1 pound = 7000 grains + case self::UNIT_SYSTEM_METRIC: + return 1000; // 1 kilogram = 1000 grams + } + } + } + + protected function getUnitMultipliers($unit, $system) + { + switch ($unit) { + case 'length': + switch ($system) { + case self::UNIT_SYSTEM_IMPERIAL: + return [1, 12, 36, 792, 7920, 63360]; + case self::UNIT_SYSTEM_METRIC: + return [1, 100, 1000, 1000000]; + } + + case 'weight': + switch ($system) { + case self::UNIT_SYSTEM_IMPERIAL: + return [1, 27.34375, 437.5, 7000, 98000, 196000, 784000, 15680000]; + case self::UNIT_SYSTEM_METRIC: + return [1, 1000, 1000000]; + } + } + } + + protected function getMeasureUnit($unit, $system, $type, $baseUnit, $params) + { + switch ($unit) { + case 'length': + switch ($system) { + case self::UNIT_SYSTEM_IMPERIAL: + if ($type === 'short') { + switch ($baseUnit) { + case 1: return Yii::t('yii', '{nFormatted} in', $params, $this->locale); + case 12: return Yii::t('yii', '{nFormatted} ft', $params, $this->locale); + case 36: return Yii::t('yii', '{nFormatted} yd', $params, $this->locale); + case 792: return Yii::t('yii', '{nFormatted} ch', $params, $this->locale); + case 7920: return Yii::t('yii', '{nFormatted} fur', $params, $this->locale); + case 63360: return Yii::t('yii', '{nFormatted} mi', $params, $this->locale); + } + } else { + switch ($baseUnit) { + case 1: return Yii::t('yii', '{nFormatted} {n, plural, =1{inch} other{inches}', $params, $this->locale); + case 12: return Yii::t('yii', '{nFormatted} {n, plural, =1{foot} other{feet}', $params, $this->locale); + case 36: return Yii::t('yii', '{nFormatted} {n, plural, =1{yard} other{yards}', $params, $this->locale); + case 792: return Yii::t('yii', '{nFormatted} {n, plural, =1{chain} other{chains}', $params, $this->locale); + case 7920: return Yii::t('yii', '{nFormatted} {n, plural, =1{furlong} other{furlongs}', $params, $this->locale); + case 63360: return Yii::t('yii', '{nFormatted} {n, plural, =1{mile} other{miles}', $params, $this->locale); + } + } + case self::UNIT_SYSTEM_METRIC: + if ($type === 'short') { + switch ($baseUnit) { + case 1: return Yii::t('yii', '{nFormatted} mm', $params, $this->locale); + case 100: return Yii::t('yii', '{nFormatted} cm', $params, $this->locale); + case 1000: return Yii::t('yii', '{nFormatted} m', $params, $this->locale); + case 1000000: return Yii::t('yii', '{nFormatted} km', $params, $this->locale); + } + } else { + switch ($baseUnit) { + case 1: return Yii::t('yii', '{nFormatted} {n, plural, =1{millimeter} other{millimeters}', $params, $this->locale); + case 100: return Yii::t('yii', '{nFormatted} {n, plural, =1{centimeter} other{centimeters}', $params, $this->locale); + case 1000: return Yii::t('yii', '{nFormatted} {n, plural, =1{meter} other{meters}', $params, $this->locale); + case 1000000: return Yii::t('yii', '{nFormatted} {n, plural, =1{kilometer} other{kilometers}', $params, $this->locale); + } + } + } + case 'weight': + switch ($system) { + case self::UNIT_SYSTEM_IMPERIAL: + if ($type === 'short') { + switch ($baseUnit) { + case 1: return Yii::t('yii', '{nFormatted} gr', $params, $this->locale); + case 27.34375: return Yii::t('yii', '{nFormatted} dr', $params, $this->locale); + case 437.5: return Yii::t('yii', '{nFormatted} oz', $params, $this->locale); + case 7000: return Yii::t('yii', '{nFormatted} lb', $params, $this->locale); + case 98000: return Yii::t('yii', '{nFormatted} st', $params, $this->locale); + case 196000: return Yii::t('yii', '{nFormatted} qr', $params, $this->locale); + case 784000: return Yii::t('yii', '{nFormatted} cwt', $params, $this->locale); + case 15680000: return Yii::t('yii', '{nFormatted} t', $params, $this->locale); + } + } else { + switch ($baseUnit) { + case 1: return Yii::t('yii', '{nFormatted} {n, plural, =1{grain} other{grains}', $params, $this->locale); + case 27.34375: return Yii::t('yii', '{nFormatted} {n, plural, =1{drachm} other{drachms}', $params, $this->locale); + case 437.5: return Yii::t('yii', '{nFormatted} {n, plural, =1{ounce} other{ounces}', $params, $this->locale); + case 7000: return Yii::t('yii', '{nFormatted} {n, plural, =1{pound} other{pounds}', $params, $this->locale); + case 98000: return Yii::t('yii', '{nFormatted} {n, plural, =1{stone} other{stones}', $params, $this->locale); + case 196000: return Yii::t('yii', '{nFormatted} {n, plural, =1{quarter} other{quarters}', $params, $this->locale); + case 784000: return Yii::t('yii', '{nFormatted} {n, plural, =1{hundredweight} other{hundredweights}', $params, $this->locale); + case 15680000: return Yii::t('yii', '{nFormatted} {n, plural, =1{ton} other{tons}', $params, $this->locale); + } + } + case self::UNIT_SYSTEM_METRIC: + if ($type === 'short') { + switch ($baseUnit) { + case 1: return Yii::t('yii', '{nFormatted} g', $params, $this->locale); + case 1000: return Yii::t('yii', '{nFormatted} kg', $params, $this->locale); + case 1000000: return Yii::t('yii', '{nFormatted} t', $params, $this->locale); + } + } else { + switch ($baseUnit) { + case 1: return Yii::t('yii', '{nFormatted} {n, plural, =1{gram} other{grams}', $params, $this->locale); + case 1000: return Yii::t('yii', '{nFormatted} {n, plural, =1{kilogram} other{kilograms}', $params, $this->locale); + case 1000000: return Yii::t('yii', '{nFormatted} {n, plural, =1{ton} other{tons}', $params, $this->locale); + } + } + } + } + } + /** * Normalizes a numeric input value * From 504c85c9fd0bbc4abd4fe3312eac6219017eb667 Mon Sep 17 00:00:00 2001 From: John Was Date: Sun, 20 Sep 2015 13:44:27 +0200 Subject: [PATCH 003/134] updated phpdocs --- framework/i18n/Formatter.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/framework/i18n/Formatter.php b/framework/i18n/Formatter.php index 2772fc7..9f02826 100644 --- a/framework/i18n/Formatter.php +++ b/framework/i18n/Formatter.php @@ -1120,7 +1120,7 @@ class Formatter extends Component } /** - * Formats the value in millimeters as a length in human readable form for example `12 meters`. + * Formats the value as a length in human readable form for example `12 meters`. * * @param integer $value value to be formatted. * @param double $baseUnit unit of value as the multiplier of the smallest unit @@ -1138,7 +1138,7 @@ class Formatter extends Component } /** - * Formats the value in millimeters as a length in human readable form for example `12 m`. + * Formats the value as a length in human readable form for example `12 m`. * * This is the short form of [[asLength]]. * @@ -1158,7 +1158,7 @@ class Formatter extends Component } /** - * Formats the value in grams as a weight in human readable form for example `12 kilograms`. + * Formats the value as a weight in human readable form for example `12 kilograms`. * * @param integer $value value to be formatted. * @param double $baseUnit unit of value as the multiplier of the smallest unit @@ -1176,7 +1176,7 @@ class Formatter extends Component } /** - * Formats the value in grams as a weight in human readable form for example `12 kg`. + * Formats the value as a weight in human readable form for example `12 kg`. * * This is the short form of [[asWeight]]. * From 4d3c211e247e36ecfde6d43cdfda02907b924e90 Mon Sep 17 00:00:00 2001 From: John Was Date: Mon, 21 Sep 2015 17:34:24 +0200 Subject: [PATCH 004/134] use unit translations from ResourceBundle --- framework/i18n/Formatter.php | 228 +++++++++++------------------ tests/framework/i18n/FormatterUnitTest.php | 46 ++++++ 2 files changed, 134 insertions(+), 140 deletions(-) create mode 100644 tests/framework/i18n/FormatterUnitTest.php diff --git a/framework/i18n/Formatter.php b/framework/i18n/Formatter.php index 9f02826..283e3c0 100644 --- a/framework/i18n/Formatter.php +++ b/framework/i18n/Formatter.php @@ -48,6 +48,10 @@ class Formatter extends Component { const UNIT_SYSTEM_METRIC = 'metric'; const UNIT_SYSTEM_IMPERIAL = 'imperial'; + const FORMAT_WIDTH_LONG = 'long'; + const FORMAT_WIDTH_SHORT = 'short'; + const UNIT_LENGTH = 'length'; + const UNIT_WEIGHT = 'weight'; /** * @var string the text to be displayed when formatting a `null` value. @@ -226,6 +230,57 @@ class Formatter extends Component * @var string default system of measure units. */ public $systemOfUnits = self::UNIT_SYSTEM_METRIC; + /** + * @var array configuration of measure units + */ + public $measureUnits = [ + self::UNIT_LENGTH => [ + self::UNIT_SYSTEM_IMPERIAL => [ + 'inch' => 1, + 'foot' => 12, + 'yard' => 36, + 'chain' => 792, + 'furlong' => 7920, + 'mile' => 63360, + ], + self::UNIT_SYSTEM_METRIC => [ + 'millimeter' => 1, + 'centimeter' => 100, + 'meter' => 1000, + 'kilometer' => 1000000, + ], + ], + self::UNIT_WEIGHT => [ + self::UNIT_SYSTEM_IMPERIAL => [ + 'grain' => 1, + 'drachm' => 27.34375, + 'ounce' => 437.5, + 'pound' => 7000, + 'stone' => 98000, + 'quarter' => 196000, + 'hundredweight' => 784000, + 'ton' => 15680000, + ], + self::UNIT_SYSTEM_METRIC => [ + 'gram' => 1, + 'kilogram' => 1000, + 'ton' => 1000000, + ], + ], + ]; + /** + * @var array base units as multipliers for smallest possible unit from $measureUnits + */ + public $baseUnits = [ + self::UNIT_LENGTH => [ + self::UNIT_SYSTEM_IMPERIAL => 12, // 1 feet = 12 inches + self::UNIT_SYSTEM_METRIC => 1000, // 1 meter = 1000 millimeters + ], + self::UNIT_WEIGHT => [ + self::UNIT_SYSTEM_IMPERIAL => 7000, // 1 pound = 7000 grains + self::UNIT_SYSTEM_METRIC => 1000, // 1 kilogram = 1000 grams + ], + ]; /** * @var boolean whether the [PHP intl extension](http://php.net/manual/en/book.intl.php) is loaded. @@ -1134,7 +1189,7 @@ class Formatter extends Component */ public function asLength($value, $baseUnit = null, $system = null, $decimals = null, $options = [], $textOptions = []) { - return $this->formatUnit('length', 'long', $value, $baseUnit, $system, $decimals, $options, $textOptions); + return $this->formatUnit(self::UNIT_LENGTH, self::FORMAT_WIDTH_LONG, $value, $baseUnit, $system, $decimals, $options, $textOptions); } /** @@ -1154,7 +1209,7 @@ class Formatter extends Component */ public function asShortLength($value, $baseUnit = null, $system = null, $decimals = null, $options = [], $textOptions = []) { - return $this->formatUnit('length', 'short', $value, $baseUnit, $system, $decimals, $options, $textOptions); + return $this->formatUnit(self::UNIT_LENGTH, self::FORMAT_WIDTH_SHORT, $value, $baseUnit, $system, $decimals, $options, $textOptions); } /** @@ -1172,7 +1227,7 @@ class Formatter extends Component */ public function asWeight($value, $baseUnit = null, $system = null, $decimals = null, $options = [], $textOptions = []) { - return $this->formatUnit('weight', 'long', $value, $baseUnit, $system, $decimals, $options, $textOptions); + return $this->formatUnit(self::UNIT_WEIGHT, self::FORMAT_WIDTH_LONG, $value, $baseUnit, $system, $decimals, $options, $textOptions); } /** @@ -1192,12 +1247,12 @@ class Formatter extends Component */ public function asShortWeight($value, $baseUnit = null, $system = null, $decimals = null, $options = [], $textOptions = []) { - return $this->formatUnit('weight', 'short', $value, $baseUnit, $system, $decimals, $options, $textOptions); + return $this->formatUnit(self::UNIT_WEIGHT, self::FORMAT_WIDTH_SHORT, $value, $baseUnit, $system, $decimals, $options, $textOptions); } /** - * @param string $unit one of: weight, length - * @param string $type one of: short, long + * @param string $unitType one of: weight, length + * @param string $unitFormat one of: short, long * @param double $value * @param double $baseUnit * @param string $system @@ -1206,7 +1261,7 @@ class Formatter extends Component * @param $textOptions * @return string */ - private function formatUnit($unit, $type, $value, $baseUnit, $system, $decimals, $options, $textOptions) + private function formatUnit($unitType, $unitFormat, $value, $baseUnit, $system, $decimals, $options, $textOptions) { if ($value === null) { return $this->nullDisplay; @@ -1215,13 +1270,30 @@ class Formatter extends Component $system = $this->systemOfUnits; } if ($baseUnit === null) { - $baseUnit = $this->getBaseMeasureUnit($unit, $system); + $baseUnit = $this->baseUnits[$unitType][$system]; } - $multipliers = $this->getUnitMultipliers($unit, $system); + $multipliers = array_values($this->measureUnits[$unitType][$system]); + $unitNames = array_keys($this->measureUnits[$unitType][$system]); - list($params, $position) = $this->formatNumber($value / $baseUnit, $decimals, null, $multipliers, $options, $textOptions); + list($params, $position) = $this->formatNumber($value * $baseUnit, $decimals, null, $multipliers, $options, $textOptions); - return $this->getMeasureUnit($unit, $system, 'short', $multipliers[$position], $params); + // build the message pattern + $resourceBundle = new \ResourceBundle($this->locale, 'ICUDATA-unit'); + $bundleKey = 'units' . ($unitFormat === self::FORMAT_WIDTH_SHORT ? 'Short' : ''); + $unitBundle = $resourceBundle[$bundleKey][$unitType][$unitNames[$position]]; + $message = []; + foreach ($unitBundle as $key => $value) { + if ($key === 'dnam') { + continue; + } + $message[] = "$key{{$value}}"; + } + $message = '{n, plural, '.implode(' ', $message).'}'; + + return (new \MessageFormatter($this->locale, $message))->format([ + '0' => $params['nFormatted'], + 'n' => $params['n'], + ]); } /** @@ -1251,7 +1323,7 @@ class Formatter extends Component } do { if (is_array($formatBase)) { - if (abs($value) < $formatBase[$position]) { + if (abs($value) < $formatBase[$position + 1]) { break; } } else { @@ -1262,8 +1334,11 @@ class Formatter extends Component } $position++; } while ($position < $maxPosition + 1); + if (is_array($formatBase) && $formatBase[$position - 1] !== 1) { + $value /= $formatBase[$position - 1]; + } - // no decimals for bytes + // no decimals for smallest unit if ($position === 0) { $decimals = 0; } elseif ($decimals !== null) { @@ -1289,133 +1364,6 @@ class Formatter extends Component return [$params, $position]; } - protected function getBaseMeasureUnit($unit, $system) - { - switch ($unit) { - case 'length': - switch ($system) { - case self::UNIT_SYSTEM_IMPERIAL: - return 12; // 1 feet = 12 inches - case self::UNIT_SYSTEM_METRIC: - return 1000; // 1 meter = 1000 millimeters - } - case 'weight': - switch ($system) { - case self::UNIT_SYSTEM_IMPERIAL: - return 7000; // 1 pound = 7000 grains - case self::UNIT_SYSTEM_METRIC: - return 1000; // 1 kilogram = 1000 grams - } - } - } - - protected function getUnitMultipliers($unit, $system) - { - switch ($unit) { - case 'length': - switch ($system) { - case self::UNIT_SYSTEM_IMPERIAL: - return [1, 12, 36, 792, 7920, 63360]; - case self::UNIT_SYSTEM_METRIC: - return [1, 100, 1000, 1000000]; - } - - case 'weight': - switch ($system) { - case self::UNIT_SYSTEM_IMPERIAL: - return [1, 27.34375, 437.5, 7000, 98000, 196000, 784000, 15680000]; - case self::UNIT_SYSTEM_METRIC: - return [1, 1000, 1000000]; - } - } - } - - protected function getMeasureUnit($unit, $system, $type, $baseUnit, $params) - { - switch ($unit) { - case 'length': - switch ($system) { - case self::UNIT_SYSTEM_IMPERIAL: - if ($type === 'short') { - switch ($baseUnit) { - case 1: return Yii::t('yii', '{nFormatted} in', $params, $this->locale); - case 12: return Yii::t('yii', '{nFormatted} ft', $params, $this->locale); - case 36: return Yii::t('yii', '{nFormatted} yd', $params, $this->locale); - case 792: return Yii::t('yii', '{nFormatted} ch', $params, $this->locale); - case 7920: return Yii::t('yii', '{nFormatted} fur', $params, $this->locale); - case 63360: return Yii::t('yii', '{nFormatted} mi', $params, $this->locale); - } - } else { - switch ($baseUnit) { - case 1: return Yii::t('yii', '{nFormatted} {n, plural, =1{inch} other{inches}', $params, $this->locale); - case 12: return Yii::t('yii', '{nFormatted} {n, plural, =1{foot} other{feet}', $params, $this->locale); - case 36: return Yii::t('yii', '{nFormatted} {n, plural, =1{yard} other{yards}', $params, $this->locale); - case 792: return Yii::t('yii', '{nFormatted} {n, plural, =1{chain} other{chains}', $params, $this->locale); - case 7920: return Yii::t('yii', '{nFormatted} {n, plural, =1{furlong} other{furlongs}', $params, $this->locale); - case 63360: return Yii::t('yii', '{nFormatted} {n, plural, =1{mile} other{miles}', $params, $this->locale); - } - } - case self::UNIT_SYSTEM_METRIC: - if ($type === 'short') { - switch ($baseUnit) { - case 1: return Yii::t('yii', '{nFormatted} mm', $params, $this->locale); - case 100: return Yii::t('yii', '{nFormatted} cm', $params, $this->locale); - case 1000: return Yii::t('yii', '{nFormatted} m', $params, $this->locale); - case 1000000: return Yii::t('yii', '{nFormatted} km', $params, $this->locale); - } - } else { - switch ($baseUnit) { - case 1: return Yii::t('yii', '{nFormatted} {n, plural, =1{millimeter} other{millimeters}', $params, $this->locale); - case 100: return Yii::t('yii', '{nFormatted} {n, plural, =1{centimeter} other{centimeters}', $params, $this->locale); - case 1000: return Yii::t('yii', '{nFormatted} {n, plural, =1{meter} other{meters}', $params, $this->locale); - case 1000000: return Yii::t('yii', '{nFormatted} {n, plural, =1{kilometer} other{kilometers}', $params, $this->locale); - } - } - } - case 'weight': - switch ($system) { - case self::UNIT_SYSTEM_IMPERIAL: - if ($type === 'short') { - switch ($baseUnit) { - case 1: return Yii::t('yii', '{nFormatted} gr', $params, $this->locale); - case 27.34375: return Yii::t('yii', '{nFormatted} dr', $params, $this->locale); - case 437.5: return Yii::t('yii', '{nFormatted} oz', $params, $this->locale); - case 7000: return Yii::t('yii', '{nFormatted} lb', $params, $this->locale); - case 98000: return Yii::t('yii', '{nFormatted} st', $params, $this->locale); - case 196000: return Yii::t('yii', '{nFormatted} qr', $params, $this->locale); - case 784000: return Yii::t('yii', '{nFormatted} cwt', $params, $this->locale); - case 15680000: return Yii::t('yii', '{nFormatted} t', $params, $this->locale); - } - } else { - switch ($baseUnit) { - case 1: return Yii::t('yii', '{nFormatted} {n, plural, =1{grain} other{grains}', $params, $this->locale); - case 27.34375: return Yii::t('yii', '{nFormatted} {n, plural, =1{drachm} other{drachms}', $params, $this->locale); - case 437.5: return Yii::t('yii', '{nFormatted} {n, plural, =1{ounce} other{ounces}', $params, $this->locale); - case 7000: return Yii::t('yii', '{nFormatted} {n, plural, =1{pound} other{pounds}', $params, $this->locale); - case 98000: return Yii::t('yii', '{nFormatted} {n, plural, =1{stone} other{stones}', $params, $this->locale); - case 196000: return Yii::t('yii', '{nFormatted} {n, plural, =1{quarter} other{quarters}', $params, $this->locale); - case 784000: return Yii::t('yii', '{nFormatted} {n, plural, =1{hundredweight} other{hundredweights}', $params, $this->locale); - case 15680000: return Yii::t('yii', '{nFormatted} {n, plural, =1{ton} other{tons}', $params, $this->locale); - } - } - case self::UNIT_SYSTEM_METRIC: - if ($type === 'short') { - switch ($baseUnit) { - case 1: return Yii::t('yii', '{nFormatted} g', $params, $this->locale); - case 1000: return Yii::t('yii', '{nFormatted} kg', $params, $this->locale); - case 1000000: return Yii::t('yii', '{nFormatted} t', $params, $this->locale); - } - } else { - switch ($baseUnit) { - case 1: return Yii::t('yii', '{nFormatted} {n, plural, =1{gram} other{grams}', $params, $this->locale); - case 1000: return Yii::t('yii', '{nFormatted} {n, plural, =1{kilogram} other{kilograms}', $params, $this->locale); - case 1000000: return Yii::t('yii', '{nFormatted} {n, plural, =1{ton} other{tons}', $params, $this->locale); - } - } - } - } - } - /** * Normalizes a numeric input value * diff --git a/tests/framework/i18n/FormatterUnitTest.php b/tests/framework/i18n/FormatterUnitTest.php new file mode 100644 index 0000000..5984f35 --- /dev/null +++ b/tests/framework/i18n/FormatterUnitTest.php @@ -0,0 +1,46 @@ +mockApplication([ + 'timeZone' => 'UTC', + 'language' => 'en-US', + ]); + $this->formatter = new Formatter(['locale' => 'pl-PL']); + } + + protected function tearDown() + { + parent::tearDown(); + IntlTestHelper::resetIntlStatus(); + $this->formatter = null; + } + + public function testAsLength() + { + $this->assertSame("53 milimetry", $this->formatter->asLength(0.053)); + $this->assertSame("0,99 centrymetra", $this->formatter->asLength(0.099)); + $this->assertSame("0,12 metra", $this->formatter->asLength(0.123)); + } +} From ac8d185539461ee853f6f1949359e563b50a60d8 Mon Sep 17 00:00:00 2001 From: John Was Date: Mon, 21 Sep 2015 18:20:23 +0200 Subject: [PATCH 005/134] cache ResourceBundle and created messages; fix unit divisor --- framework/i18n/Formatter.php | 50 ++++++++++++++++++++++-------- tests/framework/i18n/FormatterUnitTest.php | 8 +++-- 2 files changed, 42 insertions(+), 16 deletions(-) diff --git a/framework/i18n/Formatter.php b/framework/i18n/Formatter.php index 283e3c0..53b1e24 100644 --- a/framework/i18n/Formatter.php +++ b/framework/i18n/Formatter.php @@ -245,7 +245,7 @@ class Formatter extends Component ], self::UNIT_SYSTEM_METRIC => [ 'millimeter' => 1, - 'centimeter' => 100, + 'centimeter' => 10, 'meter' => 1000, 'kilometer' => 1000000, ], @@ -286,6 +286,14 @@ class Formatter extends Component * @var boolean whether the [PHP intl extension](http://php.net/manual/en/book.intl.php) is loaded. */ private $_intlLoaded = false; + /** + * @var \ResourceBundle cached ResourceBundle object used to read unit translations + */ + private $_resourceBundle; + /** + * @var array cached unit translation patterns + */ + private $_unitMessages = []; /** @@ -1273,14 +1281,35 @@ class Formatter extends Component $baseUnit = $this->baseUnits[$unitType][$system]; } $multipliers = array_values($this->measureUnits[$unitType][$system]); - $unitNames = array_keys($this->measureUnits[$unitType][$system]); list($params, $position) = $this->formatNumber($value * $baseUnit, $decimals, null, $multipliers, $options, $textOptions); - // build the message pattern - $resourceBundle = new \ResourceBundle($this->locale, 'ICUDATA-unit'); + $message = $this->getUnitMessage($unitType, $unitFormat, $system, $position); + + return (new \MessageFormatter($this->locale, $message))->format([ + '0' => $params['nFormatted'], + 'n' => $params['n'], + ]); + } + + /** + * @param string $unitType + * @param string $unitFormat + * @param string $system + * @param integer $position + * @return string + */ + private function getUnitMessage($unitType, $unitFormat, $system, $position) + { + if (isset($this->_unitMessages[$unitType][$system][$position])) { + return $this->_unitMessages[$unitType][$system][$position]; + } + if ($this->_resourceBundle === null) { + $this->_resourceBundle = new \ResourceBundle($this->locale, 'ICUDATA-unit'); + } + $unitNames = array_keys($this->measureUnits[$unitType][$system]); $bundleKey = 'units' . ($unitFormat === self::FORMAT_WIDTH_SHORT ? 'Short' : ''); - $unitBundle = $resourceBundle[$bundleKey][$unitType][$unitNames[$position]]; + $unitBundle = $this->_resourceBundle[$bundleKey][$unitType][$unitNames[$position]]; $message = []; foreach ($unitBundle as $key => $value) { if ($key === 'dnam') { @@ -1288,12 +1317,7 @@ class Formatter extends Component } $message[] = "$key{{$value}}"; } - $message = '{n, plural, '.implode(' ', $message).'}'; - - return (new \MessageFormatter($this->locale, $message))->format([ - '0' => $params['nFormatted'], - 'n' => $params['n'], - ]); + return $this->_unitMessages[$unitType][$system][$position] = '{n, plural, '.implode(' ', $message).'}'; } /** @@ -1334,8 +1358,8 @@ class Formatter extends Component } $position++; } while ($position < $maxPosition + 1); - if (is_array($formatBase) && $formatBase[$position - 1] !== 1) { - $value /= $formatBase[$position - 1]; + if (is_array($formatBase) && $position !== 0) { + $value /= $formatBase[$position]; } // no decimals for smallest unit diff --git a/tests/framework/i18n/FormatterUnitTest.php b/tests/framework/i18n/FormatterUnitTest.php index 5984f35..a07e60d 100644 --- a/tests/framework/i18n/FormatterUnitTest.php +++ b/tests/framework/i18n/FormatterUnitTest.php @@ -39,8 +39,10 @@ class FormatterUnitTest extends TestCase public function testAsLength() { - $this->assertSame("53 milimetry", $this->formatter->asLength(0.053)); - $this->assertSame("0,99 centrymetra", $this->formatter->asLength(0.099)); - $this->assertSame("0,12 metra", $this->formatter->asLength(0.123)); + $this->assertSame("5 milimetrów", $this->formatter->asLength(0.005)); + $this->assertSame("5.30 centymetra", $this->formatter->asLength(0.053)); + $this->assertSame("9.90 centymetra", $this->formatter->asLength(0.099)); + $this->assertSame("10.00 centymetrów", $this->formatter->asLength(0.1)); + $this->assertSame("1.12 metra", $this->formatter->asLength(1.123)); } } From 85cbe8dbafb06d0af49d4b221f4c6035e32afa56 Mon Sep 17 00:00:00 2001 From: kidol Date: Tue, 20 Dec 2016 15:20:01 +0100 Subject: [PATCH 006/134] Fix race conditions in FileMutex --- framework/CHANGELOG.md | 4 ++- framework/mutex/FileMutex.php | 77 ++++++++++++++++++++++++++++++++----------- 2 files changed, 60 insertions(+), 21 deletions(-) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index bacb623..9a260c4 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -24,9 +24,11 @@ Yii Framework 2 Change Log - Bug #12828: Fixed handling of nested arrays, objects in `\yii\grid\GridView::guessColumns` (githubjeka) - Bug #12836: Fixed `yii\widgets\GridView::filterUrl` to not ignore `#` part of filter URL (cebe, arogachev) - Bug #12856: Fixed `yii\web\XmlResponseFormatter` to use `true` and `false` to represent booleans (samdark) +- Bug #12860: Fixed possible race conditions in `yii\mutex\FileMutex` (kidol) - Bug #12879: Console progress bar was not working properly in Windows terminals (samdark, kids-return) - Bug #12880: Fixed `yii\behaviors\AttributeTypecastBehavior` marks attributes with `null` value as 'dirty' (klimov-paul) - Bug #12904: Fixed lowercase table name in migrations (zlakomanoff) +- Bug #12927: `ArrayHelper::getValue()` did not throw exception if the input was neither an object nor an array, even though it was documented (cebe) - Bug #12939: Hard coded table names for MSSQL in RBAC migration (arogachev) - Bug #12974: Fixed incorrect order of migrations history in case `yii\console\controllers\MigrateController::$migrationNamespaces` is in use (evgen-d, klimov-paul) - Bug #13071: Help option for commands was not working in modules (arogachev, haimanman) @@ -37,10 +39,10 @@ Yii Framework 2 Change Log - Bug #13159: Fixed `destroy` method in `yii.captcha.js` which did not work as expected (arogachev) - Bug #13198: Fixed order of checks in `yii\validators\IpValidator` that sometimes caused wrong error message (silverfire) - Bug #13200: Creating URLs for routes specified in `yii\rest\UrlRule::$extraPatterns` did not work if no HTTP verb was specified (cebe) -- Bug #13229: Fix fetching table schema for `pgsql` when `PDO::ATTR_CASE` is set (klimov-paul) - Bug #13231: Fixed `destroy` method in `yii.gridView.js` which did not work as expected (arogachev) - Bug #13232: Event handlers were not detached with changed selector in `yii.gridView.js` (arogachev) - Bug #13108: Fix execute command with negative integer parameter (pana1990, uaoleg) +- Bug: Fixed `yii\mutex\FileMutex::$autoRelease` having no effect due to missing base class initialization (kidol) - Enh #475: Added Bash and Zsh completion support for the `./yii` command (cebe, silverfire) - Enh #6242: Access to validator in inline validation (arogachev) - Enh #6373: Introduce `yii\db\Query::emulateExecution()` to force returning an empty result for a query (klimov-paul) diff --git a/framework/mutex/FileMutex.php b/framework/mutex/FileMutex.php index d673785..6d3c611 100644 --- a/framework/mutex/FileMutex.php +++ b/framework/mutex/FileMutex.php @@ -72,6 +72,7 @@ class FileMutex extends Mutex */ public function init() { + parent::init(); $this->mutexPath = Yii::getAlias($this->mutexPath); if (!is_dir($this->mutexPath)) { FileHelper::createDirectory($this->mutexPath, $this->dirMode, true); @@ -86,26 +87,51 @@ class FileMutex extends Mutex */ protected function acquireLock($name, $timeout = 0) { - $file = fopen($this->getLockFilePath($name), 'w+'); - if ($file === false) { - return false; - } - if ($this->fileMode !== null) { - @chmod($this->getLockFilePath($name), $this->fileMode); - } + $filePath = $this->getLockFilePath($name); $waitTime = 0; - while (!flock($file, LOCK_EX | LOCK_NB)) { - $waitTime++; - if ($waitTime > $timeout) { + + while (true) { + $file = fopen($filePath, 'w+'); + + if ($file === false) { + return false; + } + + if ($this->fileMode !== null) { + @chmod($filePath, $this->fileMode); + } + + if (!flock($file, LOCK_EX | LOCK_NB)) { fclose($file); - return false; + if (++$waitTime > $timeout) { + return false; + } + + sleep(1); + continue; } - sleep(1); + + // Under unix we delete the lock file before releasing the related handle. Thus it's possible that we've acquired a lock on + // a non-existing file here (race condition). We must compare the inode of the lock file handle with the inode of the actual lock file. + // If they do not match we simply continue the loop since we can assume the inodes will be equal on the next try. + // Example of race condition without inode-comparison: + // Script A: locks file + // Script B: opens file + // Script A: unlinks and unlocks file + // Script B: locks handle of *unlinked* file + // Script C: opens and locks *new* file + // In this case we would have acquired two locks for the same file path. + if (DIRECTORY_SEPARATOR !== '\\' && fstat($file)['ino'] !== @fileinode($filePath)) { + clearstatcache(true, $filePath); + flock($file, LOCK_UN); + fclose($file); + continue; + } + + $this->_files[$name] = $file; + return true; } - $this->_files[$name] = $file; - - return true; } /** @@ -115,15 +141,26 @@ class FileMutex extends Mutex */ protected function releaseLock($name) { - if (!isset($this->_files[$name]) || !flock($this->_files[$name], LOCK_UN)) { + if (!isset($this->_files[$name])) { return false; - } else { + } + + if (DIRECTORY_SEPARATOR === '\\') { + // Under windows it's not possible to delete a file opened via fopen (either by own or other process). + // That's why we must first unlock and close the handle and then *try* to delete the lock file. + flock($this->_files[$name], LOCK_UN); fclose($this->_files[$name]); + @unlink($this->getLockFilePath($name)); + } else { + // Under unix it's possible to delete a file opened via fopen (either by own or other process). + // That's why we must unlink (the currently locked) lock file first and then unlock and close the handle. unlink($this->getLockFilePath($name)); - unset($this->_files[$name]); - - return true; + flock($this->_files[$name], LOCK_UN); + fclose($this->_files[$name]); } + + unset($this->_files[$name]); + return true; } /** From 3ea5b97585d15b88d97a8f7e4d5ca6036e279900 Mon Sep 17 00:00:00 2001 From: kidol Date: Wed, 21 Dec 2016 19:37:43 +0100 Subject: [PATCH 007/134] Fix existing test --- tests/framework/mutex/MutexTestTrait.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/framework/mutex/MutexTestTrait.php b/tests/framework/mutex/MutexTestTrait.php index 2c1df6c..fb2926e 100644 --- a/tests/framework/mutex/MutexTestTrait.php +++ b/tests/framework/mutex/MutexTestTrait.php @@ -22,6 +22,7 @@ trait MutexTestTrait $mutex = $this->createMutex(); $this->assertTrue($mutex->acquire(self::$mutexName)); + $this->assertTrue($mutex->release(self::$mutexName)); } public function testThatMutexLockIsWorking() From f24d29a46f587b1017d074c585106e710d383b57 Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Mon, 24 Jul 2017 22:12:40 +0200 Subject: [PATCH 008/134] Added guide about Client side Javascript of ActiveForm (#12541) [skip ci] --- docs/guide/README.md | 1 + docs/guide/input-form-javascript.md | 202 ++++++++++++++++++++++++++++++++++++ 2 files changed, 203 insertions(+) create mode 100644 docs/guide/input-form-javascript.md diff --git a/docs/guide/README.md b/docs/guide/README.md index f420465..dddcde1 100644 --- a/docs/guide/README.md +++ b/docs/guide/README.md @@ -92,6 +92,7 @@ Getting Data from Users * [Uploading Files](input-file-upload.md) * [Collecting Tabular Input](input-tabular-input.md) * [Getting Data for Multiple Models](input-multiple-models.md) +* [Extending ActiveForm on the Client Side](input-form-javascript.md) Displaying Data diff --git a/docs/guide/input-form-javascript.md b/docs/guide/input-form-javascript.md new file mode 100644 index 0000000..35676ec --- /dev/null +++ b/docs/guide/input-form-javascript.md @@ -0,0 +1,202 @@ +Extending ActiveForm on the Client Side +======================================= + +The [[yii\widgets\ActiveForm]] widget comes with a set of JavaScript methods that are used for client validation. +Its implementation is very flexible and allows you to extend it in different ways. +In the following these are described. + +## ActiveForm events + +ActiveForm triggers a series of dedicated events. Using the code like the following you can subscribe to these +events and handle them: + +```javascript +$('#contact-form').on('beforeSubmit', function (e) { + if (!confirm("Everything is correct. Submit?")) { + return false; + } + return true; +}); +``` + +In the following we'll review events available. + +### `beforeValidate` + +`beforeValidate` is triggered before validating the whole form. + +The signature of the event handler should be: + +```javascript +function (event, messages, deferreds) +``` + +where + +- `event`: an Event object. +- `messages`: an associative array with keys being attribute IDs and values being error message arrays + for the corresponding attributes. +- `deferreds`: an array of Deferred objects. You can use `deferreds.add(callback)` to add a new + deferred validation. + +If the handler returns a boolean `false`, it will stop further form validation after this event. And as +a result, `afterValidate` event will not be triggered. + +### `afterValidate` + +`afterValidate` event is triggered after validating the whole form. + +The signature of the event handler should be: + +```javascript +function (event, messages, errorAttributes) +``` + +where + +- `event`: an Event object. +- `messages`: an associative array with keys being attribute IDs and values being error message arrays + for the corresponding attributes. +- `errorAttributes`: an array of attributes that have validation errors. Please refer to + `attributeDefaults` for the structure of this parameter. + +### `beforeValidateAttribute` + +`beforeValidateAttribute` event is triggered before validating an attribute. +The signature of the event handler should be: + +```javascript +function (event, attribute, messages, deferreds) +``` + +where + +- `event`: an Event object. +- `attribute`: the attribute to be validated. Please refer to `attributeDefaults` for the structure + of this parameter. +- `messages`: an array to which you can add validation error messages for the specified attribute. +- `deferreds`: an array of Deferred objects. You can use `deferreds.add(callback)` to add + a new deferred validation. + +If the handler returns a boolean `false`, it will stop further validation of the specified attribute. +And as a result, `afterValidateAttribute` event will not be triggered. + +### `afterValidateAttribute` + +`afterValidateAttribute` event is triggered after validating the whole form and each attribute. + +The signature of the event handler should be: + +```javascript +function (event, attribute, messages) +``` + +where + +- `event`: an Event object. +- `attribute`: the attribute being validated. Please refer to `attributeDefaults` for the structure + of this parameter. +- `messages`: an array to which you can add additional validation error messages for the specified + attribute. + +### `beforeSubmit` + +`beforeSubmit` event is triggered before submitting the form after all validations have passed. + +The signature of the event handler should be: + +```javascript +function (event) +``` + +where event is an Event object. + +If the handler returns a boolean `false`, it will stop form submission. + +### `ajaxBeforeSend` + +`ajaxBeforeSend` event is triggered before sending an AJAX request for AJAX-based validation. + +The signature of the event handler should be: + +```javascript +function (event, jqXHR, settings) +``` + +where + +- `event`: an Event object. +- `jqXHR`: a jqXHR object +- `settings`: the settings for the AJAX request + +### `ajaxComplete` + +`ajaxComplete` event is triggered after completing an AJAX request for AJAX-based validation. + +The signature of the event handler should be: + +```javascript +function (event, jqXHR, textStatus) +``` + +where + +- `event`: an Event object. +- `jqXHR`: a jqXHR object +- `textStatus`: the status of the request ("success", "notmodified", "error", "timeout", +"abort", or "parsererror"). + +## Submitting the form via AJAX + +While validation can be made on client side or via AJAX request, the form submission itself is done +as a normal request by default. If you want the form to be submitted via AJAX, you can achieve this +by handling the `beforeSubmit` event of the form in the following way: + +```javascript +var $form = $('#formId'); +$form.on('beforeSubmit', function() { + var data = $form.serialize(); + $.ajax({ + url: $form.attr('action'), + type: 'POST', + data: data, + success: function (data) { + // Implement successful + }, + error: function(jqXHR, errMsg) { + alert(errMsg); + } + }); + return false; // prevent default submit +}); +``` + +To learn more about the jQuery `ajax()` function, please refer to the [jQuery documentation](https://api.jquery.com/jQuery.ajax/). + + +## Adding fields dynamically + +In modern web applications you often have the need of changing a form after it has been displayed to the user. +This can for example be the addition of new fields after click on a "plus"-icon. +To enable client validation for these fields, they have to be registered with the ActiveForm JavaScript plugin. + +You have to add a field itself and then add it to validation list: + +```javascript +$('#contact-form').yiiActiveForm('add', { + id: 'address', + name: 'address', + container: '.field-address', + input: '#address', + error: '.help-block', + validate: function (attribute, value, messages, deferred, $form) { + yii.validation.required(value, messages, {message: "Validation Message Here"}); + } +}); +``` + +To remove a field from validation list so it's not validated you can do the following: + +```javascript +$('#contact-form').yiiActiveForm('remove', 'address'); +``` From d4ded67f69630bd78acdd476ae6a2b7f4105120d Mon Sep 17 00:00:00 2001 From: Dmitry Dorogin Date: Mon, 24 Jul 2017 23:20:03 +0300 Subject: [PATCH 009/134] Fixes #14513: Updated phpdoc and added tests for yii\rbac\ManagerInterface::assign and yii\rbac\ManagerInterface::revoke functions (#14528) --- framework/rbac/ManagerInterface.php | 4 +- tests/framework/rbac/ManagerTestCase.php | 132 ++++++++++++++++++++++++------- 2 files changed, 106 insertions(+), 30 deletions(-) diff --git a/framework/rbac/ManagerInterface.php b/framework/rbac/ManagerInterface.php index 5c7c2d1..0dd45e7 100644 --- a/framework/rbac/ManagerInterface.php +++ b/framework/rbac/ManagerInterface.php @@ -182,7 +182,7 @@ interface ManagerInterface extends CheckAccessInterface /** * Assigns a role to a user. * - * @param Role $role + * @param Role|Permission $role * @param string|int $userId the user ID (see [[\yii\web\User::id]]) * @return Assignment the role assignment information. * @throws \Exception if the role has already been assigned to the user @@ -191,7 +191,7 @@ interface ManagerInterface extends CheckAccessInterface /** * Revokes a role from a user. - * @param Role $role + * @param Role|Permission $role * @param string|int $userId the user ID (see [[\yii\web\User::id]]) * @return bool whether the revoking is successful */ diff --git a/tests/framework/rbac/ManagerTestCase.php b/tests/framework/rbac/ManagerTestCase.php index cb345ed..98af546 100644 --- a/tests/framework/rbac/ManagerTestCase.php +++ b/tests/framework/rbac/ManagerTestCase.php @@ -454,34 +454,45 @@ abstract class ManagerTestCase extends TestCase $this->assertNotEmpty($this->auth->getRoles()); } - public function testAssignRule() + public function RBACItemsProvider() + { + return [ + [Item::TYPE_ROLE], + [Item::TYPE_PERMISSION] + ]; + } + + /** + * @dataProvider RBACItemsProvider + */ + public function testAssignRule($RBACItemType) { $auth = $this->auth; $userId = 3; $auth->removeAll(); - $role = $auth->createRole('Admin'); - $auth->add($role); - $auth->assign($role, $userId); + $item = $this->createRBACItem($RBACItemType, 'Admin'); + $auth->add($item); + $auth->assign($item, $userId); $this->assertTrue($auth->checkAccess($userId, 'Admin')); // with normal register rule $auth->removeAll(); $rule = new ActionRule(); $auth->add($rule); - $role = $auth->createRole('Reader'); - $role->ruleName = $rule->name; - $auth->add($role); - $auth->assign($role, $userId); + $item = $this->createRBACItem($RBACItemType, 'Reader'); + $item->ruleName = $rule->name; + $auth->add($item); + $auth->assign($item, $userId); $this->assertTrue($auth->checkAccess($userId, 'Reader', ['action' => 'read'])); $this->assertFalse($auth->checkAccess($userId, 'Reader', ['action' => 'write'])); // using rule class name $auth->removeAll(); - $role = $auth->createRole('Reader'); - $role->ruleName = 'yiiunit\framework\rbac\ActionRule'; - $auth->add($role); - $auth->assign($role, $userId); + $item = $this->createRBACItem($RBACItemType, 'Reader'); + $item->ruleName = 'yiiunit\framework\rbac\ActionRule'; + $auth->add($item); + $auth->assign($item, $userId); $this->assertTrue($auth->checkAccess($userId, 'Reader', ['action' => 'read'])); $this->assertFalse($auth->checkAccess($userId, 'Reader', ['action' => 'write'])); @@ -490,35 +501,100 @@ abstract class ManagerTestCase extends TestCase \Yii::$container->set('delete_rule', ['class' => 'yiiunit\framework\rbac\ActionRule', 'action' => 'delete']); \Yii::$container->set('all_rule', ['class' => 'yiiunit\framework\rbac\ActionRule', 'action' => 'all']); - $role = $auth->createRole('Writer'); - $role->ruleName = 'write_rule'; - $auth->add($role); - $auth->assign($role, $userId); + $item = $this->createRBACItem($RBACItemType, 'Writer'); + $item->ruleName = 'write_rule'; + $auth->add($item); + $auth->assign($item, $userId); $this->assertTrue($auth->checkAccess($userId, 'Writer', ['action' => 'write'])); $this->assertFalse($auth->checkAccess($userId, 'Writer', ['action' => 'update'])); - $role = $auth->createRole('Deleter'); - $role->ruleName = 'delete_rule'; - $auth->add($role); - $auth->assign($role, $userId); + $item = $this->createRBACItem($RBACItemType, 'Deleter'); + $item->ruleName = 'delete_rule'; + $auth->add($item); + $auth->assign($item, $userId); $this->assertTrue($auth->checkAccess($userId, 'Deleter', ['action' => 'delete'])); $this->assertFalse($auth->checkAccess($userId, 'Deleter', ['action' => 'update'])); - $role = $auth->createRole('Author'); - $role->ruleName = 'all_rule'; - $auth->add($role); - $auth->assign($role, $userId); + $item = $this->createRBACItem($RBACItemType, 'Author'); + $item->ruleName = 'all_rule'; + $auth->add($item); + $auth->assign($item, $userId); $this->assertTrue($auth->checkAccess($userId, 'Author', ['action' => 'update'])); // update role and rule - $role = $auth->getRole('Reader'); - $role->name = 'AdminPost'; - $role->ruleName = 'all_rule'; - $auth->update('Reader', $role); + $item = $this->getRBACItem($RBACItemType, 'Reader'); + $item->name = 'AdminPost'; + $item->ruleName = 'all_rule'; + $auth->update('Reader', $item); $this->assertTrue($auth->checkAccess($userId, 'AdminPost', ['action' => 'print'])); } /** + * @dataProvider RBACItemsProvider + */ + public function testRevokeRule($RBACItemType) + { + $userId = 3; + $auth = $this->auth; + + $auth->removeAll(); + $item = $this->createRBACItem($RBACItemType, 'Admin'); + $auth->add($item); + $auth->assign($item, $userId); + + $this->assertTrue($auth->revoke($item, $userId)); + $this->assertFalse($auth->checkAccess($userId, 'Admin')); + + $auth->removeAll(); + $rule = new ActionRule(); + $auth->add($rule); + $item = $this->createRBACItem($RBACItemType, 'Reader'); + $item->ruleName = $rule->name; + $auth->add($item); + $auth->assign($item, $userId); + + $this->assertTrue($auth->revoke($item, $userId)); + $this->assertFalse($auth->checkAccess($userId, 'Reader', ['action' => 'read'])); + $this->assertFalse($auth->checkAccess($userId, 'Reader', ['action' => 'write'])); + } + + /** + * Create Role or Permission RBAC item + * @param int $RBACItemType + * @param string $name + * @return Permission|Role + */ + private function createRBACItem($RBACItemType, $name) + { + if ($RBACItemType === Item::TYPE_ROLE) { + return $this->auth->createRole($name); + } + if ($RBACItemType === Item::TYPE_PERMISSION) { + return $this->auth->createPermission($name); + } + + throw new \InvalidArgumentException(); + } + + /** + * Get Role or Permission RBAC item + * @param int $RBACItemType + * @param string $name + * @return Permission|Role + */ + private function getRBACItem($RBACItemType, $name) + { + if ($RBACItemType === Item::TYPE_ROLE) { + return $this->auth->getRole($name); + } + if ($RBACItemType === Item::TYPE_PERMISSION) { + return $this->auth->getPermission($name); + } + + throw new \InvalidArgumentException(); + } + + /** * https://github.com/yiisoft/yii2/issues/10176 * https://github.com/yiisoft/yii2/issues/12681 */ From a731fd41423502f51e4905d879f5c480bd5c43be Mon Sep 17 00:00:00 2001 From: Dmitry Dorogin Date: Mon, 24 Jul 2017 23:25:47 +0300 Subject: [PATCH 010/134] Fixes #14318: Trigger `yiiActiveForm.events.afterValidateAttribute` after updating attribute --- framework/CHANGELOG.md | 1 + framework/assets/yii.activeForm.js | 4 +- tests/js/data/yii.activeForm.html | 3 ++ tests/js/tests/yii.activeForm.test.js | 87 +++++++++++++++++++++++++++++++++++ 4 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 tests/js/data/yii.activeForm.html create mode 100644 tests/js/tests/yii.activeForm.test.js diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 9d4e439..12eacf4 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -40,6 +40,7 @@ Yii Framework 2 Change Log - Bug #14423: Fixed `ArrayHelper::merge` behavior with null values for integer-keyed elements (dmirogin) - Bug #14406: Fixed caching rules in `yii\web\UrlManager` with different ruleConfig configuration (dmirogin) - Chg #7936: Deprecate `yii\base\Object` in favor of `yii\base\BaseObject` for compatibility with PHP 7.2 (rob006, cebe, klimov-paul) +- Bug #14318: Trigger `yiiActiveForm.events.afterValidateAttribute` after updating attribute (dmirogin) - Bug #14493: Fixed getting permissions in `yii\rbac\Dbmanger::getPermissionsByUser` by user with id equals 0 (dmirogin) - Bug #14370: Fixed creating built-in validator in model with same function name (dmirogin) - Bug #14492: Fixed error handler not escaping error info debug mode (samdark) diff --git a/framework/assets/yii.activeForm.js b/framework/assets/yii.activeForm.js index 310d3ab..0678e72 100644 --- a/framework/assets/yii.activeForm.js +++ b/framework/assets/yii.activeForm.js @@ -702,7 +702,6 @@ if (!$.isArray(messages[attribute.id])) { messages[attribute.id] = []; } - $form.trigger(events.afterValidateAttribute, [attribute, messages[attribute.id]]); attribute.status = 1; if ($input.length) { @@ -725,6 +724,9 @@ } attribute.value = getValue($form, attribute); } + + $form.trigger(events.afterValidateAttribute, [attribute, messages[attribute.id]]); + return hasError; }; diff --git a/tests/js/data/yii.activeForm.html b/tests/js/data/yii.activeForm.html new file mode 100644 index 0000000..e31bdc9 --- /dev/null +++ b/tests/js/data/yii.activeForm.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/tests/js/tests/yii.activeForm.test.js b/tests/js/tests/yii.activeForm.test.js new file mode 100644 index 0000000..b7bb61f --- /dev/null +++ b/tests/js/tests/yii.activeForm.test.js @@ -0,0 +1,87 @@ +var assert = require('chai').assert; +var sinon; +var jsdom = require('mocha-jsdom'); + +var fs = require('fs'); +var vm = require('vm'); + +describe('yii.activeForm', function () { + var yiiActiveFormPath = 'framework/assets/yii.activeForm.js'; + var yiiPath = 'framework/assets/yii.js'; + var jQueryPath = 'vendor/bower-asset/jquery/dist/jquery.js'; + var $; + var $mainForm; + + function registerYii() { + var code = fs.readFileSync(yiiPath); + var script = new vm.Script(code); + var sandbox = {window: window, jQuery: $}; + var context = new vm.createContext(sandbox); + script.runInContext(context); + return sandbox.window.yii; + } + + function registerTestableCode() { + var yii = registerYii(); + var code = fs.readFileSync(yiiActiveFormPath); + var script = new vm.Script(code); + var context = new vm.createContext({ + window: window, + document: window.document, + yii: yii + }); + script.runInContext(context); + } + + var activeFormHtml = fs.readFileSync('tests/js/data/yii.activeForm.html', 'utf-8'); + var html = '' + activeFormHtml + ''; + + jsdom({ + html: html, + src: fs.readFileSync(jQueryPath, 'utf-8') + }); + + before(function () { + $ = window.$; + registerTestableCode(); + sinon = require('sinon'); + }); + + beforeEach(function () { + $mainForm = $('#main'); + }); + + describe('events', function () { + describe('afterValidateAttribute', function () { + var afterValidateAttributeEventSpy; + var dataForAssert; + + before(function () { + afterValidateAttributeEventSpy = sinon.spy(function (event, data) { + dataForAssert = data.value; + }); + }); + + after(function () { + afterValidateAttributeEventSpy.reset(); + }); + + it('should update attribute value', function () { + var inputId = 'nameInput', + $input = $('#' + inputId); + $mainForm.yiiActiveForm([ + { + id : inputId, + input: '#' + inputId + } + ]).on('afterValidateAttribute', afterValidateAttributeEventSpy); + + // Set new value, update attribute + $input.val('newValue'); + $mainForm.yiiActiveForm('updateAttribute', inputId); + + assert.equal('newValue', dataForAssert); + }); + }); + }); +}); \ No newline at end of file From 2466d397ae0cefbc048be0586487527037612e66 Mon Sep 17 00:00:00 2001 From: Klimov Paul Date: Tue, 25 Jul 2017 16:17:37 +0300 Subject: [PATCH 011/134] Fixed `yii\validators\ExistValidator` and `yii\validators\UniqueValidator` throw exception in case they are set for `yii\db\ActiveRecord` with `$targetClass` pointing to NOSQL ActiveRecord --- framework/CHANGELOG.md | 2 ++ framework/validators/ExistValidator.php | 21 ++++----------------- framework/validators/UniqueValidator.php | 21 ++++----------------- 3 files changed, 10 insertions(+), 34 deletions(-) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 12eacf4..5de97a8 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -3,7 +3,9 @@ Yii Framework 2 Change Log 2.0.13 under development ------------------------ + - Bug #14523: Added `yii\web\MultipartFormDataParser::$force` option allowing to enforce parsing even on 'POST' request (klimov-paul) +- Bug #14533: Fixed `yii\validators\ExistValidator` and `yii\validators\UniqueValidator` throw exception in case they are set for `yii\db\ActiveRecord` with `$targetClass` pointing to NOSQL ActiveRecord (klimov-paul) - Bug #14449: Fix PHP 7.2 compatibility bugs and add explicit closure support in `yii\base\Application` (dynasource) - Bug #7890: Allow `migrate/mark` to mark history at the point of the base migration (cebe) - Bug #14206: `MySqlMutex`, `PgsqlMutex` and `OracleMutex` now use `useMaster()` to ensure lock is aquired on the same DB server (cebe, ryusoft) diff --git a/framework/validators/ExistValidator.php b/framework/validators/ExistValidator.php index 31072f8..979a3c6 100644 --- a/framework/validators/ExistValidator.php +++ b/framework/validators/ExistValidator.php @@ -148,11 +148,13 @@ class ExistValidator extends Validator $conditions = [$targetAttribute => $model->$attribute]; } - if (!$model instanceof ActiveRecord) { + $targetModelClass = $this->getTargetClass($model); + if (!is_subclass_of($targetModelClass, 'yii\db\ActiveRecord')) { return $conditions; } - return $this->prefixConditions($model, $conditions); + /** @var ActiveRecord $targetModelClass */ + return $this->applyTableAlias($targetModelClass::find(), $conditions); } /** @@ -235,19 +237,4 @@ class ExistValidator extends Validator } return $prefixedConditions; } - - /** - * Prefix conditions with aliases - * - * @param ActiveRecord $model - * @param array $conditions - * @return array - */ - private function prefixConditions($model, $conditions) - { - $targetModelClass = $this->getTargetClass($model); - - /** @var ActiveRecord $targetModelClass */ - return $this->applyTableAlias($targetModelClass::find(), $conditions); - } } diff --git a/framework/validators/UniqueValidator.php b/framework/validators/UniqueValidator.php index 5b569a2..ec247b5 100644 --- a/framework/validators/UniqueValidator.php +++ b/framework/validators/UniqueValidator.php @@ -243,11 +243,13 @@ class UniqueValidator extends Validator $conditions = [$targetAttribute => $model->$attribute]; } - if (!$model instanceof ActiveRecord) { + $targetModelClass = $this->getTargetClass($model); + if (!is_subclass_of($targetModelClass, 'yii\db\ActiveRecord')) { return $conditions; } - return $this->prefixConditions($model, $conditions); + /** @var ActiveRecord $targetModelClass */ + return $this->applyTableAlias($targetModelClass::find(), $conditions); } /** @@ -302,19 +304,4 @@ class UniqueValidator extends Validator } return $prefixedConditions; } - - /** - * Prefix conditions with aliases - * - * @param ActiveRecord $model - * @param array $conditions - * @return array - */ - private function prefixConditions($model, $conditions) - { - $targetModelClass = $this->getTargetClass($model); - - /** @var ActiveRecord $targetModelClass */ - return $this->applyTableAlias($targetModelClass::find(), $conditions); - } } From 2ada766d83d5e980cc6cd47ae4e6a65c86b9165f Mon Sep 17 00:00:00 2001 From: rhertogh Date: Tue, 25 Jul 2017 15:36:14 +0200 Subject: [PATCH 012/134] Restored part of original Yii::trace documentation (#14534) [skip ci] Restored original warning in the Yii::trace documentation that this method will only log a message when the application is in debug mode. --- framework/BaseYii.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/framework/BaseYii.php b/framework/BaseYii.php index 64a2d9c..7c67f32 100644 --- a/framework/BaseYii.php +++ b/framework/BaseYii.php @@ -381,7 +381,8 @@ class BaseYii /** * Logs a trace message. * Trace messages are logged mainly for development purpose to see - * the execution work flow of some code. + * the execution work flow of some code. This method will only log + * a message when the application is in debug mode. * @param string|array $message the message to be logged. This can be a simple string or a more * complex data structure, such as array. * @param string $category the category of the message. From e472b4e29c0dae8282b7e87070f4ac362d0548d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Arnauts?= Date: Wed, 26 Jul 2017 13:35:20 +0200 Subject: [PATCH 013/134] Fixes #14525: Fixed 2.0.12 regression of loading of global fixtures trough `yii fixture/load` --- framework/CHANGELOG.md | 1 + framework/console/controllers/FixtureController.php | 4 +++- .../framework/console/controllers/FixtureControllerTest.php | 12 ++++++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 5de97a8..67cf814 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -4,6 +4,7 @@ Yii Framework 2 Change Log 2.0.13 under development ------------------------ +- Bug #14525: Fixed 2.0.12 regression of loading of global fixtures trough `yii fixture/load` (michaelarnauts) - Bug #14523: Added `yii\web\MultipartFormDataParser::$force` option allowing to enforce parsing even on 'POST' request (klimov-paul) - Bug #14533: Fixed `yii\validators\ExistValidator` and `yii\validators\UniqueValidator` throw exception in case they are set for `yii\db\ActiveRecord` with `$targetClass` pointing to NOSQL ActiveRecord (klimov-paul) - Bug #14449: Fix PHP 7.2 compatibility bugs and add explicit closure support in `yii\base\Application` (dynasource) diff --git a/framework/console/controllers/FixtureController.php b/framework/console/controllers/FixtureController.php index f47bd79..d74034b 100644 --- a/framework/console/controllers/FixtureController.php +++ b/framework/console/controllers/FixtureController.php @@ -453,10 +453,12 @@ class FixtureController extends Controller $isNamespaced = (strpos($fixture, '\\') !== false); // replace linux' path slashes to namespace backslashes, in case if $fixture is non-namespaced relative path $fixture = str_replace('/', '\\', $fixture); - $fullClassName = $isNamespaced ? $fixture . 'Fixture' : $this->namespace . '\\' . $fixture . 'Fixture'; + $fullClassName = $isNamespaced ? $fixture : $this->namespace . '\\' . $fixture; if (class_exists($fullClassName)) { $config[] = $fullClassName; + } elseif (class_exists($fullClassName . 'Fixture')) { + $config[] = $fullClassName . 'Fixture'; } } diff --git a/tests/framework/console/controllers/FixtureControllerTest.php b/tests/framework/console/controllers/FixtureControllerTest.php index 66759f7..e166a2d 100644 --- a/tests/framework/console/controllers/FixtureControllerTest.php +++ b/tests/framework/console/controllers/FixtureControllerTest.php @@ -57,6 +57,18 @@ class FixtureControllerTest extends TestCase $this->assertCount(1, FixtureStorage::$firstFixtureData, 'first fixture data should be loaded'); } + public function testLoadGlobalFixtureWithFixture() + { + $this->_fixtureController->globalFixtures = [ + '\yiiunit\data\console\controllers\fixtures\GlobalFixture', + ]; + + $this->_fixtureController->actionLoad(['First']); + + $this->assertCount(1, FixtureStorage::$globalFixturesData, 'global fixture data should be loaded'); + $this->assertCount(1, FixtureStorage::$firstFixtureData, 'first fixture data should be loaded'); + } + public function testUnloadGlobalFixture() { $this->_fixtureController->globalFixtures = [ From 20c0e9391a7c7cbeaabeaf464882514ab6d7e29e Mon Sep 17 00:00:00 2001 From: BowenChen Date: Thu, 27 Jul 2017 18:54:22 +0800 Subject: [PATCH 014/134] Updated a slip of a pen on db-dao.md in guide-zh-CN (#14541) [skip ci] --- docs/guide-zh-CN/db-dao.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/guide-zh-CN/db-dao.md b/docs/guide-zh-CN/db-dao.md index 0aa6036..87afeaa 100644 --- a/docs/guide-zh-CN/db-dao.md +++ b/docs/guide-zh-CN/db-dao.md @@ -67,14 +67,14 @@ return [ // ... 'db' => [ 'class' => 'yii\db\Connection', - 'dsn' => 'mysql:host=localhost;dbname=mydatabase', + 'dsn' => 'mysql:host=localhost;dbname=mydatabase', 'username' => 'root', 'password' => '', 'charset' => 'utf8', ], 'secondDb' => [ 'class' => 'yii\db\Connection', - 'dsn' => 'sqlite:/path/to/database/file', + 'dsn' => 'sqlite:/path/to/database/file', ], ], // ... @@ -283,7 +283,7 @@ try { ``` >注意你使用的数据库必须支持`Savepoints`才能正确地执行,以上代码在所有关系数据中都可以执行,但是只有支持`Savepoints`才能保证安全性。 -Yii 也支持为事务设置隔离级别`isolation levels`,当执行事务时会使用数据库默认的隔离级别,你也可以为事物指定隔离级别. +Yii 也支持为事务设置隔离级别`isolation levels`,当执行事务时会使用数据库默认的隔离级别,你也可以为事务指定隔离级别. Yii 提供了以下常量作为常用的隔离级别 - [[\yii\db\Transaction::READ_UNCOMMITTED]] - 允许读取改变了的还未提交的数据,可能导致脏读、不可重复读和幻读 From 203e3d3441b1347575b1708c9287cd3779070e69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Va=C5=A1=C3=AD=C4=8Dek?= Date: Thu, 27 Jul 2017 20:28:13 +0200 Subject: [PATCH 015/134] Updates Czech translations (#14547) [skip ci] --- framework/messages/cs/yii.php | 44 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/framework/messages/cs/yii.php b/framework/messages/cs/yii.php index 352eb43..0906491 100644 --- a/framework/messages/cs/yii.php +++ b/framework/messages/cs/yii.php @@ -23,6 +23,41 @@ * NOTE: this file must be saved in UTF-8 encoding. */ return [ + ' and ' => ' a ', + 'Powered by {yii}' => 'Běží na {yii}', + 'The combination {values} of {attributes} has already been taken.' => 'Kombinace {values} pro {attributes} je již použitá.', + 'Unknown alias: -{name}' => 'Neznámý alias: -{name}', + 'Yii Framework' => 'Yii Framework', + '{attribute} contains wrong subnet mask.' => '{attribute} obsahuje neplatnou masku podsítě.', + '{attribute} is not in the allowed range.' => '{attribute} není v povoleném rozsahu.', + '{attribute} must be a valid IP address.' => '{attribute} musí být platná IP adresa.', + '{attribute} must be an IP address with specified subnet.' => '{attribute} musí být IP adresa se zadanou podsítí.', + '{attribute} must be equal to "{compareValueOrAttribute}".' => '{attribute} se musí rovnat "{compareValueOrAttribute}".', + '{attribute} must be greater than "{compareValueOrAttribute}".' => '{attribute} musí být větší než "{compareValueOrAttribute}".', + '{attribute} must be greater than or equal to "{compareValueOrAttribute}".' => '{attribute} musí být větší nebo roven "{compareValueOrAttribute}".', + '{attribute} must be less than "{compareValueOrAttribute}".' => '{attribute} musí být menší než "{compareValueOrAttribute}".', + '{attribute} must be less than or equal to "{compareValueOrAttribute}".' => '{attribute} musí být menší nebo roven "{compareValueOrAttribute}".', + '{attribute} must not be a subnet.' => '{attribute} nesmí být podsíť.', + '{attribute} must not be an IPv4 address.' => '{attribute} nesmí být IPv4 adresa.', + '{attribute} must not be an IPv6 address.' => '{attribute} nesmí být IPv6 adresa.', + '{attribute} must not be equal to "{compareValueOrAttribute}".' => '{attribute} se nesmí rovnat "{compareValueOrAttribute}".', + '{delta, plural, =1{1 day} other{# days}}' => '{delta, plural, =1{1 den} few{# dny} other{# dní}}', + '{delta, plural, =1{1 hour} other{# hours}}' => '{delta, plural, =1{1 hodina} few{# hodiny} other{# hodin}}', + '{delta, plural, =1{1 minute} other{# minutes}}' => '{delta, plural, =1{1 minuta} few{# minuty} other{# minut}}', + '{delta, plural, =1{1 month} other{# months}}' => '{delta, plural, =1{1 měsíc} few{# měsíce} other{# měsíců}}', + '{delta, plural, =1{1 second} other{# seconds}}' => '{delta, plural, =1{1 sekunda} few{# sekundy} other{# sekund}}', + '{delta, plural, =1{1 year} other{# years}}' => '{delta, plural, =1{1 rok} few{# roky} other{# let}}', + '{nFormatted} B' => '{nFormatted} B', + '{nFormatted} GB' => '{nFormatted} GB', + '{nFormatted} GiB' => '{nFormatted} GiB', + '{nFormatted} KB' => '{nFormatted} KB', + '{nFormatted} KiB' => '{nFormatted} KiB', + '{nFormatted} MB' => '{nFormatted} MB', + '{nFormatted} MiB' => '{nFormatted} MiB', + '{nFormatted} PB' => '{nFormatted} PB', + '{nFormatted} PiB' => '{nFormatted} PiB', + '{nFormatted} TB' => '{nFormatted} TB', + '{nFormatted} TiB' => '{nFormatted} TiB', '(not set)' => '(není zadáno)', 'An internal server error occurred.' => 'Vyskytla se vnitřní chyba serveru.', 'Are you sure you want to delete this item?' => 'Opravdu chcete smazat tuto položku?', @@ -35,8 +70,6 @@ return [ 'Missing required arguments: {params}' => 'Chybí povinné argumenty: {params}', 'Missing required parameters: {params}' => 'Chybí povinné parametry: {params}', 'No' => 'Ne', - 'No help for unknown command "{command}".' => 'K neznámému příkazu "{command}" neexistuje nápověda.', - 'No help for unknown sub-command "{command}".' => 'K neznámému pod-příkazu "{command}" neexistuje nápověda.', 'No results found.' => 'Nenalezeny žádné záznamy.', 'Only files with these MIME types are allowed: {mimeTypes}.' => 'Povolené jsou pouze soubory následujících MIME typů: {mimeTypes}.', 'Only files with these extensions are allowed: {extensions}.' => 'Povolené jsou pouze soubory s následujícími příponami: {extensions}.', @@ -56,7 +89,6 @@ return [ 'The verification code is incorrect.' => 'Nesprávný ověřovací kód.', 'Total {count, number} {count, plural, one{item} other{items}}.' => 'Celkem {count, number} {count, plural, one{záznam} few{záznamy} other{záznamů}}.', 'Unable to verify your data submission.' => 'Nebylo možné ověřit odeslané údaje.', - 'Unknown command "{command}".' => 'Neznámý příkaz "{command}".', 'Unknown option: --{name}' => 'Neznámá volba: --{name}', 'Update' => 'Upravit', 'View' => 'Náhled', @@ -81,14 +113,8 @@ return [ '{attribute} must be a string.' => '{attribute} musí být řetězec.', '{attribute} must be an integer.' => '{attribute} musí být celé číslo.', '{attribute} must be either "{true}" or "{false}".' => '{attribute} musí být buď "{true}" nebo "{false}".', - '{attribute} must be greater than "{compareValue}".' => '{attribute} musí být větší než "{compareValue}".', - '{attribute} must be greater than or equal to "{compareValue}".' => '{attribute} musí být větší nebo roven "{compareValue}".', - '{attribute} must be less than "{compareValue}".' => '{attribute} musí být menší než "{compareValue}".', - '{attribute} must be less than or equal to "{compareValue}".' => '{attribute} musí být menší nebo roven "{compareValue}".', '{attribute} must be no greater than {max}.' => '{attribute} nesmí být větší než {max}.', '{attribute} must be no less than {min}.' => '{attribute} nesmí být menší než {min}.', - '{attribute} must be repeated exactly.' => 'Údaj {attribute} je třeba zopakovat přesně.', - '{attribute} must not be equal to "{compareValue}".' => '{attribute} se nesmí rovnat "{compareValue}".', '{attribute} should contain at least {min, number} {min, plural, one{character} other{characters}}.' => '{attribute} musí obsahovat alespoň {min, number} {min, plural, one{znak} few{znaky} other{znaků}}.', '{attribute} should contain at most {max, number} {max, plural, one{character} other{characters}}.' => '{attribute} může obsahovat nanejvýš {max, number} {max, plural, one{znak} few{znaky} other{znaků}}.', '{attribute} should contain {length, number} {length, plural, one{character} other{characters}}.' => '{attribute} musí obsahovat {length, number} {length, plural, one{znak} few{znaky} other{znaků}}.', From 37bd0229ada34896b3f812ae1c40d18536206b51 Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Fri, 28 Jul 2017 12:34:35 +0200 Subject: [PATCH 016/134] fixed migration example in guide do not register the `app\migrations` path twice when using namespaces. fixes #14536 --- docs/guide/db-migrations.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/guide/db-migrations.md b/docs/guide/db-migrations.md index e7ae53a..9a41119 100644 --- a/docs/guide/db-migrations.md +++ b/docs/guide/db-migrations.md @@ -909,6 +909,7 @@ return [ 'controllerMap' => [ 'migrate' => [ 'class' => 'yii\console\controllers\MigrateController', + 'migrationPath' => null, // disable non-namespaced migrations if app\migrations is listed below 'migrationNamespaces' => [ 'app\migrations', // Common migrations for the whole application 'module\migrations', // Migrations for the specific project's module From 8f2d9baf98003c912d91c1fad962e0835ff01f3f Mon Sep 17 00:00:00 2001 From: Alexey Rogachev Date: Sat, 29 Jul 2017 17:37:29 +0600 Subject: [PATCH 017/134] Fixes #14186, Fixes #14510: yiiActiveForm regressions - #14186: Forced validation in `yiiActiveForm` do not trigger `afterValidate` event - #14510: The state of a form is always "not validated" when using forced validation in `yiiActiveForm` --- framework/CHANGELOG.md | 2 + framework/assets/yii.activeForm.js | 10 ++--- tests/js/data/yii.activeForm.html | 4 +- tests/js/tests/yii.activeForm.test.js | 81 ++++++++++++++++++++++++----------- 4 files changed, 65 insertions(+), 32 deletions(-) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 67cf814..181b5c8 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -112,6 +112,8 @@ Yii Framework 2 Change Log - Bug #14074: Fixed default value of `yii\console\controllers\FixtureController::$globalFixtures` to contain valid class name (lynicidn) - Bug #14094: Fixed bug when single `yii\web\UrlManager::createUrl()` call my result multiple calls of `yii\web\UrlRule::createUrl()` for the same rule (rossoneri) - Bug #14133: Fixed bug when calculating timings with mixed nested profile begin and end in `yii\log\Logger::calculateTimings()` (bizley) +- Bug #14186: Forced validation in `yiiActiveForm` do not trigger `afterValidate` event (arogachev) +- Bug #14510: The state of a form is always "not validated" when using forced validation in `yiiActiveForm` (arogachev) - Enh #4793: `yii\filters\AccessControl` now can be used without `user` component (bizley) - Enh #4999: Added support for wildcards at `yii\filters\AccessRule::$controllers` (klimov-paul) - Enh #5108: `yii\validators\DateValidator` now resets `$timestampAttribute` value on empty validated attribute value (klimov-paul) diff --git a/framework/assets/yii.activeForm.js b/framework/assets/yii.activeForm.js index 0678e72..b4417fa 100644 --- a/framework/assets/yii.activeForm.js +++ b/framework/assets/yii.activeForm.js @@ -306,9 +306,9 @@ needAjaxValidation = false, messages = {}, deferreds = deferredArray(), - submitting = data.submitting && !forceValidate; + submitting = data.submitting; - if (data.submitting) { + if (submitting) { var event = $.Event(events.beforeValidate); $form.trigger(event, [messages, deferreds]); @@ -391,7 +391,7 @@ }); } else if (data.submitting) { // delay callback so that the form can be submitted without problem - setTimeout(function () { + window.setTimeout(function () { updateInputs($form, messages, submitting); }, 200); } else { @@ -435,7 +435,7 @@ // Because we bind directly to a form reset event instead of a reset button (that may not exist), // when this function is executed form input values have not been reset yet. // Therefore we do the actual reset work through setTimeout. - setTimeout(function () { + window.setTimeout(function () { $.each(data.attributes, function () { // Without setTimeout() we would get the input values that are not reset yet. this.value = getValue($form, this); @@ -535,7 +535,7 @@ if (data.settings.timer !== undefined) { clearTimeout(data.settings.timer); } - data.settings.timer = setTimeout(function () { + data.settings.timer = window.setTimeout(function () { if (data.submitting || $form.is(':hidden')) { return; } diff --git a/tests/js/data/yii.activeForm.html b/tests/js/data/yii.activeForm.html index e31bdc9..912c549 100644 --- a/tests/js/data/yii.activeForm.html +++ b/tests/js/data/yii.activeForm.html @@ -1,3 +1,3 @@ -
- + +
diff --git a/tests/js/tests/yii.activeForm.test.js b/tests/js/tests/yii.activeForm.test.js index b7bb61f..3981599 100644 --- a/tests/js/tests/yii.activeForm.test.js +++ b/tests/js/tests/yii.activeForm.test.js @@ -10,7 +10,7 @@ describe('yii.activeForm', function () { var yiiPath = 'framework/assets/yii.js'; var jQueryPath = 'vendor/bower-asset/jquery/dist/jquery.js'; var $; - var $mainForm; + var $activeForm; function registerYii() { var code = fs.readFileSync(yiiPath); @@ -25,11 +25,7 @@ describe('yii.activeForm', function () { var yii = registerYii(); var code = fs.readFileSync(yiiActiveFormPath); var script = new vm.Script(code); - var context = new vm.createContext({ - window: window, - document: window.document, - yii: yii - }); + var context = new vm.createContext({window: window, document: window.document, yii: yii}); script.runInContext(context); } @@ -47,41 +43,76 @@ describe('yii.activeForm', function () { sinon = require('sinon'); }); - beforeEach(function () { - $mainForm = $('#main'); + describe('validate method', function () { + var windowSetTimeoutStub; + var afterValidateSpy; + + beforeEach(function () { + windowSetTimeoutStub = sinon.stub(window, 'setTimeout', function (callback) { + callback(); + }); + afterValidateSpy = sinon.spy(); + }); + + afterEach(function () { + windowSetTimeoutStub.restore(); + afterValidateSpy.reset(); + }); + + describe('with forceValidate parameter set to true', function () { + it('should trigger manual form validation', function () { + var inputId = 'name'; + + $activeForm = $('#w0'); + $activeForm.yiiActiveForm([ + { + id: inputId, + input: '#' + inputId + } + ]).on('afterValidate', afterValidateSpy); + + $activeForm.yiiActiveForm('validate', true); + // https://github.com/yiisoft/yii2/issues/14510 + assert.isTrue($activeForm.data('yiiActiveForm').validated); + // https://github.com/yiisoft/yii2/issues/14186 + assert.isTrue(afterValidateSpy.calledOnce); + }); + }); }); describe('events', function () { describe('afterValidateAttribute', function () { - var afterValidateAttributeEventSpy; - var dataForAssert; + var afterValidateAttributeSpy; + var eventData; before(function () { - afterValidateAttributeEventSpy = sinon.spy(function (event, data) { - dataForAssert = data.value; + afterValidateAttributeSpy = sinon.spy(function (event, data) { + eventData = data; }); }); after(function () { - afterValidateAttributeEventSpy.reset(); + afterValidateAttributeSpy.reset(); }); - it('should update attribute value', function () { - var inputId = 'nameInput', - $input = $('#' + inputId); - $mainForm.yiiActiveForm([ + // https://github.com/yiisoft/yii2/issues/14318 + + it('should allow to get updated attribute value', function () { + var inputId = 'name'; + var $input = $('#' + inputId); + + $activeForm = $('#w0'); + $activeForm.yiiActiveForm([ { - id : inputId, + id: inputId, input: '#' + inputId } - ]).on('afterValidateAttribute', afterValidateAttributeEventSpy); - - // Set new value, update attribute - $input.val('newValue'); - $mainForm.yiiActiveForm('updateAttribute', inputId); + ]).on('afterValidateAttribute', afterValidateAttributeSpy); - assert.equal('newValue', dataForAssert); + $input.val('New value'); + $activeForm.yiiActiveForm('updateAttribute', inputId); + assert.equal('New value', eventData.value); }); }); }); -}); \ No newline at end of file +}); From 459721d471a3acd8614ce548f003b5b825e1f100 Mon Sep 17 00:00:00 2001 From: PowerGamer1 Date: Sun, 30 Jul 2017 00:49:33 +0300 Subject: [PATCH 018/134] Fixes #14471: `ContentNegotiator` will always set one of the configured server response formats even if the client does not accept any of them --- framework/CHANGELOG.md | 1 + framework/filters/ContentNegotiator.php | 15 ++++++++------- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 181b5c8..b22c8c7 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -4,6 +4,7 @@ Yii Framework 2 Change Log 2.0.13 under development ------------------------ +- Bug #14471: `ContentNegotiator` will always set one of the configured server response formats even if the client does not accept any of them (PowerGamer1) - Bug #14525: Fixed 2.0.12 regression of loading of global fixtures trough `yii fixture/load` (michaelarnauts) - Bug #14523: Added `yii\web\MultipartFormDataParser::$force` option allowing to enforce parsing even on 'POST' request (klimov-paul) - Bug #14533: Fixed `yii\validators\ExistValidator` and `yii\validators\UniqueValidator` throw exception in case they are set for `yii\db\ActiveRecord` with `$targetClass` pointing to NOSQL ActiveRecord (klimov-paul) diff --git a/framework/filters/ContentNegotiator.php b/framework/filters/ContentNegotiator.php index 622a692..07c0dd9 100644 --- a/framework/filters/ContentNegotiator.php +++ b/framework/filters/ContentNegotiator.php @@ -195,14 +195,15 @@ class ContentNegotiator extends ActionFilter implements BootstrapInterface } } + foreach ($this->formats as $type => $format) { + $response->format = $format; + $response->acceptMimeType = $type; + $response->acceptParams = []; + break; + } + if (isset($types['*/*'])) { - // return the first format - foreach ($this->formats as $type => $format) { - $response->format = $this->formats[$type]; - $response->acceptMimeType = $type; - $response->acceptParams = []; - return; - } + return; } throw new UnsupportedMediaTypeHttpException('None of your requested content types is supported.'); From e18e98ffb3f0f2c369e935da4a5cfccd74f4a49a Mon Sep 17 00:00:00 2001 From: Dmitry Dorogin Date: Sun, 30 Jul 2017 13:27:32 +0300 Subject: [PATCH 019/134] Fixes #14363: Added `yii\widgets\LinkPager::$linkContainerOptions` and possibility to override tag in `yii\widgets\LinkPager::$options` --- framework/CHANGELOG.md | 1 + framework/widgets/LinkPager.php | 18 ++++++-- tests/framework/widgets/LinkPagerTest.php | 69 ++++++++++++++++++++++--------- 3 files changed, 65 insertions(+), 23 deletions(-) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index b22c8c7..64e05b1 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -50,6 +50,7 @@ Yii Framework 2 Change Log - Bug #14492: Fixed error handler not escaping error info debug mode (samdark) - Chg #14487: Changed i18n message error to warning (dmirogin) - Enh #7823: Added `yii\filters\AjaxFilter` filter (dmirogin) +- Enh #14363: Added `yii\widgets\LinkPager::$linkContainerOptions` and possibility to override tag in `yii\widgets\LinkPager::$options` (dmirogin) 2.0.12 June 05, 2017 -------------------- diff --git a/framework/widgets/LinkPager.php b/framework/widgets/LinkPager.php index a7e5a2c..1bdfb12 100644 --- a/framework/widgets/LinkPager.php +++ b/framework/widgets/LinkPager.php @@ -42,6 +42,11 @@ class LinkPager extends Widget */ public $options = ['class' => 'pagination']; /** + * @var array HTML attributes which will be applied to all link containers + * @since 2.0.13 + */ + public $linkContainerOptions = []; + /** * @var array HTML attributes for the link in a pager container tag. * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. */ @@ -212,7 +217,9 @@ class LinkPager extends Widget $buttons[] = $this->renderPageButton($lastPageLabel, $pageCount - 1, $this->lastPageCssClass, $currentPage >= $pageCount - 1, false); } - return Html::tag('ul', implode("\n", $buttons), $this->options); + $options = $this->options; + $tag = ArrayHelper::remove($options, 'tag', 'ul'); + return Html::tag($tag, implode("\n", $buttons), $options); } /** @@ -227,7 +234,10 @@ class LinkPager extends Widget */ protected function renderPageButton($label, $page, $class, $disabled, $active) { - $options = ['class' => empty($class) ? $this->pageCssClass : $class]; + $options = $this->linkContainerOptions; + $linkWrapTag = ArrayHelper::remove($options, 'tag', 'li'); + Html::addCssClass($options, empty($class) ? $this->pageCssClass : $class); + if ($active) { Html::addCssClass($options, $this->activePageCssClass); } @@ -235,12 +245,12 @@ class LinkPager extends Widget Html::addCssClass($options, $this->disabledPageCssClass); $tag = ArrayHelper::remove($this->disabledListItemSubTagOptions, 'tag', 'span'); - return Html::tag('li', Html::tag($tag, $label, $this->disabledListItemSubTagOptions), $options); + return Html::tag($linkWrapTag, Html::tag($tag, $label, $this->disabledListItemSubTagOptions), $options); } $linkOptions = $this->linkOptions; $linkOptions['data-page'] = $page; - return Html::tag('li', Html::a($label, $this->pagination->createUrl($page), $linkOptions), $options); + return Html::tag($linkWrapTag, Html::a($label, $this->pagination->createUrl($page), $linkOptions), $options); } /** diff --git a/tests/framework/widgets/LinkPagerTest.php b/tests/framework/widgets/LinkPagerTest.php index e3af651..7a59c3e 100644 --- a/tests/framework/widgets/LinkPagerTest.php +++ b/tests/framework/widgets/LinkPagerTest.php @@ -8,6 +8,7 @@ namespace yiiunit\framework\widgets; use yii\data\Pagination; +use yii\helpers\StringHelper; use yii\widgets\LinkPager; /** @@ -27,13 +28,24 @@ class LinkPagerTest extends \yiiunit\TestCase ]); } - public function testFirstLastPageLabels() + /** + * Get pagination + * @param int $page + * @return Pagination + */ + private function getPagination($page) { $pagination = new Pagination(); - $pagination->setPage(5); + $pagination->setPage($page); $pagination->totalCount = 500; $pagination->route = 'test'; + return $pagination; + } + + public function testFirstLastPageLabels() + { + $pagination = $this->getPagination(5); $output = LinkPager::widget([ 'pagination' => $pagination, 'firstPageLabel' => true, @@ -64,13 +76,8 @@ class LinkPagerTest extends \yiiunit\TestCase public function testDisabledPageElementOptions() { - $pagination = new Pagination(); - $pagination->setPage(0); - $pagination->totalCount = 50; - $pagination->route = 'test'; - $output = LinkPager::widget([ - 'pagination' => $pagination, + 'pagination' => $this->getPagination(0), 'disabledListItemSubTagOptions' => ['class' => 'foo-bar'], ]); @@ -79,13 +86,8 @@ class LinkPagerTest extends \yiiunit\TestCase public function testDisabledPageElementOptionsWithTagOption() { - $pagination = new Pagination(); - $pagination->setPage(0); - $pagination->totalCount = 50; - $pagination->route = 'test'; - $output = LinkPager::widget([ - 'pagination' => $pagination, + 'pagination' => $this->getPagination(0), 'disabledListItemSubTagOptions' => ['class' => 'foo-bar', 'tag' => 'div'], ]); @@ -94,11 +96,7 @@ class LinkPagerTest extends \yiiunit\TestCase public function testDisableCurrentPageButton() { - $pagination = new Pagination(); - $pagination->setPage(5); - $pagination->totalCount = 500; - $pagination->route = 'test'; - + $pagination = $this->getPagination(5); $output = LinkPager::widget([ 'pagination' => $pagination, 'disableCurrentPageButton' => false, @@ -113,4 +111,37 @@ class LinkPagerTest extends \yiiunit\TestCase static::assertContains('
  • 6
  • ', $output); } + + public function testOptionsWithTagOption() + { + $output = LinkPager::widget([ + 'pagination' => $this->getPagination(5), + 'options' => [ + 'tag' => 'div', + ] + ]); + + $this->assertTrue(StringHelper::startsWith($output, '
    ')); + $this->assertTrue(StringHelper::endsWith($output, '
    ')); + } + + public function testLinkWrapOptions() + { + $output = LinkPager::widget([ + 'pagination' => $this->getPagination(1), + 'linkContainerOptions' => [ + 'tag' => 'div', + 'class' => 'my-class' + ] + ]); + + $this->assertContains( + '', + $output + ); + $this->assertContains( + '', + $output + ); + } } From e87db6751f2e68a1d13e7ee3c481d6239eae0b5e Mon Sep 17 00:00:00 2001 From: rlgy <1032998920@qq.com> Date: Mon, 31 Jul 2017 18:31:22 +0800 Subject: [PATCH 020/134] Complete translation for `guide-zh-CN/input-forms.md` and `guide-zh-CN/input-multiple-models.md` . (#14563) [skip ci] --- docs/guide-zh-CN/input-forms.md | 208 ++++++++++++++++++++++++++++++ docs/guide-zh-CN/input-multiple-models.md | 86 ++++++++---- 2 files changed, 272 insertions(+), 22 deletions(-) create mode 100644 docs/guide-zh-CN/input-forms.md diff --git a/docs/guide-zh-CN/input-forms.md b/docs/guide-zh-CN/input-forms.md new file mode 100644 index 0000000..454aa1a --- /dev/null +++ b/docs/guide-zh-CN/input-forms.md @@ -0,0 +1,208 @@ +创建表单 +============== +基于活动记录(ActiveRecord)的表单:ActiveForm +----------------------- +在yii中使用表单的主要方式是通过[[yii\widgets\ActiveForm]]。当某个表单是基于一个模型时,应该首选这种方式。此外,在[[yii\helpers\Html]]中有很多实用的方法为表单添加按钮和帮助文档。 + +在客户端显示的表单,大多数情况下都有一个相应的[模型](structure-models.md),用来在服务器上验证其输入的数据(可在[输入验证](input-validation.md)一节获取关于验证的细节)。当创建一个基于模型的表单时,第一步是定义模型本身。该模型可以是一个基于[活动记录](db-active-record.md)的类,表示数据库中的数据,也可以是一个基于通用模型的类(继承自[[yii\base\Model]]),来获取任意的输入数据,如登录表单。 + +> Tip: 如果一个表单的输入域与数据库的字段不匹配,或者它存在只适用于它的特殊的格式或者方法,则最好为它创建一个单独的继承自[[yii\base\Model]]的模型。 + +在接下来的例子中,我们展示了通用模型如何用于登录表单: + +```php + 'login-form', + 'options' => ['class' => 'form-horizontal'], +]) ?> + field($model, 'username') ?> + field($model, 'password')->passwordInput() ?> + +
    +
    + 'btn btn-primary']) ?> +
    +
    + +``` + +### 用 `begin()` 和 `end()` 包裹 + +在上面的代码中,[[yii\widgets\ActiveForm::begin()|ActiveForm::begin()]] 不仅创建了一个表单实例,同时也标志的表单的开始。所有在[[yii\widgets\ActiveForm::begin()|ActiveForm::begin()]]与[[yii\widgets\ActiveForm::end()|ActiveForm::end()]]之中的内容都会被HTML中的 `
    `标签包裹。与其他小部件一样,你可以制定一些选项,通过传递数组到到 `begin` 中来配置小部件。在这种情况下,一个额外的CSS类和ID会在 `` 标签中使用。要查看更多可用的选项,请查看API文档的 [[yii\widgets\ActiveForm]]。 + +### ActiveField . +为了在表单中创建表单元素与元素的标签,以及任意适用的Javascript验证,需要使用[[yii\widgets\ActiveForm::field()]|ActiveForm::field()]方法,其返回一个[[yii\widgets\ActiveField]]实例。当直接输出该方法时,结果是一个普通的(文本)输入。要自定义输出,可以附加上[[yii\widgets\ActiveField|ActiveField]]的其他方法来一起调用: + +```php +// 密码输入框 +field($model, 'password')->passwordInput() ?> +// 增加提示与自定义标签 +field($model, 'username')->textInput()->hint('Please enter your name')->label('Name') ?> +// 创建一个HTML5邮件输入元素 +field($model, 'email')->input('email') ?> +``` + +它会通过[[yii\widgets\ActiveField|ActiveField]]中定义的表单字段来创建`