From 83e353dd41b6c48dade0168d9853b496a3897728 Mon Sep 17 00:00:00 2001 From: Paul Klimov Date: Thu, 24 Oct 2013 16:55:48 +0300 Subject: [PATCH 01/17] 'ViewContext' interface extracted, files render workflow refactored to use this interface. --- framework/yii/base/Controller.php | 25 ++++------------ framework/yii/base/View.php | 59 +++++++++++++++++++++++++++++--------- framework/yii/base/ViewContext.php | 30 +++++++++++++++++++ framework/yii/base/Widget.php | 26 ++++------------- 4 files changed, 87 insertions(+), 53 deletions(-) create mode 100644 framework/yii/base/ViewContext.php diff --git a/framework/yii/base/Controller.php b/framework/yii/base/Controller.php index 9a168da..79c1f5e 100644 --- a/framework/yii/base/Controller.php +++ b/framework/yii/base/Controller.php @@ -25,7 +25,7 @@ use Yii; * @author Qiang Xue * @since 2.0 */ -class Controller extends Component +class Controller extends Component implements ViewContext { /** * @event ActionEvent an event raised right before executing a controller action. @@ -305,8 +305,7 @@ class Controller extends Component */ public function render($view, $params = []) { - $viewFile = $this->findViewFile($view); - $output = $this->getView()->renderFile($viewFile, $params, $this); + $output = $this->getView()->render($view, $params, $this); $layoutFile = $this->findLayoutFile(); if ($layoutFile !== false) { return $this->getView()->renderFile($layoutFile, ['content' => $output], $this); @@ -325,8 +324,7 @@ class Controller extends Component */ public function renderPartial($view, $params = []) { - $viewFile = $this->findViewFile($view); - return $this->getView()->renderFile($viewFile, $params, $this); + return $this->getView()->render($view, $params, $this); } /** @@ -382,22 +380,9 @@ class Controller extends Component * on how to specify this parameter. * @return string the view file path. Note that the file may not exist. */ - protected function findViewFile($view) + public function findViewFile($view) { - if (strncmp($view, '@', 1) === 0) { - // e.g. "@app/views/main" - $file = Yii::getAlias($view); - } elseif (strncmp($view, '//', 2) === 0) { - // e.g. "//layouts/main" - $file = Yii::$app->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); - } elseif (strncmp($view, '/', 1) === 0) { - // e.g. "/site/index" - $file = $this->module->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); - } else { - $file = $this->getViewPath() . DIRECTORY_SEPARATOR . $view; - } - - return pathinfo($file, PATHINFO_EXTENSION) === '' ? $file . '.php' : $file; + return $this->getViewPath() . DIRECTORY_SEPARATOR . $view; } /** diff --git a/framework/yii/base/View.php b/framework/yii/base/View.php index 68b1094..1cb9a59 100644 --- a/framework/yii/base/View.php +++ b/framework/yii/base/View.php @@ -89,8 +89,7 @@ class View extends Component /** - * @var object the context under which the [[renderFile()]] method is being invoked. - * This can be a controller, a widget, or any other object. + * @var ViewContext the context under which the [[renderFile()]] method is being invoked. */ public $context; /** @@ -196,29 +195,63 @@ class View extends Component /** * Renders a view. * - * This method delegates the call to the [[context]] object: + * The view to be rendered can be specified in one of the following formats: * - * - If [[context]] is a controller, the [[Controller::renderPartial()]] method will be called; - * - If [[context]] is a widget, the [[Widget::render()]] method will be called; - * - Otherwise, an InvalidCallException exception will be thrown. + * - path alias (e.g. "@app/views/site/index"); + * - absolute path within application (e.g. "//site/index"): the view name starts with double slashes. + * The actual view file will be looked for under the [[Application::viewPath|view path]] of the application. + * - absolute path within current module (e.g. "/site/index"): the view name starts with a single slash. + * The actual view file will be looked for under the [[Module::viewPath|view path]] of [[module]]. + * - resolving any other format will be performed via [[ViewContext::findViewFile()]]. * * @param string $view the view name. Please refer to [[Controller::findViewFile()]] * and [[Widget::findViewFile()]] on how to specify this parameter. * @param array $params the parameters (name-value pairs) that will be extracted and made available in the view file. + * @param object $context the context that the view should use for rendering the view. If null, + * existing [[context]] will be used. * @return string the rendering result - * @throws InvalidCallException if [[context]] is neither a controller nor a widget. * @throws InvalidParamException if the view cannot be resolved or the view file does not exist. * @see renderFile */ - public function render($view, $params = []) + public function render($view, $params = [], $context = null) { - if ($this->context instanceof Controller) { - return $this->context->renderPartial($view, $params); - } elseif ($this->context instanceof Widget) { - return $this->context->render($view, $params); + $viewFile = $this->findViewFile($view, $context); + return $this->renderFile($viewFile, $params, $context); + } + + /** + * Finds the view file based on the given view name. + * @param string $view the view name or the path alias of the view file. Please refer to [[render()]] + * on how to specify this parameter. + * @param object $context the context that the view should be used to search the view file. If null, + * existing [[context]] will be used. + * @throws InvalidCallException if [[context]] is required and invalid. + * @return string the view file path. Note that the file may not exist. + */ + protected function findViewFile($view, $context = null) + { + if (strncmp($view, '@', 1) === 0) { + // e.g. "@app/views/main" + $file = Yii::getAlias($view); + } elseif (strncmp($view, '//', 2) === 0) { + // e.g. "//layouts/main" + $file = Yii::$app->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); + } elseif (strncmp($view, '/', 1) === 0 && Yii::$app->controller !== null) { + // e.g. "/site/index" + $file = Yii::$app->controller->module->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); } else { - throw new InvalidCallException('View::render() is not supported for the current context.'); + // context required + if ($context === null) { + $context = $this->context; + } + if ($context instanceof ViewContext) { + $file = $context->findViewFile($view); + } else { + throw new InvalidCallException('Current context is not supported.'); + } } + + return pathinfo($file, PATHINFO_EXTENSION) === '' ? $file . '.php' : $file; } /** diff --git a/framework/yii/base/ViewContext.php b/framework/yii/base/ViewContext.php new file mode 100644 index 0000000..4072ffc --- /dev/null +++ b/framework/yii/base/ViewContext.php @@ -0,0 +1,30 @@ + + * @since 2.0 + */ +interface ViewContext +{ + /** + * Finds the view file based on the given view name. + * @param string $view the view name. + * @return string the view file path. Note that the file may not exist. + */ + public function findViewFile($view); +} \ No newline at end of file diff --git a/framework/yii/base/Widget.php b/framework/yii/base/Widget.php index 14c4cba..69da6a9 100644 --- a/framework/yii/base/Widget.php +++ b/framework/yii/base/Widget.php @@ -20,7 +20,7 @@ use Yii; * @author Qiang Xue * @since 2.0 */ -class Widget extends Component +class Widget extends Component implements ViewContext { /** * @var integer a counter used to generate [[id]] for widgets. @@ -167,8 +167,7 @@ class Widget extends Component */ public function render($view, $params = []) { - $viewFile = $this->findViewFile($view); - return $this->getView()->renderFile($viewFile, $params, $this); + return $this->getView()->render($view, $params, $this); } /** @@ -197,25 +196,12 @@ class Widget extends Component /** * Finds the view file based on the given view name. - * @param string $view the view name or the path alias of the view file. Please refer to [[render()]] - * on how to specify this parameter. + * File will be searched under [[viewPath]] directory. + * @param string $view the view name. * @return string the view file path. Note that the file may not exist. */ - protected function findViewFile($view) + public function findViewFile($view) { - if (strncmp($view, '@', 1) === 0) { - // e.g. "@app/views/main" - $file = Yii::getAlias($view); - } elseif (strncmp($view, '//', 2) === 0) { - // e.g. "//layouts/main" - $file = Yii::$app->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); - } elseif (strncmp($view, '/', 1) === 0 && Yii::$app->controller !== null) { - // e.g. "/site/index" - $file = Yii::$app->controller->module->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); - } else { - $file = $this->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); - } - - return pathinfo($file, PATHINFO_EXTENSION) === '' ? $file . '.php' : $file; + return $this->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); } } From e0294ea72465a4ca3a0f8b6e916847607d6fd7fe Mon Sep 17 00:00:00 2001 From: Paul Klimov Date: Thu, 24 Oct 2013 17:20:19 +0300 Subject: [PATCH 02/17] 'ViewContext' renamed to 'ViewContextInterface'. --- framework/yii/base/Controller.php | 2 +- framework/yii/base/View.php | 4 ++-- framework/yii/base/ViewContext.php | 30 ----------------------------- framework/yii/base/ViewContextInterface.php | 30 +++++++++++++++++++++++++++++ framework/yii/base/Widget.php | 2 +- 5 files changed, 34 insertions(+), 34 deletions(-) delete mode 100644 framework/yii/base/ViewContext.php create mode 100644 framework/yii/base/ViewContextInterface.php diff --git a/framework/yii/base/Controller.php b/framework/yii/base/Controller.php index 79c1f5e..d2f491c 100644 --- a/framework/yii/base/Controller.php +++ b/framework/yii/base/Controller.php @@ -25,7 +25,7 @@ use Yii; * @author Qiang Xue * @since 2.0 */ -class Controller extends Component implements ViewContext +class Controller extends Component implements ViewContextInterface { /** * @event ActionEvent an event raised right before executing a controller action. diff --git a/framework/yii/base/View.php b/framework/yii/base/View.php index 1cb9a59..bc5b972 100644 --- a/framework/yii/base/View.php +++ b/framework/yii/base/View.php @@ -89,7 +89,7 @@ class View extends Component /** - * @var ViewContext the context under which the [[renderFile()]] method is being invoked. + * @var ViewContextInterface the context under which the [[renderFile()]] method is being invoked. */ public $context; /** @@ -244,7 +244,7 @@ class View extends Component if ($context === null) { $context = $this->context; } - if ($context instanceof ViewContext) { + if ($context instanceof ViewContextInterface) { $file = $context->findViewFile($view); } else { throw new InvalidCallException('Current context is not supported.'); diff --git a/framework/yii/base/ViewContext.php b/framework/yii/base/ViewContext.php deleted file mode 100644 index 4072ffc..0000000 --- a/framework/yii/base/ViewContext.php +++ /dev/null @@ -1,30 +0,0 @@ - - * @since 2.0 - */ -interface ViewContext -{ - /** - * Finds the view file based on the given view name. - * @param string $view the view name. - * @return string the view file path. Note that the file may not exist. - */ - public function findViewFile($view); -} \ No newline at end of file diff --git a/framework/yii/base/ViewContextInterface.php b/framework/yii/base/ViewContextInterface.php new file mode 100644 index 0000000..4237eca --- /dev/null +++ b/framework/yii/base/ViewContextInterface.php @@ -0,0 +1,30 @@ + + * @since 2.0 + */ +interface ViewContextInterface +{ + /** + * Finds the view file based on the given view name. + * @param string $view the view name. + * @return string the view file path. Note that the file may not exist. + */ + public function findViewFile($view); +} \ No newline at end of file diff --git a/framework/yii/base/Widget.php b/framework/yii/base/Widget.php index 69da6a9..b6e0cd0 100644 --- a/framework/yii/base/Widget.php +++ b/framework/yii/base/Widget.php @@ -20,7 +20,7 @@ use Yii; * @author Qiang Xue * @since 2.0 */ -class Widget extends Component implements ViewContext +class Widget extends Component implements ViewContextInterface { /** * @var integer a counter used to generate [[id]] for widgets. From 449eb8424748b02d7dc0abe6550f48e41df234cf Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Mon, 28 Oct 2013 01:54:31 +0100 Subject: [PATCH 03/17] refactored message formatter --- framework/yii/i18n/FallbackMessageFormatter.php | 306 --------------------- framework/yii/i18n/I18N.php | 10 +- framework/yii/i18n/MessageFormatter.php | 289 +++++++++++++++++-- .../i18n/FallbackMessageFormatterTest.php | 32 +-- tests/unit/framework/i18n/MessageFormatterTest.php | 40 +-- 5 files changed, 284 insertions(+), 393 deletions(-) delete mode 100644 framework/yii/i18n/FallbackMessageFormatter.php diff --git a/framework/yii/i18n/FallbackMessageFormatter.php b/framework/yii/i18n/FallbackMessageFormatter.php deleted file mode 100644 index 17928f5..0000000 --- a/framework/yii/i18n/FallbackMessageFormatter.php +++ /dev/null @@ -1,306 +0,0 @@ - - * @since 2.0 - */ -class FallbackMessageFormatter -{ - private $_locale; - private $_pattern; - private $_errorMessage = ''; - private $_errorCode = 0; - - /** - * Constructs a new Message Formatter - * @link http://php.net/manual/en/messageformatter.create.php - * @param string $locale The locale to use when formatting arguments - * @param string $pattern The pattern string to stick arguments into. - */ - public function __construct($locale, $pattern) - { - $this->_locale = $locale; - $this->_pattern = $pattern; - } - - /** - * Constructs a new Message Formatter - * @link http://php.net/manual/en/messageformatter.create.php - * @param string $locale The locale to use when formatting arguments - * @param string $pattern The pattern string to stick arguments into. - * @return MessageFormatter The formatter object - */ - public static function create($locale, $pattern) - { - return new static($locale, $pattern); - } - - /** - * Format the message - * @link http://php.net/manual/en/messageformatter.format.php - * @param array $args Arguments to insert into the format string - * @return string The formatted string, or `FALSE` if an error occurred - */ - public function format($args) - { - return static::formatMessage($this->_locale, $this->_pattern, $args); - } - - /** - * Quick format message - * @link http://php.net/manual/en/messageformatter.formatmessage.php - * @param string $locale The locale to use for formatting locale-dependent parts - * @param string $pattern The pattern string to insert things into. - * @param array $args The array of values to insert into the format string - * @return string The formatted pattern string or `FALSE` if an error occurred - */ - public static function formatMessage($locale, $pattern, $args) - { - if (($tokens = static::tokenizePattern($pattern)) === false) { - return false; - } - foreach($tokens as $i => $token) { - if (is_array($token)) { - if (($tokens[$i] = static::parseToken($token, $args, $locale)) === false) { - return false; - } - } - } - return implode('', $tokens); - } - - /** - * Tokenizes a pattern by separating normal text from replaceable patterns - * @param string $pattern patter to tokenize - * @return array|bool array of tokens or false on failure - */ - private static function tokenizePattern($pattern) - { - $depth = 1; - if (($start = $pos = mb_strpos($pattern, '{')) === false) { - return [$pattern]; - } - $tokens = [mb_substr($pattern, 0, $pos)]; - while(true) { - $open = mb_strpos($pattern, '{', $pos + 1); - $close = mb_strpos($pattern, '}', $pos + 1); - if ($open === false && $close === false) { - break; - } - if ($open === false) { - $open = mb_strlen($pattern); - } - if ($close > $open) { - $depth++; - $pos = $open; - } else { - $depth--; - $pos = $close; - } - if ($depth == 0) { - $tokens[] = explode(',', mb_substr($pattern, $start + 1, $pos - $start - 1), 3); - $start = $pos + 1; - $tokens[] = mb_substr($pattern, $start, $open - $start); - $start = $open; - } - } - if ($depth != 0) { - return false; - } - return $tokens; - } - - /** - * Parses a token - * @param array $token the token to parse - * @param array $args arguments to replace - * @param string $locale the locale - * @return bool|string parsed token or false on failure - * @throws \yii\base\NotSupportedException when unsupported formatting is used. - */ - private static function parseToken($token, $args, $locale) - { - $param = trim($token[0]); - if (isset($args[$param])) { - $arg = $args[$param]; - } else { - return '{' . implode(',', $token) . '}'; - } - $type = isset($token[1]) ? trim($token[1]) : 'none'; - switch($type) - { - case 'number': - case 'date': - case 'time': - case 'spellout': - case 'ordinal': - case 'duration': - case 'choice': - case 'selectordinal': - throw new NotSupportedException("Message format '$type' is not supported. You have to install PHP intl extension to use this feature."); - case 'none': return $arg; - case 'select': - /* http://icu-project.org/apiref/icu4c/classicu_1_1SelectFormat.html - selectStyle = (selector '{' message '}')+ - */ - $select = static::tokenizePattern($token[2]); - $c = count($select); - $message = false; - for($i = 0; $i + 1 < $c; $i++) { - if (is_array($select[$i]) || !is_array($select[$i + 1])) { - return false; - } - $selector = trim($select[$i++]); - if ($message === false && $selector == 'other' || $selector == $arg) { - $message = implode(',', $select[$i]); - } - } - if ($message !== false) { - return static::formatMessage($locale, $message, $args); - } - break; - case 'plural': - /* http://icu-project.org/apiref/icu4c/classicu_1_1PluralFormat.html - pluralStyle = [offsetValue] (selector '{' message '}')+ - offsetValue = "offset:" number - selector = explicitValue | keyword - explicitValue = '=' number // adjacent, no white space in between - keyword = [^[[:Pattern_Syntax:][:Pattern_White_Space:]]]+ - message: see MessageFormat - */ - $plural = static::tokenizePattern($token[2]); - $c = count($plural); - $message = false; - $offset = 0; - for($i = 0; $i + 1 < $c; $i++) { - if (is_array($plural[$i]) || !is_array($plural[$i + 1])) { - return false; - } - $selector = trim($plural[$i++]); - if ($i == 1 && substr($selector, 0, 7) == 'offset:') { - $offset = (int) trim(mb_substr($selector, 7, ($pos = mb_strpos(str_replace(["\n", "\r", "\t"], ' ', $selector), ' ', 7)) - 7)); - $selector = trim(mb_substr($selector, $pos + 1)); - } - if ($message === false && $selector == 'other' || - $selector[0] == '=' && (int) mb_substr($selector, 1) == $arg || - $selector == 'zero' && $arg - $offset == 0 || - $selector == 'one' && $arg - $offset == 1 || - $selector == 'two' && $arg - $offset == 2 - ) { - $message = implode(',', str_replace('#', $arg - $offset, $plural[$i])); - } - } - if ($message !== false) { - return static::formatMessage($locale, $message, $args); - } - break; - } - return false; - } - - /** - * Parse input string according to pattern - * @link http://php.net/manual/en/messageformatter.parse.php - * @param string $value The string to parse - * @return array An array containing the items extracted, or `FALSE` on error - */ - public function parse($value) - { - throw new NotSupportedException('You have to install PHP intl extension to use this feature.'); - } - - /** - * Quick parse input string - * @link http://php.net/manual/en/messageformatter.parsemessage.php - * @param string $locale The locale to use for parsing locale-dependent parts - * @param string $pattern The pattern with which to parse the `value`. - * @param string $source The string to parse, conforming to the `pattern`. - * @return array An array containing items extracted, or `FALSE` on error - */ - public static function parseMessage($locale, $pattern, $source) - { - throw new NotSupportedException('You have to install PHP intl extension to use this feature.'); - } - - /** - * Set the pattern used by the formatter - * @link http://php.net/manual/en/messageformatter.setpattern.php - * @param string $pattern The pattern string to use in this message formatter. - * @return bool `TRUE` on success or `FALSE` on failure. - */ - public function setPattern($pattern) - { - $this->_pattern = $pattern; - return true; - } - - /** - * Get the pattern used by the formatter - * @link http://php.net/manual/en/messageformatter.getpattern.php - * @return string The pattern string for this message formatter - */ - public function getPattern() - { - return $this->_pattern; - } - - /** - * Get the locale for which the formatter was created. - * @link http://php.net/manual/en/messageformatter.getlocale.php - * @return string The locale name - */ - public function getLocale() - { - return $this->_locale; - } - - /** - * Get the error code from last operation - * @link http://php.net/manual/en/messageformatter.geterrorcode.php - * @return int The error code, one of UErrorCode values. Initial value is U_ZERO_ERROR. - */ - public function getErrorCode() - { - return $this->_errorCode; - } - - /** - * Get the error text from the last operation - * @link http://php.net/manual/en/messageformatter.geterrormessage.php - * @return string Description of the last error. - */ - public function getErrorMessage() - { - return $this->_errorMessage; - } -} - -if (!class_exists('MessageFormatter', false)) { - class_alias('yii\\i18n\\FallbackMessageFormatter', 'MessageFormatter'); - define('YII_INTL_MESSAGE_FALLBACK', true); -} diff --git a/framework/yii/i18n/I18N.php b/framework/yii/i18n/I18N.php index b2b99b5..61b0f77 100644 --- a/framework/yii/i18n/I18N.php +++ b/framework/yii/i18n/I18N.php @@ -78,7 +78,7 @@ class I18N extends Component } /** - * Formats a message using using [[MessageFormatter]]. + * Formats a message using [[MessageFormatter]]. * * @param string $message the message to be formatted. * @param array $params the parameters that will be used to replace the corresponding placeholders in the message. @@ -93,12 +93,8 @@ class I18N extends Component } if (preg_match('~{\s*[\d\w]+\s*,~u', $message)) { - $formatter = new MessageFormatter($language, $message); - if ($formatter === null) { - Yii::warning("Unable to format message in language '$language': $message."); - return $message; - } - $result = $formatter->format($params); + $formatter = new MessageFormatter(); + $result = $formatter->format($language, $message, $params); if ($result === false) { $errorMessage = $formatter->getErrorMessage(); Yii::warning("Formatting message for language '$language' failed with error: $errorMessage. The message being formatted was: $message."); diff --git a/framework/yii/i18n/MessageFormatter.php b/framework/yii/i18n/MessageFormatter.php index 9f198c9..55dc73d 100644 --- a/framework/yii/i18n/MessageFormatter.php +++ b/framework/yii/i18n/MessageFormatter.php @@ -7,10 +7,8 @@ namespace yii\i18n; -if (!class_exists('MessageFormatter', false)) { - require_once(__DIR__ . '/FallbackMessageFormatter.php'); -} -defined('YII_INTL_MESSAGE_FALLBACK') || define('YII_INTL_MESSAGE_FALLBACK', false); +use yii\base\Component; +use yii\base\NotSupportedException; /** * MessageFormatter enhances the message formatter class provided by PHP intl extension. @@ -25,53 +23,133 @@ defined('YII_INTL_MESSAGE_FALLBACK') || define('YII_INTL_MESSAGE_FALLBACK', fals * However it is highly recommended that you install [PHP intl extension](http://php.net/manual/en/book.intl.php) if you want * to use MessageFormatter features. * + * FallbackMessageFormatter is a fallback implementation for the PHP intl MessageFormatter that is + * used in case PHP intl extension is not installed. + * + * Do not use this class directly. Use [[MessageFormatter]] instead, which will automatically detect + * installed version of PHP intl and use the fallback if it is not installed. + * + * It is highly recommended that you install [PHP intl extension](http://php.net/manual/en/book.intl.php) if you want to use + * MessageFormatter features. + * + * This implementation only supports to following message formats: + * - plural formatting for english + * - select format + * - simple parameters + * + * The pattern also does NOT support the ['apostrophe-friendly' syntax](http://www.php.net/manual/en/messageformatter.formatmessage.php). + * + * Messages that can be parsed are also not necessarily compatible with the ICU formatting method so do not expect messages that work without intl installed + * to work with intl + * * @author Alexander Makarov * @author Carsten Brandt * @since 2.0 */ -class MessageFormatter extends \MessageFormatter +class MessageFormatter extends Component { + private $_errorCode = 0; + private $_errorMessage = ''; + /** - * Format the message. + * Get the error text from the last operation + * @link http://php.net/manual/en/messageformatter.geterrorcode.php + * @return string Description of the last error. + */ + public function getErrorCode() + { + return $this->_errorCode; + } + + /** + * Get the error text from the last operation + * @link http://php.net/manual/en/messageformatter.geterrormessage.php + * @return string Description of the last error. + */ + public function getErrorMessage() + { + return $this->_errorMessage; + } + + /** + * Formats a message via ICU message format. * - * @link http://php.net/manual/en/messageformatter.format.php - * @param array $args Arguments to insert into the format string. - * @return string|boolean The formatted string, or false if an error occurred. + * @link http://php.net/manual/en/messageformatter.formatmessage.php + * @param string $language The locale to use for formatting locale-dependent parts + * @param string $message The pattern string to insert things into. + * @param array $params The array of values to insert into the format string + * @return string|boolean The formatted pattern string or `FALSE` if an error occurred */ - public function format($args) + public function format($language, $message, $params) { - if ($args === []) { - return $this->getPattern(); + $this->_errorCode = 0; + $this->_errorMessage = ''; + + if ($params === []) { + return $message; + } + + if (!class_exists('MessageFormatter', false)) { + return $this->fallbackFormat($language, $message, $params); } - if (version_compare(PHP_VERSION, '5.5.0', '<') && !YII_INTL_MESSAGE_FALLBACK) { - $pattern = self::replaceNamedArguments($this->getPattern(), $args); - $this->setPattern($pattern); - $args = array_values($args); + if (version_compare(PHP_VERSION, '5.5.0', '<')) { + $message = $this->replaceNamedArguments($message, $params); + $params = array_values($params); + } + $formatter = new \MessageFormatter($language, $message); + if ($formatter === null) { + $this->_errorMessage = "Unable to format message in language '$language'."; + return false; + } + $result = $formatter->format($params); + if ($result === false) { + $this->_errorCode = $formatter->getErrorCode(); + $this->_errorMessage = $formatter->getErrorMessage(); + return false; + } else { + return $result; } - return parent::format($args); } /** - * Quick format message. + * Parse input string according to a message pattern * - * @link http://php.net/manual/en/messageformatter.formatmessage.php - * @param string $locale The locale to use for formatting locale-dependent parts. - * @param string $pattern The pattern string to insert things into. - * @param array $args The array of values to insert into the format string. - * @return string|boolean The formatted pattern string or false if an error occurred. + * @link http://www.php.net/manual/en/messageformatter.parsemessage.php + * @param string $language The locale to use for formatting locale-dependent parts + * @param string $pattern The pattern with which to parse the message. + * @param array $message The message to parse, conforming to the pattern. + * @return string|boolean An array containing items extracted, or `FALSE` on error. + * @throws \yii\base\NotSupportedException when PHP intl extension is not installed. */ - public static function formatMessage($locale, $pattern, $args) + public function parse($language, $pattern, $message) { - if ($args === []) { - return $pattern; + $this->_errorCode = 0; + $this->_errorMessage = ''; + + if (!class_exists('MessageFormatter', false)) { + throw new NotSupportedException('You have to install PHP intl extension to use this feature.'); } - if (version_compare(PHP_VERSION, '5.5.0', '<') && !YII_INTL_MESSAGE_FALLBACK) { - $pattern = self::replaceNamedArguments($pattern, $args); - $args = array_values($args); + // TODO try to support named args +// if (version_compare(PHP_VERSION, '5.5.0', '<')) { +// $message = $this->replaceNamedArguments($message, $params); +// $params = array_values($params); +// } + $formatter = new \MessageFormatter($language, $pattern); + if ($formatter === null) { + $this->_errorMessage = "Unable to parse message in language '$language'."; + return false; } - return parent::formatMessage($locale, $pattern, $args); + $result = $formatter->parse($message); + if ($result === false) { + $this->_errorCode = $formatter->getErrorCode(); + $this->_errorMessage = $formatter->getErrorMessage(); + return false; + } else { + return $result; + } + } /** @@ -124,4 +202,155 @@ class MessageFormatter extends \MessageFormatter } return $pattern; } + + /** + * Fallback implementation for MessageFormatter::formatMessage + * @param string $language The locale to use for formatting locale-dependent parts + * @param string $message The pattern string to insert things into. + * @param array $params The array of values to insert into the format string + * @return string|boolean The formatted pattern string or `FALSE` if an error occurred + */ + protected function fallbackFormat($locale, $pattern, $args = []) + { + if (($tokens = $this->tokenizePattern($pattern)) === false) { + return false; + } + foreach($tokens as $i => $token) { + if (is_array($token)) { + if (($tokens[$i] = $this->parseToken($token, $args, $locale)) === false) { + return false; + } + } + } + return implode('', $tokens); + } + + /** + * Tokenizes a pattern by separating normal text from replaceable patterns + * @param string $pattern patter to tokenize + * @return array|bool array of tokens or false on failure + */ + private function tokenizePattern($pattern) + { + $depth = 1; + if (($start = $pos = mb_strpos($pattern, '{')) === false) { + return [$pattern]; + } + $tokens = [mb_substr($pattern, 0, $pos)]; + while(true) { + $open = mb_strpos($pattern, '{', $pos + 1); + $close = mb_strpos($pattern, '}', $pos + 1); + if ($open === false && $close === false) { + break; + } + if ($open === false) { + $open = mb_strlen($pattern); + } + if ($close > $open) { + $depth++; + $pos = $open; + } else { + $depth--; + $pos = $close; + } + if ($depth == 0) { + $tokens[] = explode(',', mb_substr($pattern, $start + 1, $pos - $start - 1), 3); + $start = $pos + 1; + $tokens[] = mb_substr($pattern, $start, $open - $start); + $start = $open; + } + } + if ($depth != 0) { + return false; + } + return $tokens; + } + + /** + * Parses a token + * @param array $token the token to parse + * @param array $args arguments to replace + * @param string $locale the locale + * @return bool|string parsed token or false on failure + * @throws \yii\base\NotSupportedException when unsupported formatting is used. + */ + private function parseToken($token, $args, $locale) + { + $param = trim($token[0]); + if (isset($args[$param])) { + $arg = $args[$param]; + } else { + return '{' . implode(',', $token) . '}'; + } + $type = isset($token[1]) ? trim($token[1]) : 'none'; + switch($type) + { + case 'number': + case 'date': + case 'time': + case 'spellout': + case 'ordinal': + case 'duration': + case 'choice': + case 'selectordinal': + throw new NotSupportedException("Message format '$type' is not supported. You have to install PHP intl extension to use this feature."); + case 'none': return $arg; + case 'select': + /* http://icu-project.org/apiref/icu4c/classicu_1_1SelectFormat.html + selectStyle = (selector '{' message '}')+ + */ + $select = static::tokenizePattern($token[2]); + $c = count($select); + $message = false; + for($i = 0; $i + 1 < $c; $i++) { + if (is_array($select[$i]) || !is_array($select[$i + 1])) { + return false; + } + $selector = trim($select[$i++]); + if ($message === false && $selector == 'other' || $selector == $arg) { + $message = implode(',', $select[$i]); + } + } + if ($message !== false) { + return $this->fallbackFormat($locale, $message, $args); + } + break; + case 'plural': + /* http://icu-project.org/apiref/icu4c/classicu_1_1PluralFormat.html + pluralStyle = [offsetValue] (selector '{' message '}')+ + offsetValue = "offset:" number + selector = explicitValue | keyword + explicitValue = '=' number // adjacent, no white space in between + keyword = [^[[:Pattern_Syntax:][:Pattern_White_Space:]]]+ + message: see MessageFormat + */ + $plural = static::tokenizePattern($token[2]); + $c = count($plural); + $message = false; + $offset = 0; + for($i = 0; $i + 1 < $c; $i++) { + if (is_array($plural[$i]) || !is_array($plural[$i + 1])) { + return false; + } + $selector = trim($plural[$i++]); + if ($i == 1 && substr($selector, 0, 7) == 'offset:') { + $offset = (int) trim(mb_substr($selector, 7, ($pos = mb_strpos(str_replace(["\n", "\r", "\t"], ' ', $selector), ' ', 7)) - 7)); + $selector = trim(mb_substr($selector, $pos + 1)); + } + if ($message === false && $selector == 'other' || + $selector[0] == '=' && (int) mb_substr($selector, 1) == $arg || + $selector == 'zero' && $arg - $offset == 0 || + $selector == 'one' && $arg - $offset == 1 || + $selector == 'two' && $arg - $offset == 2 + ) { + $message = implode(',', str_replace('#', $arg - $offset, $plural[$i])); + } + } + if ($message !== false) { + return $this->fallbackFormat($locale, $message, $args); + } + break; + } + return false; + } } diff --git a/tests/unit/framework/i18n/FallbackMessageFormatterTest.php b/tests/unit/framework/i18n/FallbackMessageFormatterTest.php index 407ec7f..044c450 100644 --- a/tests/unit/framework/i18n/FallbackMessageFormatterTest.php +++ b/tests/unit/framework/i18n/FallbackMessageFormatterTest.php @@ -7,7 +7,7 @@ namespace yiiunit\framework\i18n; -use yii\i18n\FallbackMessageFormatter; +use yii\i18n\MessageFormatter; use yiiunit\TestCase; /** @@ -115,19 +115,10 @@ _MSG_ /** * @dataProvider patterns */ - public function testNamedArgumentsStatic($pattern, $expected, $args) - { - $result = FallbackMessageFormatter::formatMessage('en_US', $pattern, $args); - $this->assertEquals($expected, $result); - } - - /** - * @dataProvider patterns - */ public function testNamedArgumentsObject($pattern, $expected, $args) { - $formatter = new FallbackMessageFormatter('en_US', $pattern); - $result = $formatter->format($args); + $formatter = new FallbackMessageFormatter(); + $result = $formatter->format('en_US', $pattern, $args); $this->assertEquals($expected, $result, $formatter->getErrorMessage()); } @@ -135,7 +126,8 @@ _MSG_ { $expected = '{'.self::SUBJECT.'} is '.self::N_VALUE; - $result = FallbackMessageFormatter::formatMessage('en_US', '{'.self::SUBJECT.'} is {'.self::N.'}', [ + $formatter = new FallbackMessageFormatter(); + $result = $formatter->format('en_US', '{'.self::SUBJECT.'} is {'.self::N.'}', [ self::N => self::N_VALUE, ]); @@ -145,11 +137,17 @@ _MSG_ public function testNoParams() { $pattern = '{'.self::SUBJECT.'} is '.self::N; - $result = FallbackMessageFormatter::formatMessage('en_US', $pattern, []); - $this->assertEquals($pattern, $result); - $formatter = new FallbackMessageFormatter('en_US', $pattern); - $result = $formatter->format([]); + $formatter = new FallbackMessageFormatter(); + $result = $formatter->format('en_US', $pattern, []); $this->assertEquals($pattern, $result, $formatter->getErrorMessage()); } +} + +class FallbackMessageFormatter extends MessageFormatter +{ + public function fallbackFormat($locale, $pattern, $args = []) + { + return parent::fallbackFormat($locale, $pattern, $args); + } } \ No newline at end of file diff --git a/tests/unit/framework/i18n/MessageFormatterTest.php b/tests/unit/framework/i18n/MessageFormatterTest.php index c392ce0..5daaab9 100644 --- a/tests/unit/framework/i18n/MessageFormatterTest.php +++ b/tests/unit/framework/i18n/MessageFormatterTest.php @@ -122,19 +122,10 @@ _MSG_ /** * @dataProvider patterns */ - public function testNamedArgumentsStatic($pattern, $expected, $args) - { - $result = MessageFormatter::formatMessage('en_US', $pattern, $args); - $this->assertEquals($expected, $result, intl_get_error_message()); - } - - /** - * @dataProvider patterns - */ public function testNamedArgumentsObject($pattern, $expected, $args) { - $formatter = new MessageFormatter('en_US', $pattern); - $result = $formatter->format($args); + $formatter = new MessageFormatter(); + $result = $formatter->format('en_US', $pattern, $args); $this->assertEquals($expected, $result, $formatter->getErrorMessage()); } @@ -142,36 +133,19 @@ _MSG_ { $expected = '{'.self::SUBJECT.'} is '.self::N_VALUE; - $result = MessageFormatter::formatMessage('en_US', '{'.self::SUBJECT.'} is {'.self::N.', number}', [ + $formatter = new MessageFormatter(); + $result = $formatter->format('en_US', '{'.self::SUBJECT.'} is {'.self::N.', number}', [ self::N => self::N_VALUE, ]); - $this->assertEquals($expected, $result, intl_get_error_message()); - } - - /** - * When instantiating a MessageFormatter with invalid pattern it should be null with default settings. - * It will be IntlException if intl.use_exceptions=1 and PHP 5.5 or newer or an error if intl.error_level is not 0. - */ - public function testNullConstructor() - { - if(ini_get('intl.use_exceptions')) { - $this->setExpectedException('IntlException'); - } - - if (!ini_get('intl.error_level') || ini_get('intl.use_exceptions')) { - $this->assertNull(new MessageFormatter('en_US', '')); - } + $this->assertEquals($expected, $result, $formatter->getErrorMessage()); } public function testNoParams() { $pattern = '{'.self::SUBJECT.'} is '.self::N; - $result = MessageFormatter::formatMessage('en_US', $pattern, []); - $this->assertEquals($pattern, $result, intl_get_error_message()); - - $formatter = new MessageFormatter('en_US', $pattern); - $result = $formatter->format([]); + $formatter = new MessageFormatter(); + $result = $formatter->format('en_US', $pattern, []); $this->assertEquals($pattern, $result, $formatter->getErrorMessage()); } } \ No newline at end of file From bcd1ad11a433cfd90d0409b852c507881a91c4ac Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Mon, 28 Oct 2013 11:59:49 +0100 Subject: [PATCH 04/17] refactored MessageFormatter --- framework/yii/base/Application.php | 2 +- framework/yii/i18n/I18N.php | 39 +++++++++- framework/yii/i18n/MessageFormatter.php | 86 +++++++++++----------- framework/yii/log/Logger.php | 4 +- .../i18n/FallbackMessageFormatterTest.php | 12 +-- tests/unit/framework/i18n/MessageFormatterTest.php | 10 +-- 6 files changed, 95 insertions(+), 58 deletions(-) diff --git a/framework/yii/base/Application.php b/framework/yii/base/Application.php index 49b52d9..80e93a6 100644 --- a/framework/yii/base/Application.php +++ b/framework/yii/base/Application.php @@ -20,7 +20,7 @@ use yii\web\HttpException; * @property \yii\db\Connection $db The database connection. This property is read-only. * @property ErrorHandler $errorHandler The error handler application component. This property is read-only. * @property \yii\base\Formatter $formatter The formatter application component. This property is read-only. - * @property \yii\i18n\I18N $i18N The internationalization component. This property is read-only. + * @property \yii\i18n\I18N $i18n The internationalization component. This property is read-only. * @property \yii\log\Logger $log The log component. This property is read-only. * @property \yii\web\Request|\yii\console\Request $request The request component. This property is read-only. * @property string $runtimePath The directory that stores runtime files. Defaults to the "runtime" diff --git a/framework/yii/i18n/I18N.php b/framework/yii/i18n/I18N.php index 61b0f77..fe0b533 100644 --- a/framework/yii/i18n/I18N.php +++ b/framework/yii/i18n/I18N.php @@ -14,6 +14,10 @@ use yii\base\InvalidConfigException; /** * I18N provides features related with internationalization (I18N) and localization (L10N). * + * @property MessageFormatter $messageFormatter The message formatter to be used to format message via ICU + * message format. Note that the type of this property differs in getter and setter. See + * [[getMessageFormatter()]] and [[setMessageFormatter()]] for details. + * * @author Qiang Xue * @since 2.0 */ @@ -69,7 +73,7 @@ class I18N extends Component * @param string $message the message to be translated. * @param array $params the parameters that will be used to replace the corresponding placeholders in the message. * @param string $language the language code (e.g. `en_US`, `en`). - * @return string the translated message. + * @return string the translated and formatted message. */ public function translate($category, $message, $params, $language) { @@ -93,8 +97,8 @@ class I18N extends Component } if (preg_match('~{\s*[\d\w]+\s*,~u', $message)) { - $formatter = new MessageFormatter(); - $result = $formatter->format($language, $message, $params); + $formatter = $this->getMessageFormatter(); + $result = $formatter->format($message, $params, $language); if ($result === false) { $errorMessage = $formatter->getErrorMessage(); Yii::warning("Formatting message for language '$language' failed with error: $errorMessage. The message being formatted was: $message."); @@ -112,6 +116,35 @@ class I18N extends Component } /** + * @var string|array|MessageFormatter + */ + private $_messageFormatter; + + /** + * Returns the message formatter instance. + * @return MessageFormatter the message formatter to be used to format message via ICU message format. + */ + public function getMessageFormatter() + { + if ($this->_messageFormatter === null) { + $this->_messageFormatter = new MessageFormatter(); + } elseif (is_array($this->_messageFormatter) || is_string($this->_messageFormatter)) { + $this->_messageFormatter = Yii::createObject($this->_messageFormatter); + } + return $this->_messageFormatter; + } + + /** + * @param string|array|MessageFormatter $value the message formatter to be used to format message via ICU message format. + * Can be given as array or string configuration that will be given to [[Yii::createObject]] to create an instance + * or a [[MessageFormatter]] instance. + */ + public function setMessageFormatter($value) + { + $this->_messageFormatter = $value; + } + + /** * Returns the message source for the given category. * @param string $category the category name. * @return MessageSource the message source for the given category. diff --git a/framework/yii/i18n/MessageFormatter.php b/framework/yii/i18n/MessageFormatter.php index 55dc73d..d04782e 100644 --- a/framework/yii/i18n/MessageFormatter.php +++ b/framework/yii/i18n/MessageFormatter.php @@ -11,11 +11,13 @@ use yii\base\Component; use yii\base\NotSupportedException; /** - * MessageFormatter enhances the message formatter class provided by PHP intl extension. + * MessageFormatter allows formatting messages via [ICU message format](http://userguide.icu-project.org/formatparse/messages) + * + * This class enhances the message formatter class provided by the PHP intl extension. * * The following enhancements are provided: * - * - Accepts named arguments and mixed numeric and named arguments. + * - It accepts named arguments and mixed numeric and named arguments. * - Issues no error when an insufficient number of arguments have been provided. Instead, the placeholders will not be * substituted. * - Fixes PHP 5.5 weird placeholder replacement in case no arguments are provided at all (https://bugs.php.net/bug.php?id=65920). @@ -23,24 +25,18 @@ use yii\base\NotSupportedException; * However it is highly recommended that you install [PHP intl extension](http://php.net/manual/en/book.intl.php) if you want * to use MessageFormatter features. * - * FallbackMessageFormatter is a fallback implementation for the PHP intl MessageFormatter that is - * used in case PHP intl extension is not installed. - * - * Do not use this class directly. Use [[MessageFormatter]] instead, which will automatically detect - * installed version of PHP intl and use the fallback if it is not installed. - * - * It is highly recommended that you install [PHP intl extension](http://php.net/manual/en/book.intl.php) if you want to use - * MessageFormatter features. + * The fallback implementation only supports the following message formats: + * - plural formatting for english + * - select format + * - simple parameters + * - integer number parameters * - * This implementation only supports to following message formats: - * - plural formatting for english - * - select format - * - simple parameters + * The fallback implementation does NOT support the ['apostrophe-friendly' syntax](http://www.php.net/manual/en/messageformatter.formatmessage.php). + * Also messages that are working with the fallback implementation are not necessarily compatible with the + * PHP intl MessageFormatter so do not rely on the fallback if you are able to install intl extension somehow. * - * The pattern also does NOT support the ['apostrophe-friendly' syntax](http://www.php.net/manual/en/messageformatter.formatmessage.php). - * - * Messages that can be parsed are also not necessarily compatible with the ICU formatting method so do not expect messages that work without intl installed - * to work with intl + * @property string $errorCode Code of the last error. This property is read-only. + * @property string $errorMessage Description of the last error. This property is read-only. * * @author Alexander Makarov * @author Carsten Brandt @@ -52,9 +48,9 @@ class MessageFormatter extends Component private $_errorMessage = ''; /** - * Get the error text from the last operation + * Get the error code from the last operation * @link http://php.net/manual/en/messageformatter.geterrorcode.php - * @return string Description of the last error. + * @return string Code of the last error. */ public function getErrorCode() { @@ -72,34 +68,38 @@ class MessageFormatter extends Component } /** - * Formats a message via ICU message format. + * Formats a message via [ICU message format](http://userguide.icu-project.org/formatparse/messages) + * + * It uses the PHP intl extension's [MessageFormatter](http://www.php.net/manual/en/class.messageformatter.php) + * and works around some issues. + * If PHP intl is not installed a fallback will be used that supports a subset of the ICU message format. * - * @link http://php.net/manual/en/messageformatter.formatmessage.php + * @param string $pattern The pattern string to insert parameters into. + * @param array $params The array of name value pairs to insert into the format string. * @param string $language The locale to use for formatting locale-dependent parts - * @param string $message The pattern string to insert things into. - * @param array $params The array of values to insert into the format string * @return string|boolean The formatted pattern string or `FALSE` if an error occurred */ - public function format($language, $message, $params) + public function format($pattern, $params, $language) { $this->_errorCode = 0; $this->_errorMessage = ''; if ($params === []) { - return $message; + return $pattern; } if (!class_exists('MessageFormatter', false)) { - return $this->fallbackFormat($language, $message, $params); + return $this->fallbackFormat($language, $pattern, $params); } if (version_compare(PHP_VERSION, '5.5.0', '<')) { - $message = $this->replaceNamedArguments($message, $params); + $pattern = $this->replaceNamedArguments($pattern, $params); $params = array_values($params); } - $formatter = new \MessageFormatter($language, $message); + $formatter = new \MessageFormatter($language, $pattern); if ($formatter === null) { - $this->_errorMessage = "Unable to format message in language '$language'."; + $this->_errorCode = -1; + $this->_errorMessage = "Message pattern is invalid."; return false; } $result = $formatter->format($params); @@ -113,16 +113,19 @@ class MessageFormatter extends Component } /** - * Parse input string according to a message pattern + * Parses an input string according to an [ICU message format](http://userguide.icu-project.org/formatparse/messages) pattern. * - * @link http://www.php.net/manual/en/messageformatter.parsemessage.php + * It uses the PHP intl extension's [MessageFormatter::parse()](http://www.php.net/manual/en/messageformatter.parsemessage.php) + * and works around some issues. + * Usage of this method requires PHP intl extension to be installed. + * + * @param string $pattern The pattern to use for parsing the message. + * @param string $message The message to parse, conforming to the pattern. * @param string $language The locale to use for formatting locale-dependent parts - * @param string $pattern The pattern with which to parse the message. - * @param array $message The message to parse, conforming to the pattern. - * @return string|boolean An array containing items extracted, or `FALSE` on error. + * @return array|boolean An array containing items extracted, or `FALSE` on error. * @throws \yii\base\NotSupportedException when PHP intl extension is not installed. */ - public function parse($language, $pattern, $message) + public function parse($pattern, $message, $language) { $this->_errorCode = 0; $this->_errorMessage = ''; @@ -138,7 +141,8 @@ class MessageFormatter extends Component // } $formatter = new \MessageFormatter($language, $pattern); if ($formatter === null) { - $this->_errorMessage = "Unable to parse message in language '$language'."; + $this->_errorCode = -1; + $this->_errorMessage = "Message pattern is invalid."; return false; } $result = $formatter->parse($message); @@ -205,12 +209,12 @@ class MessageFormatter extends Component /** * Fallback implementation for MessageFormatter::formatMessage - * @param string $language The locale to use for formatting locale-dependent parts - * @param string $message The pattern string to insert things into. - * @param array $params The array of values to insert into the format string + * @param string $pattern The pattern string to insert things into. + * @param array $args The array of values to insert into the format string + * @param string $locale The locale to use for formatting locale-dependent parts * @return string|boolean The formatted pattern string or `FALSE` if an error occurred */ - protected function fallbackFormat($locale, $pattern, $args = []) + protected function fallbackFormat($pattern, $args, $locale) { if (($tokens = $this->tokenizePattern($pattern)) === false) { return false; diff --git a/framework/yii/log/Logger.php b/framework/yii/log/Logger.php index 4e0e547..f79c40f 100644 --- a/framework/yii/log/Logger.php +++ b/framework/yii/log/Logger.php @@ -65,8 +65,8 @@ use yii\base\InvalidConfigException; * second element the total time spent in SQL execution. This property is read-only. * @property float $elapsedTime The total elapsed time in seconds for current request. This property is * read-only. - * @property array $profiling The profiling results. Each array element has the following structure: - * `[$token, $category, $time]`. This property is read-only. + * @property array $profiling The profiling results. Each array element has the following structure: `[$token, + * $category, $time]`. This property is read-only. * * @author Qiang Xue * @since 2.0 diff --git a/tests/unit/framework/i18n/FallbackMessageFormatterTest.php b/tests/unit/framework/i18n/FallbackMessageFormatterTest.php index 044c450..f674358 100644 --- a/tests/unit/framework/i18n/FallbackMessageFormatterTest.php +++ b/tests/unit/framework/i18n/FallbackMessageFormatterTest.php @@ -118,7 +118,7 @@ _MSG_ public function testNamedArgumentsObject($pattern, $expected, $args) { $formatter = new FallbackMessageFormatter(); - $result = $formatter->format('en_US', $pattern, $args); + $result = $formatter->fallbackFormat($pattern, $args, 'en_US'); $this->assertEquals($expected, $result, $formatter->getErrorMessage()); } @@ -127,9 +127,9 @@ _MSG_ $expected = '{'.self::SUBJECT.'} is '.self::N_VALUE; $formatter = new FallbackMessageFormatter(); - $result = $formatter->format('en_US', '{'.self::SUBJECT.'} is {'.self::N.'}', [ + $result = $formatter->fallbackFormat('{'.self::SUBJECT.'} is {'.self::N.'}', [ self::N => self::N_VALUE, - ]); + ], 'en_US'); $this->assertEquals($expected, $result); } @@ -139,15 +139,15 @@ _MSG_ $pattern = '{'.self::SUBJECT.'} is '.self::N; $formatter = new FallbackMessageFormatter(); - $result = $formatter->format('en_US', $pattern, []); + $result = $formatter->fallbackFormat($pattern, [], 'en_US'); $this->assertEquals($pattern, $result, $formatter->getErrorMessage()); } } class FallbackMessageFormatter extends MessageFormatter { - public function fallbackFormat($locale, $pattern, $args = []) + public function fallbackFormat($pattern, $args, $locale) { - return parent::fallbackFormat($locale, $pattern, $args); + return parent::fallbackFormat($pattern, $args, $locale); } } \ No newline at end of file diff --git a/tests/unit/framework/i18n/MessageFormatterTest.php b/tests/unit/framework/i18n/MessageFormatterTest.php index 5daaab9..9030d87 100644 --- a/tests/unit/framework/i18n/MessageFormatterTest.php +++ b/tests/unit/framework/i18n/MessageFormatterTest.php @@ -122,10 +122,10 @@ _MSG_ /** * @dataProvider patterns */ - public function testNamedArgumentsObject($pattern, $expected, $args) + public function testNamedArguments($pattern, $expected, $args) { $formatter = new MessageFormatter(); - $result = $formatter->format('en_US', $pattern, $args); + $result = $formatter->format($pattern, $args, 'en_US'); $this->assertEquals($expected, $result, $formatter->getErrorMessage()); } @@ -134,9 +134,9 @@ _MSG_ $expected = '{'.self::SUBJECT.'} is '.self::N_VALUE; $formatter = new MessageFormatter(); - $result = $formatter->format('en_US', '{'.self::SUBJECT.'} is {'.self::N.', number}', [ + $result = $formatter->format('{'.self::SUBJECT.'} is {'.self::N.', number}', [ self::N => self::N_VALUE, - ]); + ], 'en_US'); $this->assertEquals($expected, $result, $formatter->getErrorMessage()); } @@ -145,7 +145,7 @@ _MSG_ { $pattern = '{'.self::SUBJECT.'} is '.self::N; $formatter = new MessageFormatter(); - $result = $formatter->format('en_US', $pattern, []); + $result = $formatter->format($pattern, [], 'en_US'); $this->assertEquals($pattern, $result, $formatter->getErrorMessage()); } } \ No newline at end of file From 4d036999075fb3cc4d48375506fa221adf1063c5 Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Mon, 28 Oct 2013 12:09:26 +0100 Subject: [PATCH 05/17] fixed fallback message format parameter order --- framework/yii/i18n/MessageFormatter.php | 4 ++-- tests/unit/framework/i18n/FallbackMessageFormatterTest.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/framework/yii/i18n/MessageFormatter.php b/framework/yii/i18n/MessageFormatter.php index d04782e..9754e0b 100644 --- a/framework/yii/i18n/MessageFormatter.php +++ b/framework/yii/i18n/MessageFormatter.php @@ -316,7 +316,7 @@ class MessageFormatter extends Component } } if ($message !== false) { - return $this->fallbackFormat($locale, $message, $args); + return $this->fallbackFormat($message, $args, $locale); } break; case 'plural': @@ -351,7 +351,7 @@ class MessageFormatter extends Component } } if ($message !== false) { - return $this->fallbackFormat($locale, $message, $args); + return $this->fallbackFormat($message, $args, $locale); } break; } diff --git a/tests/unit/framework/i18n/FallbackMessageFormatterTest.php b/tests/unit/framework/i18n/FallbackMessageFormatterTest.php index f674358..910c433 100644 --- a/tests/unit/framework/i18n/FallbackMessageFormatterTest.php +++ b/tests/unit/framework/i18n/FallbackMessageFormatterTest.php @@ -115,7 +115,7 @@ _MSG_ /** * @dataProvider patterns */ - public function testNamedArgumentsObject($pattern, $expected, $args) + public function testNamedArguments($pattern, $expected, $args) { $formatter = new FallbackMessageFormatter(); $result = $formatter->fallbackFormat($pattern, $args, 'en_US'); From 61b0d8824b8afac6dc08715fe696bfa77a56f3ec Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Mon, 28 Oct 2013 12:24:43 +0100 Subject: [PATCH 06/17] support integer number type in fallback message formatter --- framework/yii/i18n/MessageFormatter.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/framework/yii/i18n/MessageFormatter.php b/framework/yii/i18n/MessageFormatter.php index 9754e0b..8fcf4bf 100644 --- a/framework/yii/i18n/MessageFormatter.php +++ b/framework/yii/i18n/MessageFormatter.php @@ -289,7 +289,6 @@ class MessageFormatter extends Component $type = isset($token[1]) ? trim($token[1]) : 'none'; switch($type) { - case 'number': case 'date': case 'time': case 'spellout': @@ -298,6 +297,11 @@ class MessageFormatter extends Component case 'choice': case 'selectordinal': throw new NotSupportedException("Message format '$type' is not supported. You have to install PHP intl extension to use this feature."); + case 'number': + if (is_int($arg) && (!isset($token[2]) || trim($token[2]) == 'integer')) { + return $arg; + } + throw new NotSupportedException("Message format 'number' is only supported for integer values. You have to install PHP intl extension to use this feature."); case 'none': return $arg; case 'select': /* http://icu-project.org/apiref/icu4c/classicu_1_1SelectFormat.html From 0b82575a0b94a5559e9a44886bcea090be10db1a Mon Sep 17 00:00:00 2001 From: resurtm Date: Mon, 28 Oct 2013 17:53:36 +0600 Subject: [PATCH 07/17] Add missing @throws PHPDoc declaration. --- framework/yii/base/Object.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/framework/yii/base/Object.php b/framework/yii/base/Object.php index ddb0f58..f0bd92b 100644 --- a/framework/yii/base/Object.php +++ b/framework/yii/base/Object.php @@ -63,6 +63,7 @@ class Object implements Arrayable * @param string $name the property name * @return mixed the property value * @throws UnknownPropertyException if the property is not defined + * @throws InvalidCallException if the property is write-only * @see __set */ public function __get($name) @@ -85,7 +86,7 @@ class Object implements Arrayable * @param string $name the property name or the event name * @param mixed $value the property value * @throws UnknownPropertyException if the property is not defined - * @throws InvalidCallException if the property is read-only. + * @throws InvalidCallException if the property is read-only * @see __get */ public function __set($name, $value) From 80e816efc31b2a966a7d7d97066b7cf02d1f57c5 Mon Sep 17 00:00:00 2001 From: Panagiotis Moustafellos Date: Mon, 28 Oct 2013 14:39:09 +0200 Subject: [PATCH 08/17] pgsql: added 'bool' type (alias of 'boolean') Relates #1086 and #1087 --- framework/yii/db/pgsql/Schema.php | 1 + 1 file changed, 1 insertion(+) diff --git a/framework/yii/db/pgsql/Schema.php b/framework/yii/db/pgsql/Schema.php index a5c59f4..c4da801 100644 --- a/framework/yii/db/pgsql/Schema.php +++ b/framework/yii/db/pgsql/Schema.php @@ -34,6 +34,7 @@ class Schema extends \yii\db\Schema public $typeMap = [ 'abstime' => self::TYPE_TIMESTAMP, 'bit' => self::TYPE_STRING, + 'bool' => self::TYPE_BOOLEAN, 'boolean' => self::TYPE_BOOLEAN, 'box' => self::TYPE_STRING, 'character' => self::TYPE_STRING, From d637a76f566823fdabb50ae582875444d5d259b6 Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Mon, 28 Oct 2013 15:06:44 +0100 Subject: [PATCH 09/17] add support for named arguments to messageformatter parse --- framework/yii/i18n/MessageFormatter.php | 30 +++++-- .../i18n/FallbackMessageFormatterTest.php | 18 +++++ tests/unit/framework/i18n/MessageFormatterTest.php | 92 ++++++++++++++++++++++ 3 files changed, 133 insertions(+), 7 deletions(-) diff --git a/framework/yii/i18n/MessageFormatter.php b/framework/yii/i18n/MessageFormatter.php index 8fcf4bf..c3822a7 100644 --- a/framework/yii/i18n/MessageFormatter.php +++ b/framework/yii/i18n/MessageFormatter.php @@ -134,11 +134,24 @@ class MessageFormatter extends Component throw new NotSupportedException('You have to install PHP intl extension to use this feature.'); } - // TODO try to support named args -// if (version_compare(PHP_VERSION, '5.5.0', '<')) { -// $message = $this->replaceNamedArguments($message, $params); -// $params = array_values($params); -// } + // replace named arguments + if (($tokens = $this->tokenizePattern($pattern)) === false) { + return false; + } + $map = []; + foreach($tokens as $i => $token) { + if (is_array($token)) { + $param = trim($token[0]); + if (!isset($map[$param])) { + $map[$param] = count($map); + } + $token[0] = $map[$param]; + $tokens[$i] = '{' . implode(',', $token) . '}'; + } + } + $pattern = implode('', $tokens); + $map = array_flip($map); + $formatter = new \MessageFormatter($language, $pattern); if ($formatter === null) { $this->_errorCode = -1; @@ -151,9 +164,12 @@ class MessageFormatter extends Component $this->_errorMessage = $formatter->getErrorMessage(); return false; } else { - return $result; + $values = []; + foreach($result as $key => $value) { + $values[$map[$key]] = $value; + } + return $values; } - } /** diff --git a/tests/unit/framework/i18n/FallbackMessageFormatterTest.php b/tests/unit/framework/i18n/FallbackMessageFormatterTest.php index 910c433..6aa8a22 100644 --- a/tests/unit/framework/i18n/FallbackMessageFormatterTest.php +++ b/tests/unit/framework/i18n/FallbackMessageFormatterTest.php @@ -34,6 +34,24 @@ class FallbackMessageFormatterTest extends TestCase ] ], + [ + '{'.self::SUBJECT.'} is {'.self::N.', number}', // pattern + self::SUBJECT_VALUE.' is '.self::N_VALUE, // expected + [ // params + self::N => self::N_VALUE, + self::SUBJECT => self::SUBJECT_VALUE, + ] + ], + + [ + '{'.self::SUBJECT.'} is {'.self::N.', number, integer}', // pattern + self::SUBJECT_VALUE.' is '.self::N_VALUE, // expected + [ // params + self::N => self::N_VALUE, + self::SUBJECT => self::SUBJECT_VALUE, + ] + ], + // This one was provided by Aura.Intl. Thanks! [<<<_MSG_ {gender_of_host, select, diff --git a/tests/unit/framework/i18n/MessageFormatterTest.php b/tests/unit/framework/i18n/MessageFormatterTest.php index 9030d87..7b20d43 100644 --- a/tests/unit/framework/i18n/MessageFormatterTest.php +++ b/tests/unit/framework/i18n/MessageFormatterTest.php @@ -41,6 +41,15 @@ class MessageFormatterTest extends TestCase ] ], + [ + '{'.self::SUBJECT.'} is {'.self::N.', number, integer}', // pattern + self::SUBJECT_VALUE.' is '.self::N_VALUE, // expected + [ // params + self::N => self::N_VALUE, + self::SUBJECT => self::SUBJECT_VALUE, + ] + ], + // This one was provided by Aura.Intl. Thanks! [<<<_MSG_ {gender_of_host, select, @@ -119,6 +128,79 @@ _MSG_ ]; } + public function parsePatterns() + { + return [ + [ + self::SUBJECT_VALUE.' is {0, number}', // pattern + self::SUBJECT_VALUE.' is '.self::N_VALUE, // expected + [ // params + 0 => self::N_VALUE, + ] + ], + + [ + self::SUBJECT_VALUE.' is {'.self::N.', number}', // pattern + self::SUBJECT_VALUE.' is '.self::N_VALUE, // expected + [ // params + self::N => self::N_VALUE, + ] + ], + + [ + self::SUBJECT_VALUE.' is {'.self::N.', number, integer}', // pattern + self::SUBJECT_VALUE.' is '.self::N_VALUE, // expected + [ // params + self::N => self::N_VALUE, + ] + ], + + [ + "{0,number,integer} monkeys on {1,number,integer} trees make {2,number} monkeys per tree", + "4,560 monkeys on 123 trees make 37.073 monkeys per tree", + [ + 0 => 4560, + 1 => 123, + 2 => 37.073 + ], + 'en_US' + ], + + [ + "{0,number,integer} Affen auf {1,number,integer} Bäumen sind {2,number} Affen pro Baum", + "4.560 Affen auf 123 Bäumen sind 37,073 Affen pro Baum", + [ + 0 => 4560, + 1 => 123, + 2 => 37.073 + ], + 'de', + ], + + [ + "{monkeyCount,number,integer} monkeys on {trees,number,integer} trees make {monkeysPerTree,number} monkeys per tree", + "4,560 monkeys on 123 trees make 37.073 monkeys per tree", + [ + 'monkeyCount' => 4560, + 'trees' => 123, + 'monkeysPerTree' => 37.073 + ], + 'en_US' + ], + + [ + "{monkeyCount,number,integer} Affen auf {trees,number,integer} Bäumen sind {monkeysPerTree,number} Affen pro Baum", + "4.560 Affen auf 123 Bäumen sind 37,073 Affen pro Baum", + [ + 'monkeyCount' => 4560, + 'trees' => 123, + 'monkeysPerTree' => 37.073 + ], + 'de', + ], + ]; + } + /** * @dataProvider patterns */ @@ -129,6 +211,16 @@ _MSG_ $this->assertEquals($expected, $result, $formatter->getErrorMessage()); } + /** + * @dataProvider parsePatterns + */ + public function testParseNamedArguments($pattern, $expected, $args, $locale = 'en_US') + { + $formatter = new MessageFormatter(); + $result = $formatter->parse($pattern, $expected, $locale); + $this->assertEquals($args, $result, $formatter->getErrorMessage() . ' Pattern: ' . $pattern); + } + public function testInsufficientArguments() { $expected = '{'.self::SUBJECT.'} is '.self::N_VALUE; From 7bb4826a0b178027757b21e63f9e953ebaa14102 Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Mon, 28 Oct 2013 15:21:11 +0100 Subject: [PATCH 10/17] doc fix --- framework/yii/i18n/MessageFormatter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/yii/i18n/MessageFormatter.php b/framework/yii/i18n/MessageFormatter.php index c3822a7..a038be1 100644 --- a/framework/yii/i18n/MessageFormatter.php +++ b/framework/yii/i18n/MessageFormatter.php @@ -116,7 +116,7 @@ class MessageFormatter extends Component * Parses an input string according to an [ICU message format](http://userguide.icu-project.org/formatparse/messages) pattern. * * It uses the PHP intl extension's [MessageFormatter::parse()](http://www.php.net/manual/en/messageformatter.parsemessage.php) - * and works around some issues. + * and adds support for named arguments. * Usage of this method requires PHP intl extension to be installed. * * @param string $pattern The pattern to use for parsing the message. From a043be3ef9c727f846fbd52e44ea97b840e9684c Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Mon, 28 Oct 2013 15:48:57 +0100 Subject: [PATCH 11/17] added README for unit tests as requested in issue #1084 --- tests/README.md | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 tests/README.md diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..fa6b3a4 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,41 @@ +Yii 2.0 Unit tests +================== + +DIRECTORY STRUCTURE +------------------- + + unit/ Unit tests to run with PHPUnit + data/ models, config and other test data + config.php this file contains configuration for database and caching backends + framework/ the framework unit tests + runtime/ the application runtime dir for the yii test app + web/ webapp for functional testing + + +HOW TO RUN THE TESTS +-------------------- + +Make sure you have PHPUnit installed. + +Run PHPUnit in the yii repo base directory. + +```php +phpunit +``` + +You can run tests for specific groups only: + +```php +phpunit --group=mysql,base,i18n +``` + +You can get a list of available groups via `phpunit --list-groups`. + +TEST CONFIGURATION +------------------ + +PHPUnit configuration is in `phpunit.xml.dist` in repository root folder. +You can create your own phpunit.xml to override dist config. + +Database and other backend system configuration can be found in `unit/data/config.php` +adjust them to your needs to allow testing databases and caching in your environment. \ No newline at end of file From 73d018600e6f59ed6f8d8528786e019d643332b1 Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Mon, 28 Oct 2013 15:52:06 +0100 Subject: [PATCH 12/17] added phpunit.xml to .gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index f2915a9..6482763 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,6 @@ composer.phar # Mac DS_Store Files .DS_Store + +# local phpunit config +/phpunit.xml \ No newline at end of file From 66b27fcaf101889e1919370e5e0aa7c71dd1e964 Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Mon, 28 Oct 2013 19:33:01 +0100 Subject: [PATCH 13/17] fixes #1092 --- framework/yii/i18n/MessageFormatter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/yii/i18n/MessageFormatter.php b/framework/yii/i18n/MessageFormatter.php index a038be1..3c165e6 100644 --- a/framework/yii/i18n/MessageFormatter.php +++ b/framework/yii/i18n/MessageFormatter.php @@ -89,7 +89,7 @@ class MessageFormatter extends Component } if (!class_exists('MessageFormatter', false)) { - return $this->fallbackFormat($language, $pattern, $params); + return $this->fallbackFormat($pattern, $params, $language); } if (version_compare(PHP_VERSION, '5.5.0', '<')) { From 51211898f815443dcb162c37ff6554e913dcfbb5 Mon Sep 17 00:00:00 2001 From: Alexander Mohorev Date: Mon, 28 Oct 2013 23:51:48 +0300 Subject: [PATCH 14/17] Control statements based on the if and elseif constructs must have a single space before the opening parenthesis of the conditional and a single space after the closing parenthesis. --- apps/advanced/init | 2 +- build/controllers/PhpDocController.php | 10 +++++----- docs/guide/controller.md | 8 ++++---- docs/guide/upgrade-from-v1.md | 2 +- docs/guide/view.md | 2 +- framework/yii/BaseYii.php | 2 +- framework/yii/base/View.php | 4 ++-- framework/yii/caching/RedisCache.php | 2 +- framework/yii/data/ActiveDataProvider.php | 2 +- framework/yii/db/cubrid/Schema.php | 4 ++-- framework/yii/gii/generators/form/templates/action.php | 2 +- framework/yii/gii/views/default/view.php | 10 +++++----- framework/yii/i18n/MessageFormatter.php | 8 ++++---- framework/yii/redis/Connection.php | 6 +++--- tests/unit/framework/caching/RedisCacheTest.php | 4 ++-- tests/unit/framework/helpers/ConsoleTest.php | 2 +- tests/unit/framework/validators/UrlValidatorTest.php | 2 +- 17 files changed, 36 insertions(+), 36 deletions(-) diff --git a/apps/advanced/init b/apps/advanced/init index f5d2691..c8f7f73 100755 --- a/apps/advanced/init +++ b/apps/advanced/init @@ -21,7 +21,7 @@ if (empty($params['env'])) { exit(1); } - if(isset($envNames[$answer])) { + if (isset($envNames[$answer])) { $envName = $envNames[$answer]; } } diff --git a/build/controllers/PhpDocController.php b/build/controllers/PhpDocController.php index a104dd5..760cb05 100644 --- a/build/controllers/PhpDocController.php +++ b/build/controllers/PhpDocController.php @@ -115,7 +115,7 @@ class PhpDocController extends Controller if (trim($lines[1]) == '*' || substr(trim($lines[1]), 0, 3) == '* @') { $this->stderr("[WARN] Class $className has no short description.\n", Console::FG_YELLOW, Console::BOLD); } - foreach($lines as $line) { + foreach ($lines as $line) { if (substr(trim($line), 0, 9) == '* @since ') { $seenSince = true; } elseif (substr(trim($line), 0, 10) == '* @author ') { @@ -138,7 +138,7 @@ class PhpDocController extends Controller $newFileContent = []; $n = count($fileContent); - for($i = 0; $i < $n; $i++) { + for ($i = 0; $i < $n; $i++) { if ($i > $start || $i < $docStart) { $newFileContent[] = $fileContent[$i]; } else { @@ -164,7 +164,7 @@ class PhpDocController extends Controller { $lines = explode("\n", $doc); $n = count($lines); - for($i = 0; $i < $n; $i++) { + for ($i = 0; $i < $n; $i++) { $lines[$i] = rtrim($lines[$i]); if (trim($lines[$i]) == '*' && trim($lines[$i + 1]) == '*') { unset($lines[$i]); @@ -184,7 +184,7 @@ class PhpDocController extends Controller $lines = explode("\n", $doc); $propertyPart = false; $propertyPosition = false; - foreach($lines as $i => $line) { + foreach ($lines as $i => $line) { if (substr(trim($line), 0, 12) == '* @property ') { $propertyPart = true; } elseif ($propertyPart && trim($line) == '*') { @@ -200,7 +200,7 @@ class PhpDocController extends Controller } } $finalDoc = ''; - foreach($lines as $i => $line) { + foreach ($lines as $i => $line) { $finalDoc .= $line . "\n"; if ($i == $propertyPosition) { $finalDoc .= $properties; diff --git a/docs/guide/controller.md b/docs/guide/controller.md index bb78120..39b7f0f 100644 --- a/docs/guide/controller.md +++ b/docs/guide/controller.md @@ -89,7 +89,7 @@ class BlogController extends Controller $post = Post::find($id); $text = $post->text; - if($version) { + if ($version) { $text = $post->getHistory($version); } @@ -121,13 +121,13 @@ class BlogController extends Controller public function actionUpdate($id) { $post = Post::find($id); - if(!$post) { + if (!$post) { throw new HttpException(404); } - if(\Yii::$app->request->isPost)) { + if (\Yii::$app->request->isPost)) { $post->load($_POST); - if($post->save()) { + if ($post->save()) { $this->redirect(['view', 'id' => $post->id]); } } diff --git a/docs/guide/upgrade-from-v1.md b/docs/guide/upgrade-from-v1.md index f04e849..628cf54 100644 --- a/docs/guide/upgrade-from-v1.md +++ b/docs/guide/upgrade-from-v1.md @@ -179,7 +179,7 @@ $model->save(); $postTags = []; $tagsCount = count($_POST['PostTag']); -while($tagsCount-- > 0){ +while ($tagsCount-- > 0) { $postTags[] = new PostTag(['post_id' => $model->id]); } Model::loadMultiple($postTags, $_POST); diff --git a/docs/guide/view.md b/docs/guide/view.md index 98e0140..2fdd974 100644 --- a/docs/guide/view.md +++ b/docs/guide/view.md @@ -305,7 +305,7 @@ Then we're using it in `index.php` view where we display a list of users: ```php
render('_profile', [ 'username' => $user->name, 'tagline' => $user->tagline, diff --git a/framework/yii/BaseYii.php b/framework/yii/BaseYii.php index 5d546bd..09312c3 100644 --- a/framework/yii/BaseYii.php +++ b/framework/yii/BaseYii.php @@ -515,7 +515,7 @@ class BaseYii return self::$app->getI18n()->translate($category, $message, $params, $language ?: self::$app->language); } else { $p = []; - foreach((array) $params as $name => $value) { + foreach ((array) $params as $name => $value) { $p['{' . $name . '}'] = $value; } return ($p === []) ? $message : strtr($message, $p); diff --git a/framework/yii/base/View.php b/framework/yii/base/View.php index d3db27d..e7ea3fe 100644 --- a/framework/yii/base/View.php +++ b/framework/yii/base/View.php @@ -519,7 +519,7 @@ class View extends Component $this->trigger(self::EVENT_END_PAGE); $content = ob_get_clean(); - foreach(array_keys($this->assetBundles) as $bundle) { + foreach (array_keys($this->assetBundles) as $bundle) { $this->registerAssetFiles($bundle); } echo strtr($content, [ @@ -549,7 +549,7 @@ class View extends Component return; } $bundle = $this->assetBundles[$name]; - foreach($bundle->depends as $dep) { + foreach ($bundle->depends as $dep) { $this->registerAssetFiles($dep); } $bundle->registerAssetFiles($this); diff --git a/framework/yii/caching/RedisCache.php b/framework/yii/caching/RedisCache.php index 3a40254..d31d66b 100644 --- a/framework/yii/caching/RedisCache.php +++ b/framework/yii/caching/RedisCache.php @@ -139,7 +139,7 @@ class RedisCache extends Cache $response = $this->_connection->executeCommand('MGET', $keys); $result = []; $i = 0; - foreach($keys as $key) { + foreach ($keys as $key) { $result[$key] = $response[$i++]; } return $result; diff --git a/framework/yii/data/ActiveDataProvider.php b/framework/yii/data/ActiveDataProvider.php index 012b7dd..ca7696f 100644 --- a/framework/yii/data/ActiveDataProvider.php +++ b/framework/yii/data/ActiveDataProvider.php @@ -170,7 +170,7 @@ class ActiveDataProvider extends BaseDataProvider if (($sort = $this->getSort()) !== false && empty($sort->attributes) && $this->query instanceof ActiveQuery) { /** @var Model $model */ $model = new $this->query->modelClass; - foreach($model->attributes() as $attribute) { + foreach ($model->attributes() as $attribute) { $sort->attributes[$attribute] = [ 'asc' => [$attribute => Sort::ASC], 'desc' => [$attribute => Sort::DESC], diff --git a/framework/yii/db/cubrid/Schema.php b/framework/yii/db/cubrid/Schema.php index 68aa8ee..0b9d0f5 100644 --- a/framework/yii/db/cubrid/Schema.php +++ b/framework/yii/db/cubrid/Schema.php @@ -147,7 +147,7 @@ class Schema extends \yii\db\Schema } $foreignKeys = $this->db->pdo->cubrid_schema(\PDO::CUBRID_SCH_IMPORTED_KEYS, $table->name); - foreach($foreignKeys as $key) { + foreach ($foreignKeys as $key) { if (isset($table->foreignKeys[$key['FK_NAME']])) { $table->foreignKeys[$key['FK_NAME']][$key['FKCOLUMN_NAME']] = $key['PKCOLUMN_NAME']; } else { @@ -230,7 +230,7 @@ class Schema extends \yii\db\Schema $this->db->open(); $tables = $this->db->pdo->cubrid_schema(\PDO::CUBRID_SCH_TABLE); $tableNames = []; - foreach($tables as $table) { + foreach ($tables as $table) { // do not list system tables if ($table['TYPE'] != 0) { $tableNames[] = $table['NAME']; diff --git a/framework/yii/gii/generators/form/templates/action.php b/framework/yii/gii/generators/form/templates/action.php index 7e2f876..adc2a59 100644 --- a/framework/yii/gii/generators/form/templates/action.php +++ b/framework/yii/gii/generators/form/templates/action.php @@ -17,7 +17,7 @@ public function actionviewName $model = new modelClass ?>scenarioName) ? "" : "(['scenario' => '{$generator->scenarioName}'])" ?>; if ($model->load($_POST)) { - if($model->validate()) { + if ($model->validate()) { // form inputs are valid, do something here return; } diff --git a/framework/yii/gii/views/default/view.php b/framework/yii/gii/views/default/view.php index 3123b57..2104d75 100644 --- a/framework/yii/gii/views/default/view.php +++ b/framework/yii/gii/views/default/view.php @@ -34,19 +34,19 @@ foreach ($generator->templates as $name => $path) { ]); ?>
- renderFile($generator->formView(), [ + renderFile($generator->formView(), [ 'generator' => $generator, 'form' => $form, - ]); ?> - field($generator, 'template')->sticky() + ]) ?> + field($generator, 'template')->sticky() ->label('Code Template') ->dropDownList($templates)->hint(' Please select which set of the templates should be used to generated the code. - '); ?> + ') ?>
'preview', 'class' => 'btn btn-primary']) ?> - + 'generate', 'class' => 'btn btn-success']) ?>
diff --git a/framework/yii/i18n/MessageFormatter.php b/framework/yii/i18n/MessageFormatter.php index 3c165e6..7abff83 100644 --- a/framework/yii/i18n/MessageFormatter.php +++ b/framework/yii/i18n/MessageFormatter.php @@ -190,7 +190,7 @@ class MessageFormatter extends Component $pattern = $parts[0]; $d = 0; $stack = []; - for($i = 1; $i < $c; $i++) { + for ($i = 1; $i < $c; $i++) { if (preg_match('~^(\s*)([\d\w]+)(\s*)([},])(\s*)(.*)$~us', $parts[$i], $matches)) { // if we are not inside a plural or select this is a message if (!isset($stack[$d]) || $stack[$d] != 'plural' && $stack[$d] != 'select') { @@ -257,7 +257,7 @@ class MessageFormatter extends Component return [$pattern]; } $tokens = [mb_substr($pattern, 0, $pos)]; - while(true) { + while (true) { $open = mb_strpos($pattern, '{', $pos + 1); $close = mb_strpos($pattern, '}', $pos + 1); if ($open === false && $close === false) { @@ -326,7 +326,7 @@ class MessageFormatter extends Component $select = static::tokenizePattern($token[2]); $c = count($select); $message = false; - for($i = 0; $i + 1 < $c; $i++) { + for ($i = 0; $i + 1 < $c; $i++) { if (is_array($select[$i]) || !is_array($select[$i + 1])) { return false; } @@ -352,7 +352,7 @@ class MessageFormatter extends Component $c = count($plural); $message = false; $offset = 0; - for($i = 0; $i + 1 < $c; $i++) { + for ($i = 0; $i + 1 < $c; $i++) { if (is_array($plural[$i]) || !is_array($plural[$i + 1])) { return false; } diff --git a/framework/yii/redis/Connection.php b/framework/yii/redis/Connection.php index a5da73e..140f985 100644 --- a/framework/yii/redis/Connection.php +++ b/framework/yii/redis/Connection.php @@ -384,7 +384,7 @@ class Connection extends Component private function parseResponse($command) { - if(($line = fgets($this->_socket)) === false) { + if (($line = fgets($this->_socket)) === false) { throw new Exception("Failed to read from socket.\nRedis command was: " . $command); } $type = $line[0]; @@ -405,7 +405,7 @@ class Connection extends Component $length = $line + 2; $data = ''; while ($length > 0) { - if(($block = fread($this->_socket, $line + 2)) === false) { + if (($block = fread($this->_socket, $line + 2)) === false) { throw new Exception("Failed to read from socket.\nRedis command was: " . $command); } $data .= $block; @@ -415,7 +415,7 @@ class Connection extends Component case '*': // Multi-bulk replies $count = (int) $line; $data = []; - for($i = 0; $i < $count; $i++) { + for ($i = 0; $i < $count; $i++) { $data[] = $this->parseResponse($command); } return $data; diff --git a/tests/unit/framework/caching/RedisCacheTest.php b/tests/unit/framework/caching/RedisCacheTest.php index b9df7a9..b064f78 100644 --- a/tests/unit/framework/caching/RedisCacheTest.php +++ b/tests/unit/framework/caching/RedisCacheTest.php @@ -24,11 +24,11 @@ class RedisCacheTest extends CacheTestCase 'dataTimeout' => 0.1, ]; $dsn = $config['hostname'] . ':' .$config['port']; - if(!@stream_socket_client($dsn, $errorNumber, $errorDescription, 0.5)) { + if (!@stream_socket_client($dsn, $errorNumber, $errorDescription, 0.5)) { $this->markTestSkipped('No redis server running at ' . $dsn .' : ' . $errorNumber . ' - ' . $errorDescription); } - if($this->_cacheInstance === null) { + if ($this->_cacheInstance === null) { $this->_cacheInstance = new RedisCache($config); } return $this->_cacheInstance; diff --git a/tests/unit/framework/helpers/ConsoleTest.php b/tests/unit/framework/helpers/ConsoleTest.php index 6775b5a..ce6d3fe 100644 --- a/tests/unit/framework/helpers/ConsoleTest.php +++ b/tests/unit/framework/helpers/ConsoleTest.php @@ -73,7 +73,7 @@ class ConsoleTest extends TestCase /* public function testScreenSize() { - for($i = 1; $i < 20; $i++) { + for ($i = 1; $i < 20; $i++) { echo implode(', ', Console::getScreenSize(true)) . "\n"; ob_flush(); sleep(1); diff --git a/tests/unit/framework/validators/UrlValidatorTest.php b/tests/unit/framework/validators/UrlValidatorTest.php index 1323434..50baa9c 100644 --- a/tests/unit/framework/validators/UrlValidatorTest.php +++ b/tests/unit/framework/validators/UrlValidatorTest.php @@ -59,7 +59,7 @@ class UrlValidatorTest extends TestCase public function testValidateWithIdn() { - if(!function_exists('idn_to_ascii')) { + if (!function_exists('idn_to_ascii')) { $this->markTestSkipped('intl package required'); return; } From 0d04846c0899df3e904f922439b95b2ec51f0e2a Mon Sep 17 00:00:00 2001 From: Alexander Mohorev Date: Mon, 28 Oct 2013 23:57:40 +0300 Subject: [PATCH 15/17] php short echo syntax --- apps/advanced/frontend/views/site/contact.php | 4 ++-- apps/basic/views/site/contact.php | 4 ++-- apps/basic/views/site/login.php | 4 ++-- docs/guide/view.md | 2 +- framework/yii/views/errorHandler/exception.php | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/apps/advanced/frontend/views/site/contact.php b/apps/advanced/frontend/views/site/contact.php index 97580ca..4202508 100644 --- a/apps/advanced/frontend/views/site/contact.php +++ b/apps/advanced/frontend/views/site/contact.php @@ -25,10 +25,10 @@ $this->params['breadcrumbs'][] = $this->title; field($model, 'email') ?> field($model, 'subject') ?> field($model, 'body')->textArea(['rows' => 6]) ?> - field($model, 'verifyCode')->widget(Captcha::className(), [ + field($model, 'verifyCode')->widget(Captcha::className(), [ 'options' => ['class' => 'form-control'], 'template' => '
{image}
{input}
', - ]); ?> + ]) ?>
'btn btn-primary']) ?>
diff --git a/apps/basic/views/site/contact.php b/apps/basic/views/site/contact.php index 66711a5..cd6d9ce 100644 --- a/apps/basic/views/site/contact.php +++ b/apps/basic/views/site/contact.php @@ -33,10 +33,10 @@ $this->params['breadcrumbs'][] = $this->title; field($model, 'email') ?> field($model, 'subject') ?> field($model, 'body')->textArea(['rows' => 6]) ?> - field($model, 'verifyCode')->widget(Captcha::className(), [ + field($model, 'verifyCode')->widget(Captcha::className(), [ 'options' => ['class' => 'form-control'], 'template' => '
{image}
{input}
', - ]); ?> + ]) ?>
'btn btn-primary']) ?>
diff --git a/apps/basic/views/site/login.php b/apps/basic/views/site/login.php index 62b4398..14522af 100644 --- a/apps/basic/views/site/login.php +++ b/apps/basic/views/site/login.php @@ -28,9 +28,9 @@ $this->params['breadcrumbs'][] = $this->title; field($model, 'password')->passwordInput() ?> - field($model, 'rememberMe', [ + field($model, 'rememberMe', [ 'template' => "
{input}
\n
{error}
", - ])->checkbox(); ?> + ])->checkbox() ?>
diff --git a/docs/guide/view.md b/docs/guide/view.md index 2fdd974..ee469e6 100644 --- a/docs/guide/view.md +++ b/docs/guide/view.md @@ -28,7 +28,7 @@ as the corresponding key. So the view for the action above should be in `views/site/index.php` and can be something like: ```php -

Hello, !

+

Hello, !

``` Instead of just scalar values you can pass anything else such as arrays or objects. diff --git a/framework/yii/views/errorHandler/exception.php b/framework/yii/views/errorHandler/exception.php index beebb29..4b90de9 100644 --- a/framework/yii/views/errorHandler/exception.php +++ b/framework/yii/views/errorHandler/exception.php @@ -367,8 +367,8 @@ pre .diff .change{
    renderCallStackItem($exception->getFile(), $exception->getLine(), null, null, 1) ?> getTrace(), $length = count($trace); $i < $length; ++$i): ?> - renderCallStackItem(@$trace[$i]['file'] ?: null, @$trace[$i]['line'] ?: null, - @$trace[$i]['class'] ?: null, @$trace[$i]['function'] ?: null, $i + 2); ?> + renderCallStackItem(@$trace[$i]['file'] ?: null, @$trace[$i]['line'] ?: null, + @$trace[$i]['class'] ?: null, @$trace[$i]['function'] ?: null, $i + 2) ?>
From 63d391b3098930eada080b41e295f0e824c10674 Mon Sep 17 00:00:00 2001 From: Panagiotis Moustafellos Date: Tue, 29 Oct 2013 01:03:22 +0200 Subject: [PATCH 16/17] typo fix s/value to be cached value/value to be cached/g --- framework/yii/caching/Cache.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/framework/yii/caching/Cache.php b/framework/yii/caching/Cache.php index f9d117d..b2d96dc 100644 --- a/framework/yii/caching/Cache.php +++ b/framework/yii/caching/Cache.php @@ -192,7 +192,7 @@ abstract class Cache extends Component implements \ArrayAccess * If the cache already contains such a key, the existing value and * expiration time will be replaced with the new ones, respectively. * - * @param mixed $key a key identifying the value to be cached value. This can be a simple string or + * @param mixed $key a key identifying the value to be cached. This can be a simple string or * a complex data structure consisting of factors representing the key. * @param mixed $value the value to be cached * @param integer $expire the number of seconds in which the cached value will expire. 0 means never expire. @@ -218,7 +218,7 @@ abstract class Cache extends Component implements \ArrayAccess /** * Stores a value identified by a key into cache if the cache does not contain this key. * Nothing will be done if the cache already contains the key. - * @param mixed $key a key identifying the value to be cached value. This can be a simple string or + * @param mixed $key a key identifying the value to be cached. This can be a simple string or * a complex data structure consisting of factors representing the key. * @param mixed $value the value to be cached * @param integer $expire the number of seconds in which the cached value will expire. 0 means never expire. From 155e9c6cba33039863d62686b309c248567d0c3a Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Mon, 28 Oct 2013 22:11:58 -0400 Subject: [PATCH 17/17] minor refactoring of view resolving code. --- framework/yii/base/View.php | 12 ++++++++---- framework/yii/base/ViewContextInterface.php | 14 +++++--------- framework/yii/base/Widget.php | 6 +++--- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/framework/yii/base/View.php b/framework/yii/base/View.php index 66b3be8..222f94b 100644 --- a/framework/yii/base/View.php +++ b/framework/yii/base/View.php @@ -225,8 +225,8 @@ class View extends Component * on how to specify this parameter. * @param object $context the context that the view should be used to search the view file. If null, * existing [[context]] will be used. - * @throws InvalidCallException if [[context]] is required and invalid. * @return string the view file path. Note that the file may not exist. + * @throws InvalidCallException if [[context]] is required and invalid. */ protected function findViewFile($view, $context = null) { @@ -236,9 +236,13 @@ class View extends Component } elseif (strncmp($view, '//', 2) === 0) { // e.g. "//layouts/main" $file = Yii::$app->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); - } elseif (strncmp($view, '/', 1) === 0 && Yii::$app->controller !== null) { + } elseif (strncmp($view, '/', 1) === 0) { // e.g. "/site/index" - $file = Yii::$app->controller->module->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); + if (Yii::$app->controller !== null) { + $file = Yii::$app->controller->module->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); + } else { + throw new InvalidCallException("Unable to locate view file for view '$view': no active controller."); + } } else { // context required if ($context === null) { @@ -247,7 +251,7 @@ class View extends Component if ($context instanceof ViewContextInterface) { $file = $context->findViewFile($view); } else { - throw new InvalidCallException('Current context is not supported.'); + throw new InvalidCallException("Unable to locate view file for view '$view': no active view context."); } } diff --git a/framework/yii/base/ViewContextInterface.php b/framework/yii/base/ViewContextInterface.php index 4237eca..94f6751 100644 --- a/framework/yii/base/ViewContextInterface.php +++ b/framework/yii/base/ViewContextInterface.php @@ -8,13 +8,9 @@ namespace yii\base; /** - * ViewContextInterface represents possible context for the view rendering. - * It determines the way the non-global view files are searched. - * This interface introduces method [[findViewFile]], which will be used - * at [[View::render()]] to determine the file by view name, which does - * not match default format. + * ViewContextInterface is the interface that should implemented by classes who want to support relative view names. * - * @see View + * The method [[findViewFile()]] should be implemented to convert a relative view name into a file path. * * @author Paul Klimov * @since 2.0 @@ -22,9 +18,9 @@ namespace yii\base; interface ViewContextInterface { /** - * Finds the view file based on the given view name. - * @param string $view the view name. + * Finds the view file corresponding to the specified relative view name. + * @param string $view a relative view name. The name does NOT start with a slash. * @return string the view file path. Note that the file may not exist. */ public function findViewFile($view); -} \ No newline at end of file +} diff --git a/framework/yii/base/Widget.php b/framework/yii/base/Widget.php index b6e0cd0..c47a89d 100644 --- a/framework/yii/base/Widget.php +++ b/framework/yii/base/Widget.php @@ -8,6 +8,7 @@ namespace yii\base; use Yii; +use ReflectionClass; /** * Widget is the base class for widgets. @@ -189,8 +190,7 @@ class Widget extends Component implements ViewContextInterface */ public function getViewPath() { - $className = get_class($this); - $class = new \ReflectionClass($className); + $class = new ReflectionClass($this); return dirname($class->getFileName()) . DIRECTORY_SEPARATOR . 'views'; } @@ -202,6 +202,6 @@ class Widget extends Component implements ViewContextInterface */ public function findViewFile($view) { - return $this->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); + return $this->getViewPath() . DIRECTORY_SEPARATOR . $view; } }