diff --git a/build/.htaccess b/build/.htaccess new file mode 100644 index 0000000..e019832 --- /dev/null +++ b/build/.htaccess @@ -0,0 +1 @@ +deny from all diff --git a/build/build b/build/build new file mode 100755 index 0000000..fff4282 --- /dev/null +++ b/build/build @@ -0,0 +1,20 @@ +#!/usr/bin/env php +run(); diff --git a/build/build.bat b/build/build.bat new file mode 100644 index 0000000..a1ae41f --- /dev/null +++ b/build/build.bat @@ -0,0 +1,23 @@ +@echo off + +rem ------------------------------------------------------------- +rem build script for Windows. +rem +rem This is the bootstrap script for running build on Windows. +rem +rem @author Qiang Xue +rem @link http://www.yiiframework.com/ +rem @copyright 2008 Yii Software LLC +rem @license http://www.yiiframework.com/license/ +rem @version $Id$ +rem ------------------------------------------------------------- + +@setlocal + +set BUILD_PATH=%~dp0 + +if "%PHP_COMMAND%" == "" set PHP_COMMAND=php.exe + +%PHP_COMMAND% "%BUILD_PATH%build" %* + +@endlocal \ No newline at end of file diff --git a/build/build.xml b/build/build.xml new file mode 100644 index 0000000..18a420d --- /dev/null +++ b/build/build.xml @@ -0,0 +1,276 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Building package ${pkgname}... + Copying files to build directory... + + + + + Changing file permissions... + + + + + + + + Generating source release file... + + + + + + + + + + + + + + + + + + + + + + + + Building documentation... + + Building Guide PDF... + + + + + + + Building Blog PDF... + + + + + + + Building API... + + + + + Generating doc release file... + + + + + + + + + + + + + + + + Building online API... + + + + Copying tutorials... + + + + + + + + Copying release text files... + + + + + + +Finished building Web files. +Please update yiisite/common/data/versions.php file with the following code: + + '1.1'=>array( + 'version'=>'${yii.version}', + 'revision'=>'${yii.revision}', + 'date'=>'${yii.date}', + 'latest'=>true, + ), + + + + + + Synchronizing code changes for ${pkgname}... + + Building autoload map... + + + Building yiilite.php... + + + + + Extracting i18n messages... + + + + + + + Cleaning up the build... + + + + + + + Welcome to use Yii build script! + -------------------------------- + You may use the following command format to build a target: + + phing <target name> + + where <target name> can be one of the following: + + - sync : synchronize yiilite.php and YiiBase.php + - message : extract i18n messages of the framework + - src : build source release + - doc : build documentation release (Windows only) + - clean : clean up the build + + + + diff --git a/build/controllers/LocaleController.php b/build/controllers/LocaleController.php new file mode 100644 index 0000000..d471c0d --- /dev/null +++ b/build/controllers/LocaleController.php @@ -0,0 +1,112 @@ + + * @since 2.0 + */ +class LocaleController extends Controller +{ + public $defaultAction = 'plural'; + + /** + * Generates the plural rules data. + * + * This command will parse the plural rule XML file from CLDR and convert them + * into appropriate PHP representation to support Yii message translation feature. + * @param string $xmlFile the original plural rule XML file (from CLDR). This file may be found in + * http://www.unicode.org/Public/cldr/latest/core.zip + * Extract the zip file and locate the file "common/supplemental/plurals.xml". + * @throws Exception + */ + public function actionPlural($xmlFile) + { + if (!is_file($xmlFile)) { + throw new Exception("The source plural rule file does not exist: $xmlFile"); + } + + $xml = simplexml_load_file($xmlFile); + + $allRules = array(); + + $patterns = array( + '/n in 0..1/' => '(n==0||n==1)', + '/\s+is\s+not\s+/i' => '!=', //is not + '/\s+is\s+/i' => '==', //is + '/n\s+mod\s+(\d+)/i' => 'fmod(n,$1)', //mod (CLDR's "mod" is "fmod()", not "%") + '/^(.*?)\s+not\s+in\s+(\d+)\.\.(\d+)/i' => '!in_array($1,range($2,$3))', //not in + '/^(.*?)\s+in\s+(\d+)\.\.(\d+)/i' => 'in_array($1,range($2,$3))', //in + '/^(.*?)\s+not\s+within\s+(\d+)\.\.(\d+)/i' => '($1<$2||$1>$3)', //not within + '/^(.*?)\s+within\s+(\d+)\.\.(\d+)/i' => '($1>=$2&&$1<=$3)', //within + ); + foreach ($xml->plurals->pluralRules as $node) { + $attributes = $node->attributes(); + $locales = explode(' ', $attributes['locales']); + $rules = array(); + + if (!empty($node->pluralRule)) { + foreach ($node->pluralRule as $rule) { + $expr_or = preg_split('/\s+or\s+/i', $rule); + foreach ($expr_or as $key_or => $val_or) { + $expr_and = preg_split('/\s+and\s+/i', $val_or); + $expr_and = preg_replace(array_keys($patterns), array_values($patterns), $expr_and); + $expr_or[$key_or] = implode('&&', $expr_and); + } + $expr = preg_replace('/\\bn\\b/', '$n', implode('||', $expr_or)); + $rules[] = preg_replace_callback('/range\((\d+),(\d+)\)/', function ($matches) { + if ($matches[2] - $matches[1] <= 5) { + return 'array(' . implode(',', range($matches[1], $matches[2])) . ')'; + } else { + return $matches[0]; + } + }, $expr); + + } + foreach ($locales as $locale) { + $allRules[$locale] = $rules; + } + } + } + // hard fix for "br": the rule is too complex + $allRules['br'] = array( + 0 => 'fmod($n,10)==1&&!in_array(fmod($n,100),array(11,71,91))', + 1 => 'fmod($n,10)==2&&!in_array(fmod($n,100),array(12,72,92))', + 2 => 'in_array(fmod($n,10),array(3,4,9))&&!in_array(fmod($n,100),array_merge(range(10,19),range(70,79),range(90,99)))', + 3 => 'fmod($n,1000000)==0&&$n!=0', + ); + if (preg_match('/\d+/', $xml->version['number'], $matches)) { + $revision = $matches[0]; + } else { + $revision = -1; + } + + echo "instance(); } else { diff --git a/framework/YiiBase.php b/framework/YiiBase.php index 16e237d..e81a288 100644 --- a/framework/YiiBase.php +++ b/framework/YiiBase.php @@ -192,6 +192,7 @@ class YiiBase * @param boolean $throwException whether to throw an exception if the given alias is invalid. * If this is false and an invalid alias is given, false will be returned by this method. * @return string|boolean path corresponding to the alias, false if the root alias is not previously registered. + * @throws InvalidParamException if the alias is invalid while $throwException is true. * @see setAlias */ public static function getAlias($alias, $throwException = true) @@ -231,6 +232,7 @@ class YiiBase * - a URL (e.g. `http://www.yiiframework.com`) * - a path alias (e.g. `@yii/base`). In this case, the path alias will be converted into the * actual path first by calling [[getAlias]]. + * * @throws Exception if $path is an invalid alias * @see getAlias */ @@ -238,7 +240,7 @@ class YiiBase { if ($path === null) { unset(self::$aliases[$alias]); - } elseif ($path[0] !== '@') { + } elseif (strncmp($path, '@', 1)) { self::$aliases[$alias] = rtrim($path, '\\/'); } else { self::$aliases[$alias] = static::getAlias($path); @@ -504,20 +506,28 @@ class YiiBase /** * Translates a message to the specified language. - * This method supports choice format (see {@link CChoiceFormat}), - * i.e., the message returned will be chosen from a few candidates according to the given - * number value. This feature is mainly used to solve plural format issue in case - * a message has different plural forms in some languages. - * @param string $message the original message - * @param array $params parameters to be applied to the message using strtr. - * The first parameter can be a number without key. - * And in this case, the method will call {@link CChoiceFormat::format} to choose - * an appropriate message translation. - * You can pass parameter for {@link CChoiceFormat::format} - * or plural forms format without wrapping it with array. - * @param string $language the target language. If null (default), the {@link CApplication::getLanguage application language} will be used. - * @return string the translated message - * @see CMessageSource + * + * The translation will be conducted according to the message category and the target language. + * To specify the category of the message, prefix the message with the category name and separate it + * with "|". For example, "app|hello world". If the category is not specified, the default category "app" + * will be used. The actual message translation is done by a [[\yii\i18n\MessageSource|message source]]. + * + * In case when a translated message has different plural forms (separated by "|"), this method + * will also attempt to choose an appropriate one according to a given numeric value which is + * specified as the first parameter (indexed by 0) in `$params`. + * + * For example, if a translated message is "I have an apple.|I have {n} apples.", and the first + * parameter is 2, the message returned will be "I have 2 apples.". Note that the placeholder "{n}" + * will be replaced with the given number. + * + * For more details on how plural rules are applied, please refer to: + * [[http://www.unicode.org/cldr/charts/supplemental/language_plural_rules.html]] + * + * @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`). If this is null, the current + * [[\yii\base\Application::language|application language]] will be used. + * @return string the translated message. */ public static function t($message, $params = array(), $language = null) { diff --git a/framework/base/Application.php b/framework/base/Application.php index fd2ecad..9be1939 100644 --- a/framework/base/Application.php +++ b/framework/base/Application.php @@ -78,7 +78,7 @@ class Application extends Module */ public $preload = array(); /** - * @var Controller the currently active controller instance + * @var \yii\web\Controller|\yii\console\Controller the currently active controller instance */ public $controller; /** @@ -355,7 +355,7 @@ class Application extends Module /** * Returns the request component. - * @return Request the request component + * @return \yii\web\Request|\yii\console\Request the request component */ public function getRequest() { diff --git a/framework/base/Component.php b/framework/base/Component.php index 2d081d3..f1d549b 100644 --- a/framework/base/Component.php +++ b/framework/base/Component.php @@ -422,7 +422,10 @@ class Component extends \yii\base\Object $this->ensureBehaviors(); if (isset($this->_e[$name]) && $this->_e[$name]->getCount()) { if ($event === null) { - $event = new Event($this); + $event = new Event; + } + if ($event->sender === null) { + $event->sender = $this; } $event->handled = false; $event->name = $name; diff --git a/framework/base/Controller.php b/framework/base/Controller.php index 17fb4da..241c66c 100644 --- a/framework/base/Controller.php +++ b/framework/base/Controller.php @@ -14,9 +14,6 @@ use yii\helpers\StringHelper; /** * Controller is the base class for classes containing controller logic. * - * @property string $route the route (module ID, controller ID and action ID) of the current request. - * @property string $uniqueId the controller ID that is prefixed with the module ID (if any). - * * @author Qiang Xue * @since 2.0 */ @@ -50,6 +47,11 @@ class Controller extends Component * by [[run()]] when it is called by [[Application]] to run an action. */ public $action; + /** + * @var View the view object that can be used to render views or view files. + */ + private $_view; + /** * @param string $id the ID of this controller @@ -138,7 +140,7 @@ class Controller extends Component } elseif ($pos > 0) { return $this->module->runAction($route, $params); } else { - return \Yii::$app->runAction(ltrim($route, '/'), $params); + return Yii::$app->runAction(ltrim($route, '/'), $params); } } @@ -296,6 +298,37 @@ class Controller extends Component /** * Renders a view and applies layout if available. + * + * The view to be rendered can be specified in one of the following formats: + * + * - 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 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]]. + * - relative path (e.g. "index"): the actual view file will be looked for under [[viewPath]]. + * + * To determine which layout should be applied, the following two steps are conducted: + * + * 1. In the first step, it determines the layout name and the context module: + * + * - If [[layout]] is specified as a string, use it as the layout name and [[module]] as the context module; + * - If [[layout]] is null, search through all ancestor modules of this controller and find the first + * module whose [[Module::layout|layout]] is not null. The layout and the corresponding module + * are used as the layout name and the context module, respectively. If such a module is not found + * or the corresponding layout is not a string, it will return false, meaning no applicable layout. + * + * 2. In the second step, it determines the actual layout file according to the previously found layout name + * and context module. The layout name can be + * + * - a path alias (e.g. "@app/views/layouts/main"); + * - an absolute path (e.g. "/main"): the layout name starts with a slash. The actual layout file will be + * looked for under the [[Application::layoutPath|layout path]] of the application; + * - a relative path (e.g. "main"): the actual layout layout file will be looked for under the + * [[Module::viewPath|view path]] of the context module. + * + * If the layout name does not contain a file extension, it will use the default one `.php`. + * * @param string $view the view name. Please refer to [[findViewFile()]] on how to specify a view name. * @param array $params the parameters (name-value pairs) that should be made available in the view. * These parameters will not be available in the layout. @@ -304,10 +337,11 @@ class Controller extends Component */ public function render($view, $params = array()) { - $output = Yii::$app->getView()->render($view, $params, $this); + $viewFile = $this->findViewFile($view); + $output = $this->getView()->renderFile($viewFile, $params, $this); $layoutFile = $this->findLayoutFile(); if ($layoutFile !== false) { - return Yii::$app->getView()->renderFile($layoutFile, array('content' => $output), $this); + return $this->getView()->renderFile($layoutFile, array('content' => $output), $this); } else { return $output; } @@ -316,14 +350,14 @@ class Controller extends Component /** * Renders a view. * This method differs from [[render()]] in that it does not apply any layout. - * @param string $view the view name. Please refer to [[findViewFile()]] on how to specify a view name. + * @param string $view the view name. Please refer to [[render()]] on how to specify a view name. * @param array $params the parameters (name-value pairs) that should be made available in the view. * @return string the rendering result. * @throws InvalidParamException if the view file does not exist. */ public function renderPartial($view, $params = array()) { - return Yii::$app->getView()->render($view, $params, $this); + return $this->getView()->render($view, $params, $this); } /** @@ -335,7 +369,30 @@ class Controller extends Component */ public function renderFile($file, $params = array()) { - return Yii::$app->getView()->renderFile($file, $params, $this); + return $this->getView()->renderFile($file, $params, $this); + } + + /** + * Returns the view object that can be used to render views or view files. + * The [[render()]], [[renderPartial()]] and [[renderFile()]] methods will use + * this view object to implement the actual view rendering. + * @return View the view object that can be used to render views or view files. + */ + public function getView() + { + if ($this->_view === null) { + $this->_view = Yii::$app->getView(); + } + return $this->_view; + } + + /** + * Sets the view object to be used by this controller. + * @param View $view the view object that can be used to render views or view files. + */ + public function setView($view) + { + $this->_view = $view; } /** @@ -350,30 +407,33 @@ class Controller 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. + * @return string the view file path. Note that the file may not exist. + */ + protected 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 FileHelper::getExtension($file) === '' ? $file . '.php' : $file; + } + + /** * Finds the applicable layout file. - * - * This method locates an applicable layout file via two steps. - * - * In the first step, it determines the layout name and the context module: - * - * - If [[layout]] is specified as a string, use it as the layout name and [[module]] as the context module; - * - If [[layout]] is null, search through all ancestor modules of this controller and find the first - * module whose [[Module::layout|layout]] is not null. The layout and the corresponding module - * are used as the layout name and the context module, respectively. If such a module is not found - * or the corresponding layout is not a string, it will return false, meaning no applicable layout. - * - * In the second step, it determines the actual layout file according to the previously found layout name - * and context module. The layout name can be - * - * - a path alias (e.g. "@app/views/layouts/main"); - * - an absolute path (e.g. "/main"): the layout name starts with a slash. The actual layout file will be - * looked for under the [[Application::layoutPath|layout path]] of the application; - * - a relative path (e.g. "main"): the actual layout layout file will be looked for under the - * [[Module::viewPath|view path]] of the context module. - * - * If the layout name does not contain a file extension, it will use the default one `.php`. - * * @return string|boolean the layout file path, or false if layout is not needed. + * Please refer to [[render()]] on how to specify this parameter. * @throws InvalidParamException if an invalid path alias is used to specify the layout */ protected function findLayoutFile() diff --git a/framework/base/Dictionary.php b/framework/base/Dictionary.php index 9343d68..52262cb 100644 --- a/framework/base/Dictionary.php +++ b/framework/base/Dictionary.php @@ -148,7 +148,7 @@ class Dictionary extends Object implements \IteratorAggregate, \ArrayAccess, \Co * Defaults to false, meaning all items in the dictionary will be cleared directly * without calling [[remove]]. */ - public function clear($safeClear = false) + public function removeAll($safeClear = false) { if ($safeClear) { foreach (array_keys($this->_d) as $key) { @@ -164,7 +164,7 @@ class Dictionary extends Object implements \IteratorAggregate, \ArrayAccess, \Co * @param mixed $key the key * @return boolean whether the dictionary contains an item with the specified key */ - public function contains($key) + public function has($key) { return isset($this->_d[$key]) || array_key_exists($key, $this->_d); } @@ -188,7 +188,7 @@ class Dictionary extends Object implements \IteratorAggregate, \ArrayAccess, \Co { if (is_array($data) || $data instanceof \Traversable) { if ($this->_d !== array()) { - $this->clear(); + $this->removeAll(); } if ($data instanceof self) { $data = $data->_d; @@ -252,7 +252,7 @@ class Dictionary extends Object implements \IteratorAggregate, \ArrayAccess, \Co */ public function offsetExists($offset) { - return $this->contains($offset); + return $this->has($offset); } /** diff --git a/framework/base/ErrorHandler.php b/framework/base/ErrorHandler.php index f71b8c8..a2f372c 100644 --- a/framework/base/ErrorHandler.php +++ b/framework/base/ErrorHandler.php @@ -16,8 +16,6 @@ namespace yii\base; * @author Qiang Xue * @since 2.0 */ -use yii\helpers\VarDumper; - class ErrorHandler extends Component { /** @@ -63,10 +61,10 @@ class ErrorHandler extends Component $this->clearOutput(); } - $this->render($exception); + $this->renderException($exception); } - protected function render($exception) + protected function renderException($exception) { if ($this->errorAction !== null) { \Yii::$app->runAction($this->errorAction); @@ -84,7 +82,7 @@ class ErrorHandler extends Component } else { $viewName = $this->exceptionView; } - echo $view->render($viewName, array( + echo $view->renderFile($viewName, array( 'exception' => $exception, ), $this); } @@ -255,7 +253,7 @@ class ErrorHandler extends Component { $view = new View; $name = !YII_DEBUG || $exception instanceof HttpException ? $this->errorView : $this->exceptionView; - echo $view->render($name, array( + echo $view->renderFile($name, array( 'exception' => $exception, ), $this); } diff --git a/framework/base/Event.php b/framework/base/Event.php index 4ba57b2..b86ed7c 100644 --- a/framework/base/Event.php +++ b/framework/base/Event.php @@ -28,7 +28,8 @@ class Event extends \yii\base\Object */ public $name; /** - * @var object the sender of this event + * @var object the sender of this event. If not set, this property will be + * set as the object whose "trigger()" method is called. */ public $sender; /** @@ -38,21 +39,7 @@ class Event extends \yii\base\Object */ public $handled = false; /** - * @var mixed extra data associated with the event. + * @var mixed extra custom data associated with the event. */ public $data; - - /** - * Constructor. - * - * @param mixed $sender sender of the event - * @param mixed $data extra data associated with the event - * @param array $config name-value pairs that will be used to initialize the object properties - */ - public function __construct($sender = null, $data = null, $config = array()) - { - $this->sender = $sender; - $this->data = $data; - parent::__construct($config); - } } diff --git a/framework/base/HttpException.php b/framework/base/HttpException.php index 94a9a55..948d96b 100644 --- a/framework/base/HttpException.php +++ b/framework/base/HttpException.php @@ -29,11 +29,12 @@ class HttpException extends UserException * @param integer $status HTTP status code, such as 404, 500, etc. * @param string $message error message * @param integer $code error code + * @param \Exception $previous The previous exception used for the exception chaining. */ - public function __construct($status, $message = null, $code = 0) + public function __construct($status, $message = null, $code = 0, \Exception $previous = null) { $this->statusCode = $status; - parent::__construct($message, $code); + parent::__construct($message, $code, $previous); } /** diff --git a/framework/base/Model.php b/framework/base/Model.php index 402a558..611b12e 100644 --- a/framework/base/Model.php +++ b/framework/base/Model.php @@ -258,7 +258,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess */ public function beforeValidate() { - $event = new ModelEvent($this); + $event = new ModelEvent; $this->trigger(self::EVENT_BEFORE_VALIDATE, $event); return $event->isValid; } @@ -329,7 +329,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess foreach ($this->rules() as $rule) { if ($rule instanceof Validator) { $validators->add($rule); - } elseif (isset($rule[0], $rule[1])) { // attributes, validator type + } elseif (is_array($rule) && isset($rule[0], $rule[1])) { // attributes, validator type $validator = Validator::createValidator($rule[1], $this, $rule[0], array_slice($rule, 2)); $validators->add($validator); } else { @@ -429,9 +429,9 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess return array(); } else { $errors = array(); - foreach ($this->_errors as $errors) { - if (isset($errors[0])) { - $errors[] = $errors[0]; + foreach ($this->_errors as $attributeErrors) { + if (isset($attributeErrors[0])) { + $errors[] = $attributeErrors[0]; } } } @@ -541,7 +541,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess public function onUnsafeAttribute($name, $value) { if (YII_DEBUG) { - \Yii::info("Failed to set unsafe attribute '$name' in '" . get_class($this) . "'.", __CLASS__); + \Yii::info("Failed to set unsafe attribute '$name' in '" . get_class($this) . "'.", __METHOD__); } } @@ -656,13 +656,13 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess } /** - * Unsets the element at the specified offset. + * Sets the element value at the specified offset to null. * This method is required by the SPL interface `ArrayAccess`. * It is implicitly called when you use something like `unset($model[$offset])`. * @param mixed $offset the offset to unset element */ public function offsetUnset($offset) { - unset($this->$offset); + $this->$offset = null; } } diff --git a/framework/base/Module.php b/framework/base/Module.php index 9988164..3e7eb04 100644 --- a/framework/base/Module.php +++ b/framework/base/Module.php @@ -1,623 +1,629 @@ - configuration). - * @property array $components The components (indexed by their IDs) registered within this module. - * @property array $import List of aliases to be imported. This property is write-only. - * @property array $aliases List of aliases to be defined. This property is write-only. - * - * @author Qiang Xue - * @since 2.0 - */ -abstract class Module extends Component -{ - /** - * @var array custom module parameters (name => value). - */ - public $params = array(); - /** - * @var array the IDs of the components that should be preloaded when this module is created. - */ - public $preload = array(); - /** - * @var string an ID that uniquely identifies this module among other modules which have the same [[module|parent]]. - */ - public $id; - /** - * @var Module the parent module of this module. Null if this module does not have a parent. - */ - public $module; - /** - * @var string|boolean the layout that should be applied for views within this module. This refers to a view name - * relative to [[layoutPath]]. If this is not set, it means the layout value of the [[module|parent module]] - * will be taken. If this is false, layout will be disabled within this module. - */ - public $layout; - /** - * @var array mapping from controller ID to controller configurations. - * Each name-value pair specifies the configuration of a single controller. - * A controller configuration can be either a string or an array. - * If the former, the string should be the class name or path alias of the controller. - * If the latter, the array must contain a 'class' element which specifies - * the controller's class name or path alias, and the rest of the name-value pairs - * in the array are used to initialize the corresponding controller properties. For example, - * - * ~~~ - * array( - * 'account' => '@app/controllers/UserController', - * 'article' => array( - * 'class' => '@app/controllers/PostController', - * 'pageTitle' => 'something new', - * ), - * ) - * ~~~ - */ - public $controllerMap = array(); - /** - * @var string the namespace that controller classes are in. Default is to use global namespace. - */ - public $controllerNamespace; - /** - * @return string the default route of this module. Defaults to 'default'. - * The route may consist of child module ID, controller ID, and/or action ID. - * For example, `help`, `post/create`, `admin/post/create`. - * If action ID is not given, it will take the default value as specified in - * [[Controller::defaultAction]]. - */ - public $defaultRoute = 'default'; - /** - * @var string the root directory of the module. - */ - private $_basePath; - /** - * @var string the root directory that contains view files for this module - */ - private $_viewPath; - /** - * @var string the root directory that contains layout view files for this module. - */ - private $_layoutPath; - /** - * @var string the directory containing controller classes in the module. - */ - private $_controllerPath; - /** - * @var array child modules of this module - */ - private $_modules = array(); - /** - * @var array components registered under this module - */ - private $_components = array(); - - /** - * Constructor. - * @param string $id the ID of this module - * @param Module $parent the parent module (if any) - * @param array $config name-value pairs that will be used to initialize the object properties - */ - public function __construct($id, $parent = null, $config = array()) - { - $this->id = $id; - $this->module = $parent; - parent::__construct($config); - } - - /** - * Getter magic method. - * This method is overridden to support accessing components - * like reading module properties. - * @param string $name component or property name - * @return mixed the named property value - */ - public function __get($name) - { - if ($this->hasComponent($name)) { - return $this->getComponent($name); - } else { - return parent::__get($name); - } - } - - /** - * Checks if a property value is null. - * This method overrides the parent implementation by checking - * if the named component is loaded. - * @param string $name the property name or the event name - * @return boolean whether the property value is null - */ - public function __isset($name) - { - if ($this->hasComponent($name)) { - return $this->getComponent($name) !== null; - } else { - return parent::__isset($name); - } - } - - /** - * Initializes the module. - * This method is called after the module is created and initialized with property values - * given in configuration. The default implement will create a path alias using the module [[id]] - * and then call [[preloadComponents()]] to load components that are declared in [[preload]]. - */ - public function init() - { - Yii::setAlias('@' . $this->id, $this->getBasePath()); - $this->preloadComponents(); - } - - /** - * Returns an ID that uniquely identifies this module among all modules within the current application. - * Note that if the module is an application, an empty string will be returned. - * @return string the unique ID of the module. - */ - public function getUniqueId() - { - if ($this instanceof Application) { - return ''; - } elseif ($this->module) { - return $this->module->getUniqueId() . '/' . $this->id; - } else { - return $this->id; - } - } - - /** - * Returns the root directory of the module. - * It defaults to the directory containing the module class file. - * @return string the root directory of the module. - */ - public function getBasePath() - { - if ($this->_basePath === null) { - $class = new \ReflectionClass($this); - $this->_basePath = dirname($class->getFileName()); - } - return $this->_basePath; - } - - /** - * Sets the root directory of the module. - * This method can only be invoked at the beginning of the constructor. - * @param string $path the root directory of the module. This can be either a directory name or a path alias. - * @throws Exception if the directory does not exist. - */ - public function setBasePath($path) - { - $this->_basePath = FileHelper::ensureDirectory($path); - } - - /** - * Returns the directory that contains the controller classes. - * Defaults to "[[basePath]]/controllers". - * @return string the directory that contains the controller classes. - */ - public function getControllerPath() - { - if ($this->_controllerPath !== null) { - return $this->_controllerPath; - } else { - return $this->_controllerPath = $this->getBasePath() . DIRECTORY_SEPARATOR . 'controllers'; - } - } - - /** - * Sets the directory that contains the controller classes. - * @param string $path the directory that contains the controller classes. - * This can be either a directory name or a path alias. - * @throws Exception if the directory is invalid - */ - public function setControllerPath($path) - { - $this->_controllerPath = FileHelper::ensureDirectory($path); - } - - /** - * Returns the directory that contains the view files for this module. - * @return string the root directory of view files. Defaults to "[[basePath]]/view". - */ - public function getViewPath() - { - if ($this->_viewPath !== null) { - return $this->_viewPath; - } else { - return $this->_viewPath = $this->getBasePath() . DIRECTORY_SEPARATOR . 'views'; - } - } - - /** - * Sets the directory that contains the view files. - * @param string $path the root directory of view files. - * @throws Exception if the directory is invalid - */ - public function setViewPath($path) - { - $this->_viewPath = FileHelper::ensureDirectory($path); - } - - /** - * Returns the directory that contains layout view files for this module. - * @return string the root directory of layout files. Defaults to "[[viewPath]]/layouts". - */ - public function getLayoutPath() - { - if ($this->_layoutPath !== null) { - return $this->_layoutPath; - } else { - return $this->_layoutPath = $this->getViewPath() . DIRECTORY_SEPARATOR . 'layouts'; - } - } - - /** - * Sets the directory that contains the layout files. - * @param string $path the root directory of layout files. - * @throws Exception if the directory is invalid - */ - public function setLayoutPath($path) - { - $this->_layoutPath = FileHelper::ensureDirectory($path); - } - - /** - * Imports the specified path aliases. - * This method is provided so that you can import a set of path aliases when configuring a module. - * The path aliases will be imported by calling [[Yii::import()]]. - * @param array $aliases list of path aliases to be imported - */ - public function setImport($aliases) - { - foreach ($aliases as $alias) { - Yii::import($alias); - } - } - - /** - * Defines path aliases. - * This method calls [[Yii::setAlias()]] to register the path aliases. - * This method is provided so that you can define path aliases when configuring a module. - * @param array $aliases list of path aliases to be defined. The array keys are alias names - * (must start with '@') and the array values are the corresponding paths or aliases. - * For example, - * - * ~~~ - * array( - * '@models' => '@app/models', // an existing alias - * '@backend' => __DIR__ . '/../backend', // a directory - * ) - * ~~~ - */ - public function setAliases($aliases) - { - foreach ($aliases as $name => $alias) { - Yii::setAlias($name, $alias); - } - } - - /** - * Checks whether the named module exists. - * @param string $id module ID - * @return boolean whether the named module exists. Both loaded and unloaded modules - * are considered. - */ - public function hasModule($id) - { - return isset($this->_modules[$id]); - } - - /** - * Retrieves the named module. - * @param string $id module ID (case-sensitive) - * @param boolean $load whether to load the module if it is not yet loaded. - * @return Module|null the module instance, null if the module - * does not exist. - * @see hasModule() - */ - public function getModule($id, $load = true) - { - if (isset($this->_modules[$id])) { - if ($this->_modules[$id] instanceof Module) { - return $this->_modules[$id]; - } elseif ($load) { - Yii::trace("Loading module: $id", __CLASS__); - return $this->_modules[$id] = Yii::createObject($this->_modules[$id], $id, $this); - } - } - return null; - } - - /** - * Adds a sub-module to this module. - * @param string $id module ID - * @param Module|array|null $module the sub-module to be added to this module. This can - * be one of the followings: - * - * - a [[Module]] object - * - a configuration array: when [[getModule()]] is called initially, the array - * will be used to instantiate the sub-module - * - null: the named sub-module will be removed from this module - */ - public function setModule($id, $module) - { - if ($module === null) { - unset($this->_modules[$id]); - } else { - $this->_modules[$id] = $module; - } - } - - /** - * Returns the sub-modules in this module. - * @param boolean $loadedOnly whether to return the loaded sub-modules only. If this is set false, - * then all sub-modules registered in this module will be returned, whether they are loaded or not. - * Loaded modules will be returned as objects, while unloaded modules as configuration arrays. - * @return array the modules (indexed by their IDs) - */ - public function getModules($loadedOnly = false) - { - if ($loadedOnly) { - $modules = array(); - foreach ($this->_modules as $module) { - if ($module instanceof Module) { - $modules[] = $module; - } - } - return $modules; - } else { - return $this->_modules; - } - } - - /** - * Registers sub-modules in the current module. - * - * Each sub-module should be specified as a name-value pair, where - * name refers to the ID of the module and value the module or a configuration - * array that can be used to create the module. In the latter case, [[Yii::createObject()]] - * will be used to create the module. - * - * If a new sub-module has the same ID as an existing one, the existing one will be overwritten silently. - * - * The following is an example for registering two sub-modules: - * - * ~~~ - * array( - * 'comment' => array( - * 'class' => 'app\modules\CommentModule', - * 'connectionID' => 'db', - * ), - * 'booking' => array( - * 'class' => 'app\modules\BookingModule', - * ), - * ) - * ~~~ - * - * @param array $modules modules (id => module configuration or instances) - */ - public function setModules($modules) - { - foreach ($modules as $id => $module) { - $this->_modules[$id] = $module; - } - } - - /** - * Checks whether the named component exists. - * @param string $id component ID - * @return boolean whether the named component exists. Both loaded and unloaded components - * are considered. - */ - public function hasComponent($id) - { - return isset($this->_components[$id]); - } - - /** - * Retrieves the named component. - * @param string $id component ID (case-sensitive) - * @param boolean $load whether to load the component if it is not yet loaded. - * @return Component|null the component instance, null if the component does not exist. - * @see hasComponent() - */ - public function getComponent($id, $load = true) - { - if (isset($this->_components[$id])) { - if ($this->_components[$id] instanceof Component) { - return $this->_components[$id]; - } elseif ($load) { - Yii::trace("Loading component: $id", __CLASS__); - return $this->_components[$id] = Yii::createObject($this->_components[$id]); - } - } - return null; - } - - /** - * Registers a component with this module. - * @param string $id component ID - * @param Component|array|null $component the component to be registered with the module. This can - * be one of the followings: - * - * - a [[Component]] object - * - a configuration array: when [[getComponent()]] is called initially for this component, the array - * will be used to instantiate the component via [[Yii::createObject()]]. - * - null: the named component will be removed from the module - */ - public function setComponent($id, $component) - { - if ($component === null) { - unset($this->_components[$id]); - } else { - $this->_components[$id] = $component; - } - } - - /** - * Returns the registered components. - * @param boolean $loadedOnly whether to return the loaded components only. If this is set false, - * then all components specified in the configuration will be returned, whether they are loaded or not. - * Loaded components will be returned as objects, while unloaded components as configuration arrays. - * @return array the components (indexed by their IDs) - */ - public function getComponents($loadedOnly = false) - { - if ($loadedOnly) { - $components = array(); - foreach ($this->_components as $component) { - if ($component instanceof Component) { - $components[] = $component; - } - } - return $components; - } else { - return $this->_components; - } - } - - /** - * Registers a set of components in this module. - * - * Each component should be specified as a name-value pair, where - * name refers to the ID of the component and value the component or a configuration - * array that can be used to create the component. In the latter case, [[Yii::createObject()]] - * will be used to create the component. - * - * If a new component has the same ID as an existing one, the existing one will be overwritten silently. - * - * The following is an example for setting two components: - * - * ~~~ - * array( - * 'db' => array( - * 'class' => 'yii\db\Connection', - * 'dsn' => 'sqlite:path/to/file.db', - * ), - * 'cache' => array( - * 'class' => 'yii\caching\DbCache', - * 'connectionID' => 'db', - * ), - * ) - * ~~~ - * - * @param array $components components (id => component configuration or instance) - */ - public function setComponents($components) - { - foreach ($components as $id => $component) { - $this->_components[$id] = $component; - } - } - - /** - * Loads components that are declared in [[preload]]. - */ - public function preloadComponents() - { - foreach ($this->preload as $id) { - $this->getComponent($id); - } - } - - /** - * Runs a controller action specified by a route. - * This method parses the specified route and creates the corresponding child module(s), controller and action - * instances. It then calls [[Controller::runAction()]] to run the action with the given parameters. - * If the route is empty, the method will use [[defaultRoute]]. - * @param string $route the route that specifies the action. - * @param array $params the parameters to be passed to the action - * @return integer the status code returned by the action execution. 0 means normal, and other values mean abnormal. - * @throws InvalidRouteException if the requested route cannot be resolved into an action successfully - */ - public function runAction($route, $params = array()) - { - $result = $this->createController($route); - if (is_array($result)) { - /** @var $controller Controller */ - list($controller, $actionID) = $result; - $oldController = Yii::$app->controller; - Yii::$app->controller = $controller; - $status = $controller->runAction($actionID, $params); - Yii::$app->controller = $oldController; - return $status; - } else { - throw new InvalidRouteException('Unable to resolve the request "' . trim($this->getUniqueId() . '/' . $route, '/') . '".'); - } - } - - /** - * Creates a controller instance based on the controller ID. - * - * The controller is created within this module. The method first attempts to - * create the controller based on the [[controllerMap]] of the module. If not available, - * it will look for the controller class under the [[controllerPath]] and create an - * instance of it. - * - * @param string $route the route consisting of module, controller and action IDs. - * @return array|boolean if the controller is created successfully, it will be returned together - * with the remainder of the route which represents the action ID. Otherwise false will be returned. - */ - public function createController($route) - { - if ($route === '') { - $route = $this->defaultRoute; - } - if (($pos = strpos($route, '/')) !== false) { - $id = substr($route, 0, $pos); - $route = substr($route, $pos + 1); - } else { - $id = $route; - $route = ''; - } - - $module = $this->getModule($id); - if ($module !== null) { - return $module->createController($route); - } - - if (isset($this->controllerMap[$id])) { - $controller = Yii::createObject($this->controllerMap[$id], $id, $this); - } elseif (preg_match('/^[a-z0-9\\-_]+$/', $id)) { - $className = StringHelper::id2camel($id) . 'Controller'; - - $classFile = $this->controllerPath . DIRECTORY_SEPARATOR . $className . '.php'; - if (is_file($classFile)) { - $className = $this->controllerNamespace . '\\' . $className; - if (!class_exists($className, false)) { - require($classFile); - } - if (class_exists($className, false) && is_subclass_of($className, '\yii\base\Controller')) { - $controller = new $className($id, $this); - } - } - } - - return isset($controller) ? array($controller, $route) : false; - } -} + configuration). + * @property array $components The components (indexed by their IDs) registered within this module. + * @property array $import List of aliases to be imported. This property is write-only. + * @property array $aliases List of aliases to be defined. This property is write-only. + * + * @author Qiang Xue + * @since 2.0 + */ +abstract class Module extends Component +{ + /** + * @var array custom module parameters (name => value). + */ + public $params = array(); + /** + * @var array the IDs of the components that should be preloaded when this module is created. + */ + public $preload = array(); + /** + * @var string an ID that uniquely identifies this module among other modules which have the same [[module|parent]]. + */ + public $id; + /** + * @var Module the parent module of this module. Null if this module does not have a parent. + */ + public $module; + /** + * @var string|boolean the layout that should be applied for views within this module. This refers to a view name + * relative to [[layoutPath]]. If this is not set, it means the layout value of the [[module|parent module]] + * will be taken. If this is false, layout will be disabled within this module. + */ + public $layout; + /** + * @var array mapping from controller ID to controller configurations. + * Each name-value pair specifies the configuration of a single controller. + * A controller configuration can be either a string or an array. + * If the former, the string should be the class name or path alias of the controller. + * If the latter, the array must contain a 'class' element which specifies + * the controller's class name or path alias, and the rest of the name-value pairs + * in the array are used to initialize the corresponding controller properties. For example, + * + * ~~~ + * array( + * 'account' => '@app/controllers/UserController', + * 'article' => array( + * 'class' => '@app/controllers/PostController', + * 'pageTitle' => 'something new', + * ), + * ) + * ~~~ + */ + public $controllerMap = array(); + /** + * @var string the namespace that controller classes are in. Default is to use global namespace. + */ + public $controllerNamespace; + /** + * @return string the default route of this module. Defaults to 'default'. + * The route may consist of child module ID, controller ID, and/or action ID. + * For example, `help`, `post/create`, `admin/post/create`. + * If action ID is not given, it will take the default value as specified in + * [[Controller::defaultAction]]. + */ + public $defaultRoute = 'default'; + /** + * @var string the root directory of the module. + */ + private $_basePath; + /** + * @var string the root directory that contains view files for this module + */ + private $_viewPath; + /** + * @var string the root directory that contains layout view files for this module. + */ + private $_layoutPath; + /** + * @var string the directory containing controller classes in the module. + */ + private $_controllerPath; + /** + * @var array child modules of this module + */ + private $_modules = array(); + /** + * @var array components registered under this module + */ + private $_components = array(); + + /** + * Constructor. + * @param string $id the ID of this module + * @param Module $parent the parent module (if any) + * @param array $config name-value pairs that will be used to initialize the object properties + */ + public function __construct($id, $parent = null, $config = array()) + { + $this->id = $id; + $this->module = $parent; + parent::__construct($config); + } + + /** + * Getter magic method. + * This method is overridden to support accessing components + * like reading module properties. + * @param string $name component or property name + * @return mixed the named property value + */ + public function __get($name) + { + if ($this->hasComponent($name)) { + return $this->getComponent($name); + } else { + return parent::__get($name); + } + } + + /** + * Checks if a property value is null. + * This method overrides the parent implementation by checking + * if the named component is loaded. + * @param string $name the property name or the event name + * @return boolean whether the property value is null + */ + public function __isset($name) + { + if ($this->hasComponent($name)) { + return $this->getComponent($name) !== null; + } else { + return parent::__isset($name); + } + } + + /** + * Initializes the module. + * This method is called after the module is created and initialized with property values + * given in configuration. The default implement will create a path alias using the module [[id]] + * and then call [[preloadComponents()]] to load components that are declared in [[preload]]. + */ + public function init() + { + Yii::setAlias('@' . $this->id, $this->getBasePath()); + $this->preloadComponents(); + } + + /** + * Returns an ID that uniquely identifies this module among all modules within the current application. + * Note that if the module is an application, an empty string will be returned. + * @return string the unique ID of the module. + */ + public function getUniqueId() + { + if ($this instanceof Application) { + return ''; + } elseif ($this->module) { + return $this->module->getUniqueId() . '/' . $this->id; + } else { + return $this->id; + } + } + + /** + * Returns the root directory of the module. + * It defaults to the directory containing the module class file. + * @return string the root directory of the module. + */ + public function getBasePath() + { + if ($this->_basePath === null) { + $class = new \ReflectionClass($this); + $this->_basePath = dirname($class->getFileName()); + } + return $this->_basePath; + } + + /** + * Sets the root directory of the module. + * This method can only be invoked at the beginning of the constructor. + * @param string $path the root directory of the module. This can be either a directory name or a path alias. + * @throws Exception if the directory does not exist. + */ + public function setBasePath($path) + { + $this->_basePath = FileHelper::ensureDirectory($path); + } + + /** + * Returns the directory that contains the controller classes. + * Defaults to "[[basePath]]/controllers". + * @return string the directory that contains the controller classes. + */ + public function getControllerPath() + { + if ($this->_controllerPath !== null) { + return $this->_controllerPath; + } else { + return $this->_controllerPath = $this->getBasePath() . DIRECTORY_SEPARATOR . 'controllers'; + } + } + + /** + * Sets the directory that contains the controller classes. + * @param string $path the directory that contains the controller classes. + * This can be either a directory name or a path alias. + * @throws Exception if the directory is invalid + */ + public function setControllerPath($path) + { + $this->_controllerPath = FileHelper::ensureDirectory($path); + } + + /** + * Returns the directory that contains the view files for this module. + * @return string the root directory of view files. Defaults to "[[basePath]]/view". + */ + public function getViewPath() + { + if ($this->_viewPath !== null) { + return $this->_viewPath; + } else { + return $this->_viewPath = $this->getBasePath() . DIRECTORY_SEPARATOR . 'views'; + } + } + + /** + * Sets the directory that contains the view files. + * @param string $path the root directory of view files. + * @throws Exception if the directory is invalid + */ + public function setViewPath($path) + { + $this->_viewPath = FileHelper::ensureDirectory($path); + } + + /** + * Returns the directory that contains layout view files for this module. + * @return string the root directory of layout files. Defaults to "[[viewPath]]/layouts". + */ + public function getLayoutPath() + { + if ($this->_layoutPath !== null) { + return $this->_layoutPath; + } else { + return $this->_layoutPath = $this->getViewPath() . DIRECTORY_SEPARATOR . 'layouts'; + } + } + + /** + * Sets the directory that contains the layout files. + * @param string $path the root directory of layout files. + * @throws Exception if the directory is invalid + */ + public function setLayoutPath($path) + { + $this->_layoutPath = FileHelper::ensureDirectory($path); + } + + /** + * Imports the specified path aliases. + * This method is provided so that you can import a set of path aliases when configuring a module. + * The path aliases will be imported by calling [[Yii::import()]]. + * @param array $aliases list of path aliases to be imported + */ + public function setImport($aliases) + { + foreach ($aliases as $alias) { + Yii::import($alias); + } + } + + /** + * Defines path aliases. + * This method calls [[Yii::setAlias()]] to register the path aliases. + * This method is provided so that you can define path aliases when configuring a module. + * @param array $aliases list of path aliases to be defined. The array keys are alias names + * (must start with '@') and the array values are the corresponding paths or aliases. + * For example, + * + * ~~~ + * array( + * '@models' => '@app/models', // an existing alias + * '@backend' => __DIR__ . '/../backend', // a directory + * ) + * ~~~ + */ + public function setAliases($aliases) + { + foreach ($aliases as $name => $alias) { + Yii::setAlias($name, $alias); + } + } + + /** + * Checks whether the named module exists. + * @param string $id module ID + * @return boolean whether the named module exists. Both loaded and unloaded modules + * are considered. + */ + public function hasModule($id) + { + return isset($this->_modules[$id]); + } + + /** + * Retrieves the named module. + * @param string $id module ID (case-sensitive) + * @param boolean $load whether to load the module if it is not yet loaded. + * @return Module|null the module instance, null if the module + * does not exist. + * @see hasModule() + */ + public function getModule($id, $load = true) + { + if (isset($this->_modules[$id])) { + if ($this->_modules[$id] instanceof Module) { + return $this->_modules[$id]; + } elseif ($load) { + Yii::trace("Loading module: $id", __METHOD__); + return $this->_modules[$id] = Yii::createObject($this->_modules[$id], $id, $this); + } + } + return null; + } + + /** + * Adds a sub-module to this module. + * @param string $id module ID + * @param Module|array|null $module the sub-module to be added to this module. This can + * be one of the followings: + * + * - a [[Module]] object + * - a configuration array: when [[getModule()]] is called initially, the array + * will be used to instantiate the sub-module + * - null: the named sub-module will be removed from this module + */ + public function setModule($id, $module) + { + if ($module === null) { + unset($this->_modules[$id]); + } else { + $this->_modules[$id] = $module; + } + } + + /** + * Returns the sub-modules in this module. + * @param boolean $loadedOnly whether to return the loaded sub-modules only. If this is set false, + * then all sub-modules registered in this module will be returned, whether they are loaded or not. + * Loaded modules will be returned as objects, while unloaded modules as configuration arrays. + * @return array the modules (indexed by their IDs) + */ + public function getModules($loadedOnly = false) + { + if ($loadedOnly) { + $modules = array(); + foreach ($this->_modules as $module) { + if ($module instanceof Module) { + $modules[] = $module; + } + } + return $modules; + } else { + return $this->_modules; + } + } + + /** + * Registers sub-modules in the current module. + * + * Each sub-module should be specified as a name-value pair, where + * name refers to the ID of the module and value the module or a configuration + * array that can be used to create the module. In the latter case, [[Yii::createObject()]] + * will be used to create the module. + * + * If a new sub-module has the same ID as an existing one, the existing one will be overwritten silently. + * + * The following is an example for registering two sub-modules: + * + * ~~~ + * array( + * 'comment' => array( + * 'class' => 'app\modules\CommentModule', + * 'db' => 'db', + * ), + * 'booking' => array( + * 'class' => 'app\modules\BookingModule', + * ), + * ) + * ~~~ + * + * @param array $modules modules (id => module configuration or instances) + */ + public function setModules($modules) + { + foreach ($modules as $id => $module) { + $this->_modules[$id] = $module; + } + } + + /** + * Checks whether the named component exists. + * @param string $id component ID + * @return boolean whether the named component exists. Both loaded and unloaded components + * are considered. + */ + public function hasComponent($id) + { + return isset($this->_components[$id]); + } + + /** + * Retrieves the named component. + * @param string $id component ID (case-sensitive) + * @param boolean $load whether to load the component if it is not yet loaded. + * @return Component|null the component instance, null if the component does not exist. + * @see hasComponent() + */ + public function getComponent($id, $load = true) + { + if (isset($this->_components[$id])) { + if ($this->_components[$id] instanceof Component) { + return $this->_components[$id]; + } elseif ($load) { + Yii::trace("Loading component: $id", __METHOD__); + return $this->_components[$id] = Yii::createObject($this->_components[$id]); + } + } + return null; + } + + /** + * Registers a component with this module. + * @param string $id component ID + * @param Component|array|null $component the component to be registered with the module. This can + * be one of the followings: + * + * - a [[Component]] object + * - a configuration array: when [[getComponent()]] is called initially for this component, the array + * will be used to instantiate the component via [[Yii::createObject()]]. + * - null: the named component will be removed from the module + */ + public function setComponent($id, $component) + { + if ($component === null) { + unset($this->_components[$id]); + } else { + $this->_components[$id] = $component; + } + } + + /** + * Returns the registered components. + * @param boolean $loadedOnly whether to return the loaded components only. If this is set false, + * then all components specified in the configuration will be returned, whether they are loaded or not. + * Loaded components will be returned as objects, while unloaded components as configuration arrays. + * @return array the components (indexed by their IDs) + */ + public function getComponents($loadedOnly = false) + { + if ($loadedOnly) { + $components = array(); + foreach ($this->_components as $component) { + if ($component instanceof Component) { + $components[] = $component; + } + } + return $components; + } else { + return $this->_components; + } + } + + /** + * Registers a set of components in this module. + * + * Each component should be specified as a name-value pair, where + * name refers to the ID of the component and value the component or a configuration + * array that can be used to create the component. In the latter case, [[Yii::createObject()]] + * will be used to create the component. + * + * If a new component has the same ID as an existing one, the existing one will be overwritten silently. + * + * The following is an example for setting two components: + * + * ~~~ + * array( + * 'db' => array( + * 'class' => 'yii\db\Connection', + * 'dsn' => 'sqlite:path/to/file.db', + * ), + * 'cache' => array( + * 'class' => 'yii\caching\DbCache', + * 'db' => 'db', + * ), + * ) + * ~~~ + * + * @param array $components components (id => component configuration or instance) + */ + public function setComponents($components) + { + foreach ($components as $id => $component) { + $this->_components[$id] = $component; + } + } + + /** + * Loads components that are declared in [[preload]]. + */ + public function preloadComponents() + { + foreach ($this->preload as $id) { + $this->getComponent($id); + } + } + + /** + * Runs a controller action specified by a route. + * This method parses the specified route and creates the corresponding child module(s), controller and action + * instances. It then calls [[Controller::runAction()]] to run the action with the given parameters. + * If the route is empty, the method will use [[defaultRoute]]. + * @param string $route the route that specifies the action. + * @param array $params the parameters to be passed to the action + * @return integer the status code returned by the action execution. 0 means normal, and other values mean abnormal. + * @throws InvalidRouteException if the requested route cannot be resolved into an action successfully + */ + public function runAction($route, $params = array()) + { + $result = $this->createController($route); + if (is_array($result)) { + /** @var $controller Controller */ + list($controller, $actionID) = $result; + $oldController = Yii::$app->controller; + Yii::$app->controller = $controller; + $status = $controller->runAction($actionID, $params); + Yii::$app->controller = $oldController; + return $status; + } else { + throw new InvalidRouteException('Unable to resolve the request "' . trim($this->getUniqueId() . '/' . $route, '/') . '".'); + } + } + + /** + * Creates a controller instance based on the controller ID. + * + * The controller is created within this module. The method first attempts to + * create the controller based on the [[controllerMap]] of the module. If not available, + * it will look for the controller class under the [[controllerPath]] and create an + * instance of it. + * + * @param string $route the route consisting of module, controller and action IDs. + * @return array|boolean if the controller is created successfully, it will be returned together + * with the remainder of the route which represents the action ID. Otherwise false will be returned. + */ + public function createController($route) + { + if ($route === '') { + $route = $this->defaultRoute; + } + if (($pos = strpos($route, '/')) !== false) { + $id = substr($route, 0, $pos); + $route = substr($route, $pos + 1); + } else { + $id = $route; + $route = ''; + } + + $module = $this->getModule($id); + if ($module !== null) { + return $module->createController($route); + } + + if (isset($this->controllerMap[$id])) { + $controller = Yii::createObject($this->controllerMap[$id], $id, $this); + } elseif (preg_match('/^[a-z0-9\\-_]+$/', $id)) { + $className = StringHelper::id2camel($id) . 'Controller'; + + $classFile = $this->controllerPath . DIRECTORY_SEPARATOR . $className . '.php'; + if (is_file($classFile)) { + $className = $this->controllerNamespace . '\\' . $className; + if (!class_exists($className, false)) { + require($classFile); + } + if (class_exists($className, false) && is_subclass_of($className, '\yii\base\Controller')) { + $controller = new $className($id, $this); + } elseif (YII_DEBUG) { + if (!class_exists($className, false)) { + throw new InvalidConfigException("Class file name does not match class name: $className."); + } elseif (!is_subclass_of($className, '\yii\base\Controller')) { + throw new InvalidConfigException("Controller class must extend from \\yii\\base\\Controller."); + } + } + } + } + + return isset($controller) ? array($controller, $route) : false; + } +} diff --git a/framework/base/Vector.php b/framework/base/Vector.php index 18f7037..7d43fdb 100644 --- a/framework/base/Vector.php +++ b/framework/base/Vector.php @@ -191,7 +191,7 @@ class Vector extends Object implements \IteratorAggregate, \ArrayAccess, \Counta * Defaults to false, meaning all items in the vector will be cleared directly * without calling [[removeAt]]. */ - public function clear($safeClear = false) + public function removeAll($safeClear = false) { if ($safeClear) { for ($i = $this->_c - 1; $i >= 0; --$i) { @@ -209,7 +209,7 @@ class Vector extends Object implements \IteratorAggregate, \ArrayAccess, \Counta * @param mixed $item the item * @return boolean whether the vector contains the item */ - public function contains($item) + public function has($item) { return $this->indexOf($item) >= 0; } @@ -246,7 +246,7 @@ class Vector extends Object implements \IteratorAggregate, \ArrayAccess, \Counta { if (is_array($data) || $data instanceof \Traversable) { if ($this->_c > 0) { - $this->clear(); + $this->removeAll(); } if ($data instanceof self) { $data = $data->_d; diff --git a/framework/base/View.php b/framework/base/View.php index c7087c1..d3d9339 100644 --- a/framework/base/View.php +++ b/framework/base/View.php @@ -79,22 +79,29 @@ class View extends Component /** * Renders a view. * - * This method will call [[findViewFile()]] to convert the view name into the corresponding view - * file path, and it will then call [[renderFile()]] to render the view. + * This method delegates the call to the [[context]] object: * - * @param string $view the view name. Please refer to [[findViewFile()]] on how to specify this parameter. + * - 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. + * + * @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 - * @see findViewFile */ - public function render($view, $params = array(), $context = null) + public function render($view, $params = array()) { - $viewFile = $this->findViewFile($context, $view); - return $this->renderFile($viewFile, $params, $context); + if ($this->context instanceof Controller) { + return $this->context->renderPartial($view, $params); + } elseif ($this->context instanceof Widget) { + return $this->context->render($view, $params); + } else { + throw new InvalidCallException('View::render() is not supported for the current context.'); + } } /** @@ -213,49 +220,6 @@ class View extends Component } /** - * Finds the view file based on the given view name. - * - * A view name can be specified in one of the following formats: - * - * - 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 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 the currently - * active module. - * - relative path (e.g. "index"): the actual view file will be looked for under [[Controller::viewPath|viewPath]] - * of the context object, assuming the context is either a [[Controller]] or a [[Widget]]. - * - * If the view name does not contain a file extension, it will use the default one `.php`. - * - * @param object $context the view context object - * @param string $view the view name or the path alias of the view file. - * @return string the view file path. Note that the file may not exist. - * @throws InvalidParamException if the view file is an invalid path alias or the context cannot be - * used to determine the actual view file corresponding to the specified view. - */ - protected function findViewFile($context, $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 = Yii::$app->controller->module->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); - } elseif ($context instanceof Controller || $context instanceof Widget) { - /** @var $context Controller|Widget */ - $file = $context->getViewPath() . DIRECTORY_SEPARATOR . $view; - } else { - throw new InvalidParamException("Unable to resolve the view file for '$view'."); - } - - return FileHelper::getExtension($file) === '' ? $file . '.php' : $file; - } - - /** * Creates a widget. * This method will use [[Yii::createObject()]] to create the widget. * @param string $class the widget class name or path alias @@ -265,7 +229,10 @@ class View extends Component public function createWidget($class, $properties = array()) { $properties['class'] = $class; - return Yii::createObject($properties, $this->context); + if (!isset($properties['view'])) { + $properties['view'] = $this; + } + return Yii::createObject($properties, $this); } /** @@ -341,7 +308,6 @@ class View extends Component return $this->beginWidget('yii\widgets\Clip', array( 'id' => $id, 'renderInPlace' => $renderInPlace, - 'view' => $this, )); } @@ -355,17 +321,25 @@ class View extends Component /** * Begins the rendering of content that is to be decorated by the specified view. - * @param string $view the name of the view that will be used to decorate the content enclosed by this widget. - * Please refer to [[View::findViewFile()]] on how to set this property. + * This method can be used to implement nested layout. For example, a layout can be embedded + * in another layout file specified as '@app/view/layouts/base' like the following: + * + * ~~~ + * beginContent('@app/view/layouts/base'); ?> + * ...layout content here... + * endContent(); ?> + * ~~~ + * + * @param string $viewFile the view file that will be used to decorate the content enclosed by this widget. + * This can be specified as either the view file path or path alias. * @param array $params the variables (name=>value) to be extracted and made available in the decorative view. * @return \yii\widgets\ContentDecorator the ContentDecorator widget instance * @see \yii\widgets\ContentDecorator */ - public function beginContent($view, $params = array()) + public function beginContent($viewFile, $params = array()) { return $this->beginWidget('yii\widgets\ContentDecorator', array( - 'view' => $this, - 'viewName' => $view, + 'viewFile' => $viewFile, 'params' => $params, )); } @@ -400,7 +374,6 @@ class View extends Component public function beginCache($id, $properties = array()) { $properties['id'] = $id; - $properties['view'] = $this; /** @var $cache \yii\widgets\FragmentCache */ $cache = $this->beginWidget('yii\widgets\FragmentCache', $properties); if ($cache->getCachedContent() !== false) { diff --git a/framework/base/Widget.php b/framework/base/Widget.php index 24d0685..c6667fa 100644 --- a/framework/base/Widget.php +++ b/framework/base/Widget.php @@ -19,9 +19,11 @@ use yii\helpers\FileHelper; class Widget extends Component { /** - * @var Widget|Controller the owner/creator of this widget. It could be either a widget or a controller. + * @var View the view object that is used to create this widget. + * This property is automatically set by [[View::createWidget()]]. + * This property is required by [[render()]] and [[renderFile()]]. */ - public $owner; + public $view; /** * @var string id of the widget. */ @@ -32,17 +34,6 @@ class Widget extends Component private static $_counter = 0; /** - * Constructor. - * @param Widget|Controller $owner owner/creator of this widget. - * @param array $config name-value pairs that will be used to initialize the object properties - */ - public function __construct($owner, $config = array()) - { - $this->owner = $owner; - parent::__construct($config); - } - - /** * Returns the ID of the widget. * @param boolean $autoGenerate whether to generate an ID if it is not set previously * @return string ID of the widget. @@ -73,6 +64,18 @@ class Widget extends Component /** * Renders a view. + * The view to be rendered can be specified in one of the following formats: + * + * - 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 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 the currently + * active module. + * - relative path (e.g. "index"): the actual view file will be looked for under [[viewPath]]. + * + * If the view name does not contain a file extension, it will use the default one `.php`. + * @param string $view the view name. Please refer to [[findViewFile()]] on how to specify a view name. * @param array $params the parameters (name-value pairs) that should be made available in the view. * @return string the rendering result. @@ -80,7 +83,7 @@ class Widget extends Component */ public function render($view, $params = array()) { - return Yii::$app->getView()->render($view, $params, $this); + return $this->view->render($view, $params, $this); } /** @@ -92,7 +95,7 @@ class Widget extends Component */ public function renderFile($file, $params = array()) { - return Yii::$app->getView()->renderFile($file, $params, $this); + return $this->view->renderFile($file, $params, $this); } /** @@ -106,4 +109,28 @@ class Widget extends Component $class = new \ReflectionClass($className); return dirname($class->getFileName()) . DIRECTORY_SEPARATOR . 'views'; } + + /** + * 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. + * @return string the view file path. Note that the file may not exist. + */ + protected 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 FileHelper::getExtension($file) === '' ? $file . '.php' : $file; + } } \ No newline at end of file diff --git a/framework/caching/ChainedDependency.php b/framework/caching/ChainedDependency.php index 9c4e547..af34e9d 100644 --- a/framework/caching/ChainedDependency.php +++ b/framework/caching/ChainedDependency.php @@ -57,7 +57,7 @@ class ChainedDependency extends Dependency if (!$dependency instanceof Dependency) { $dependency = \Yii::createObject($dependency); } - $dependency->evalulateDependency(); + $dependency->evaluateDependency(); } } diff --git a/framework/caching/DbCache.php b/framework/caching/DbCache.php index 44d0d03..dee8c7a 100644 --- a/framework/caching/DbCache.php +++ b/framework/caching/DbCache.php @@ -7,6 +7,7 @@ namespace yii\caching; +use Yii; use yii\base\InvalidConfigException; use yii\db\Connection; use yii\db\Query; @@ -14,30 +15,20 @@ use yii\db\Query; /** * DbCache implements a cache application component by storing cached data in a database. * - * DbCache stores cache data in a DB table whose name is specified via [[cacheTableName]]. - * For MySQL database, the table should be created beforehand as follows : - * - * ~~~ - * CREATE TABLE tbl_cache ( - * id char(128) NOT NULL, - * expire int(11) DEFAULT NULL, - * data LONGBLOB, - * PRIMARY KEY (id), - * KEY expire (expire) - * ); - * ~~~ - * - * You should replace `LONGBLOB` as follows if you are using a different DBMS: - * - * - PostgreSQL: `BYTEA` - * - SQLite, SQL server, Oracle: `BLOB` - * - * DbCache connects to the database via the DB connection specified in [[connectionID]] - * which must refer to a valid DB application component. + * By default, DbCache stores session data in a DB table named 'tbl_cache'. This table + * must be pre-created. The table name can be changed by setting [[cacheTable]]. * * Please refer to [[Cache]] for common cache operations that are supported by DbCache. * - * @property Connection $db The DB connection instance. + * The following example shows how you can configure the application to use DbCache: + * + * ~~~ + * 'cache' => array( + * 'class' => 'yii\caching\DbCache', + * // 'db' => 'mydb', + * // 'cacheTable' => 'my_cache', + * ) + * ~~~ * * @author Qiang Xue * @since 2.0 @@ -45,50 +36,56 @@ use yii\db\Query; class DbCache extends Cache { /** - * @var string the ID of the [[Connection|DB connection]] application component. Defaults to 'db'. + * @var Connection|string the DB connection object or the application component ID of the DB connection. + * After the DbCache object is created, if you want to change this property, you should only assign it + * with a DB connection object. */ - public $connectionID = 'db'; + public $db = 'db'; /** - * @var string name of the DB table to store cache content. Defaults to 'tbl_cache'. - * The table must be created before using this cache component. + * @var string name of the DB table to store cache content. + * The table should be pre-created as follows: + * + * ~~~ + * CREATE TABLE tbl_cache ( + * id char(128) NOT NULL PRIMARY KEY, + * expire int(11), + * data BLOB + * ); + * ~~~ + * + * where 'BLOB' refers to the BLOB-type of your preferred DBMS. Below are the BLOB type + * that can be used for some popular DBMS: + * + * - MySQL: LONGBLOB + * - PostgreSQL: BYTEA + * - MSSQL: BLOB + * + * When using DbCache in a production server, we recommend you create a DB index for the 'expire' + * column in the cache table to improve the performance. */ - public $cacheTableName = 'tbl_cache'; + public $cacheTable = 'tbl_cache'; /** * @var integer the probability (parts per million) that garbage collection (GC) should be performed - * when storing a piece of data in the cache. Defaults to 10, meaning 0.001% chance. + * when storing a piece of data in the cache. Defaults to 100, meaning 0.01% chance. * This number should be between 0 and 1000000. A value 0 meaning no GC will be performed at all. **/ public $gcProbability = 100; - /** - * @var Connection the DB connection instance - */ - private $_db; - /** - * Returns the DB connection instance used for caching purpose. - * @return Connection the DB connection instance - * @throws InvalidConfigException if [[connectionID]] does not point to a valid application component. - */ - public function getDb() - { - if ($this->_db === null) { - $db = \Yii::$app->getComponent($this->connectionID); - if ($db instanceof Connection) { - $this->_db = $db; - } else { - throw new InvalidConfigException("DbCache::connectionID must refer to the ID of a DB application component."); - } - } - return $this->_db; - } /** - * Sets the DB connection used by the cache component. - * @param Connection $value the DB connection instance + * Initializes the DbCache component. + * This method will initialize the [[db]] property to make sure it refers to a valid DB connection. + * @throws InvalidConfigException if [[db]] is invalid. */ - public function setDb($value) + public function init() { - $this->_db = $value; + parent::init(); + if (is_string($this->db)) { + $this->db = Yii::$app->getComponent($this->db); + } + if (!$this->db instanceof Connection) { + throw new InvalidConfigException("DbCache::db must be either a DB connection instance or the application component ID of a DB connection."); + } } /** @@ -101,17 +98,16 @@ class DbCache extends Cache { $query = new Query; $query->select(array('data')) - ->from($this->cacheTableName) - ->where('id = :id AND (expire = 0 OR expire >' . time() . ')', array(':id' => $key)); - $db = $this->getDb(); - if ($db->enableQueryCache) { + ->from($this->cacheTable) + ->where('[[id]] = :id AND ([[expire]] = 0 OR [[expire]] >' . time() . ')', array(':id' => $key)); + if ($this->db->enableQueryCache) { // temporarily disable and re-enable query caching - $db->enableQueryCache = false; - $result = $query->createCommand($db)->queryScalar(); - $db->enableQueryCache = true; + $this->db->enableQueryCache = false; + $result = $query->createCommand($this->db)->queryScalar(); + $this->db->enableQueryCache = true; return $result; } else { - return $query->createCommand($db)->queryScalar(); + return $query->createCommand($this->db)->queryScalar(); } } @@ -127,17 +123,16 @@ class DbCache extends Cache } $query = new Query; $query->select(array('id', 'data')) - ->from($this->cacheTableName) + ->from($this->cacheTable) ->where(array('id' => $keys)) - ->andWhere('(expire = 0 OR expire > ' . time() . ')'); + ->andWhere('([[expire]] = 0 OR [[expire]] > ' . time() . ')'); - $db = $this->getDb(); - if ($db->enableQueryCache) { - $db->enableQueryCache = false; - $rows = $query->createCommand($db)->queryAll(); - $db->enableQueryCache = true; + if ($this->db->enableQueryCache) { + $this->db->enableQueryCache = false; + $rows = $query->createCommand($this->db)->queryAll(); + $this->db->enableQueryCache = true; } else { - $rows = $query->createCommand($db)->queryAll(); + $rows = $query->createCommand($this->db)->queryAll(); } $results = array(); @@ -161,13 +156,13 @@ class DbCache extends Cache */ protected function setValue($key, $value, $expire) { - $command = $this->getDb()->createCommand(); - $command->update($this->cacheTableName, array( - 'expire' => $expire > 0 ? $expire + time() : 0, - 'data' => array($value, \PDO::PARAM_LOB), - ), array( - 'id' => $key, - ));; + $command = $this->db->createCommand() + ->update($this->cacheTable, array( + 'expire' => $expire > 0 ? $expire + time() : 0, + 'data' => array($value, \PDO::PARAM_LOB), + ), array( + 'id' => $key, + )); if ($command->execute()) { $this->gc(); @@ -196,14 +191,13 @@ class DbCache extends Cache $expire = 0; } - $command = $this->getDb()->createCommand(); - $command->insert($this->cacheTableName, array( - 'id' => $key, - 'expire' => $expire, - 'data' => array($value, \PDO::PARAM_LOB), - )); try { - $command->execute(); + $this->db->createCommand() + ->insert($this->cacheTable, array( + 'id' => $key, + 'expire' => $expire, + 'data' => array($value, \PDO::PARAM_LOB), + ))->execute(); return true; } catch (\Exception $e) { return false; @@ -218,8 +212,9 @@ class DbCache extends Cache */ protected function deleteValue($key) { - $command = $this->getDb()->createCommand(); - $command->delete($this->cacheTableName, array('id' => $key))->execute(); + $this->db->createCommand() + ->delete($this->cacheTable, array('id' => $key)) + ->execute(); return true; } @@ -231,8 +226,9 @@ class DbCache extends Cache public function gc($force = false) { if ($force || mt_rand(0, 1000000) < $this->gcProbability) { - $command = $this->getDb()->createCommand(); - $command->delete($this->cacheTableName, 'expire > 0 AND expire < ' . time())->execute(); + $this->db->createCommand() + ->delete($this->cacheTable, '[[expire]] > 0 AND [[expire]] < ' . time()) + ->execute(); } } @@ -243,8 +239,9 @@ class DbCache extends Cache */ protected function flushValues() { - $command = $this->getDb()->createCommand(); - $command->delete($this->cacheTableName)->execute(); + $this->db->createCommand() + ->delete($this->cacheTable) + ->execute(); return true; } } diff --git a/framework/caching/DbDependency.php b/framework/caching/DbDependency.php index 247109b..4308dc1 100644 --- a/framework/caching/DbDependency.php +++ b/framework/caching/DbDependency.php @@ -23,41 +23,28 @@ use yii\db\Connection; class DbDependency extends Dependency { /** - * @var string the ID of the [[Connection|DB connection]] application component. Defaults to 'db'. + * @var string the application component ID of the DB connection. */ - public $connectionID = 'db'; + public $db = 'db'; /** * @var string the SQL query whose result is used to determine if the dependency has been changed. - * Only the first row of the query result will be used. + * Only the first row of the query result will be used. This property must be always set, otherwise + * an exception would be raised. */ public $sql; /** * @var array the parameters (name=>value) to be bound to the SQL statement specified by [[sql]]. */ - public $params; + public $params = array(); /** - * Constructor. - * @param string $sql the SQL query whose result is used to determine if the dependency has been changed. - * @param array $params the parameters (name=>value) to be bound to the SQL statement specified by [[sql]]. - * @param array $config name-value pairs that will be used to initialize the object properties + * Initializes the database dependency object. */ - public function __construct($sql, $params = array(), $config = array()) + public function init() { - $this->sql = $sql; - $this->params = $params; - parent::__construct($config); - } - - /** - * PHP sleep magic method. - * This method ensures that the database instance is set null because it contains resource handles. - * @return array - */ - public function __sleep() - { - $this->_db = null; - return array_keys((array)$this); + if ($this->sql === null) { + throw new InvalidConfigException('DbDependency::sql must be set.'); + } } /** @@ -67,7 +54,11 @@ class DbDependency extends Dependency */ protected function generateDependencyData() { - $db = $this->getDb(); + $db = Yii::$app->getComponent($this->db); + if (!$db instanceof Connection) { + throw new InvalidConfigException("DbDependency::db must be the application component ID of a DB connection."); + } + if ($db->enableQueryCache) { // temporarily disable and re-enable query caching $db->enableQueryCache = false; @@ -78,36 +69,4 @@ class DbDependency extends Dependency } return $result; } - - /** - * @var Connection the DB connection instance - */ - private $_db; - - /** - * Returns the DB connection instance used for caching purpose. - * @return Connection the DB connection instance - * @throws InvalidConfigException if [[connectionID]] does not point to a valid application component. - */ - public function getDb() - { - if ($this->_db === null) { - $db = Yii::$app->getComponent($this->connectionID); - if ($db instanceof Connection) { - $this->_db = $db; - } else { - throw new InvalidConfigException("DbCacheDependency::connectionID must refer to the ID of a DB application component."); - } - } - return $this->_db; - } - - /** - * Sets the DB connection used by the cache component. - * @param Connection $value the DB connection instance - */ - public function setDb($value) - { - $this->_db = $value; - } } diff --git a/framework/caching/ExpressionDependency.php b/framework/caching/ExpressionDependency.php index e13c962..bf70291 100644 --- a/framework/caching/ExpressionDependency.php +++ b/framework/caching/ExpressionDependency.php @@ -22,18 +22,7 @@ class ExpressionDependency extends Dependency /** * @var string the PHP expression whose result is used to determine the dependency. */ - public $expression; - - /** - * Constructor. - * @param string $expression the PHP expression whose result is used to determine the dependency. - * @param array $config name-value pairs that will be used to initialize the object properties - */ - public function __construct($expression = 'true', $config = array()) - { - $this->expression = $expression; - parent::__construct($config); - } + public $expression = 'true'; /** * Generates the data needed to determine if dependency has been changed. diff --git a/framework/caching/FileDependency.php b/framework/caching/FileDependency.php index 3797dde..8d858ec 100644 --- a/framework/caching/FileDependency.php +++ b/framework/caching/FileDependency.php @@ -7,6 +7,8 @@ namespace yii\caching; +use yii\base\InvalidConfigException; + /** * FileDependency represents a dependency based on a file's last modification time. * @@ -20,19 +22,19 @@ class FileDependency extends Dependency { /** * @var string the name of the file whose last modification time is used to - * check if the dependency has been changed. + * check if the dependency has been changed. This property must be always set, + * otherwise an exception would be raised. */ public $fileName; /** - * Constructor. - * @param string $fileName name of the file whose change is to be checked. - * @param array $config name-value pairs that will be used to initialize the object properties + * Initializes the database dependency object. */ - public function __construct($fileName = null, $config = array()) + public function init() { - $this->fileName = $fileName; - parent::__construct($config); + if ($this->file === null) { + throw new InvalidConfigException('FileDependency::fileName must be set.'); + } } /** diff --git a/framework/console/Controller.php b/framework/console/Controller.php index b9b0523..c7c5642 100644 --- a/framework/console/Controller.php +++ b/framework/console/Controller.php @@ -24,7 +24,6 @@ use yii\base\InvalidRouteException; * ~~~ * * @author Qiang Xue - * * @since 2.0 */ class Controller extends \yii\base\Controller @@ -135,9 +134,13 @@ class Controller extends \yii\base\Controller /** * Returns the names of the global options for this command. - * A global option requires the existence of a global member variable whose + * A global option requires the existence of a public member variable whose * name is the option name. * Child classes may override this method to specify possible global options. + * + * Note that the values setting via global options are not available + * until [[beforeAction()]] is being called. + * * @return array the names of the global options for this command. */ public function globalOptions() diff --git a/framework/console/controllers/HelpController.php b/framework/console/controllers/HelpController.php index ea7e3d5..74c354b 100644 --- a/framework/console/controllers/HelpController.php +++ b/framework/console/controllers/HelpController.php @@ -9,9 +9,9 @@ namespace yii\console\controllers; use Yii; use yii\base\Application; -use yii\console\Exception; use yii\base\InlineAction; use yii\console\Controller; +use yii\console\Exception; use yii\console\Request; use yii\helpers\StringHelper; @@ -128,7 +128,7 @@ class HelpController extends Controller $files = scandir($module->getControllerPath()); foreach ($files as $file) { - if(strcmp(substr($file,-14),'Controller.php') === 0 && is_file($file)) { + if (strcmp(substr($file, -14), 'Controller.php') === 0) { $commands[] = $prefix . lcfirst(substr(basename($file), 0, -14)); } } diff --git a/framework/console/controllers/MigrateController.php b/framework/console/controllers/MigrateController.php index 7f9a18f..3f816f1 100644 --- a/framework/console/controllers/MigrateController.php +++ b/framework/console/controllers/MigrateController.php @@ -1,651 +1,630 @@ - - * @link http://www.yiiframework.com/ - * @copyright Copyright (c) 2008 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -namespace yii\console\controllers; - -use Yii; -use yii\console\Exception; -use yii\console\Controller; -use yii\db\Connection; -use yii\db\Query; -use yii\helpers\ArrayHelper; - -/** - * This command manages application migrations. - * - * A migration means a set of persistent changes to the application environment - * that is shared among different developers. For example, in an application - * backed by a database, a migration may refer to a set of changes to - * the database, such as creating a new table, adding a new table column. - * - * This command provides support for tracking the migration history, upgrading - * or downloading with migrations, and creating new migration skeletons. - * - * The migration history is stored in a database table named as [[migrationTable]]. - * The table will be automatically created the first this command is executed. - * You may also manually create it with the following structure: - * - * ~~~ - * CREATE TABLE tbl_migration ( - * version varchar(255) PRIMARY KEY, - * apply_time integer - * ) - * ~~~ - * - * Below are some common usages of this command: - * - * ~~~ - * # creates a new migration named 'create_user_table' - * yiic migrate/create create_user_table - * - * # applies ALL new migrations - * yiic migrate - * - * # reverts the last applied migration - * yiic migrate/down - * ~~~ - * - * @author Qiang Xue - * @since 2.0 - */ -class MigrateController extends Controller -{ - /** - * The name of the dummy migration that marks the beginning of the whole migration history. - */ - const BASE_MIGRATION = 'm000000_000000_base'; - - /** - * @var string the default command action. - */ - public $defaultAction = 'up'; - /** - * @var string the directory storing the migration classes. This can be either - * a path alias or a directory. - */ - public $migrationPath = '@app/migrations'; - /** - * @var string the name of the table for keeping applied migration information. - */ - public $migrationTable = 'tbl_migration'; - /** - * @var string the component ID that specifies the database connection for - * storing migration information. - */ - public $connectionID = 'db'; - /** - * @var string the template file for generating new migrations. - * This can be either a path alias (e.g. "@app/migrations/template.php") - * or a file path. - */ - public $templateFile = '@yii/views/migration.php'; - /** - * @var boolean whether to execute the migration in an interactive mode. - */ - public $interactive = true; - /** - * @var Connection the DB connection used for storing migration history. - * @see connectionID - */ - public $db; - - /** - * Returns the names of the global options for this command. - * @return array the names of the global options for this command. - */ - public function globalOptions() - { - return array('migrationPath', 'migrationTable', 'connectionID', 'templateFile', 'interactive'); - } - - /** - * This method is invoked right before an action is to be executed (after all possible filters.) - * It checks the existence of the [[migrationPath]]. - * @param \yii\base\Action $action the action to be executed. - * @return boolean whether the action should continue to be executed. - * @throws Exception if the migration directory does not exist. - */ - public function beforeAction($action) - { - if (parent::beforeAction($action)) { - $path = Yii::getAlias($this->migrationPath); - if (!is_dir($path)) { - throw new Exception("The migration directory \"{$this->migrationPath}\" does not exist."); - } - $this->migrationPath = $path; - - $this->db = Yii::$app->getComponent($this->connectionID); - if (!$this->db instanceof Connection) { - throw new Exception("Invalid DB connection \"{$this->connectionID}\"."); - } - - $version = Yii::getVersion(); - echo "Yii Migration Tool (based on Yii v{$version})\n\n"; - return true; - } else { - return false; - } - } - - /** - * Upgrades the application by applying new migrations. - * For example, - * - * ~~~ - * yiic migrate # apply all new migrations - * yiic migrate 3 # apply the first 3 new migrations - * ~~~ - * - * @param integer $limit the number of new migrations to be applied. If 0, it means - * applying all available new migrations. - */ - public function actionUp($limit = 0) - { - if (($migrations = $this->getNewMigrations()) === array()) { - echo "No new migration found. Your system is up-to-date.\n"; - Yii::$app->end(); - } - - $total = count($migrations); - $limit = (int)$limit; - if ($limit > 0) { - $migrations = array_slice($migrations, 0, $limit); - } - - $n = count($migrations); - if ($n === $total) { - echo "Total $n new " . ($n === 1 ? 'migration' : 'migrations') . " to be applied:\n"; - } else { - echo "Total $n out of $total new " . ($total === 1 ? 'migration' : 'migrations') . " to be applied:\n"; - } - - foreach ($migrations as $migration) { - echo " $migration\n"; - } - echo "\n"; - - if ($this->confirm('Apply the above ' . ($n === 1 ? 'migration' : 'migrations') . "?")) { - foreach ($migrations as $migration) { - if (!$this->migrateUp($migration)) { - echo "\nMigration failed. The rest of the migrations are canceled.\n"; - return; - } - } - echo "\nMigrated up successfully.\n"; - } - } - - /** - * Downgrades the application by reverting old migrations. - * For example, - * - * ~~~ - * yiic migrate/down # revert the last migration - * yiic migrate/down 3 # revert the last 3 migrations - * ~~~ - * - * @param integer $limit the number of migrations to be reverted. Defaults to 1, - * meaning the last applied migration will be reverted. - * @throws Exception if the number of the steps specified is less than 1. - */ - public function actionDown($limit = 1) - { - $limit = (int)$limit; - if ($limit < 1) { - throw new Exception("The step argument must be greater than 0."); - } - - if (($migrations = $this->getMigrationHistory($limit)) === array()) { - echo "No migration has been done before.\n"; - return; - } - $migrations = array_keys($migrations); - - $n = count($migrations); - echo "Total $n " . ($n === 1 ? 'migration' : 'migrations') . " to be reverted:\n"; - foreach ($migrations as $migration) { - echo " $migration\n"; - } - echo "\n"; - - if ($this->confirm('Revert the above ' . ($n === 1 ? 'migration' : 'migrations') . "?")) { - foreach ($migrations as $migration) { - if (!$this->migrateDown($migration)) { - echo "\nMigration failed. The rest of the migrations are canceled.\n"; - return; - } - } - echo "\nMigrated down successfully.\n"; - } - } - - /** - * Redoes the last few migrations. - * - * This command will first revert the specified migrations, and then apply - * them again. For example, - * - * ~~~ - * yiic migrate/redo # redo the last applied migration - * yiic migrate/redo 3 # redo the last 3 applied migrations - * ~~~ - * - * @param integer $limit the number of migrations to be redone. Defaults to 1, - * meaning the last applied migration will be redone. - * @throws Exception if the number of the steps specified is less than 1. - */ - public function actionRedo($limit = 1) - { - $limit = (int)$limit; - if ($limit < 1) { - throw new Exception("The step argument must be greater than 0."); - } - - if (($migrations = $this->getMigrationHistory($limit)) === array()) { - echo "No migration has been done before.\n"; - return; - } - $migrations = array_keys($migrations); - - $n = count($migrations); - echo "Total $n " . ($n === 1 ? 'migration' : 'migrations') . " to be redone:\n"; - foreach ($migrations as $migration) { - echo " $migration\n"; - } - echo "\n"; - - if ($this->confirm('Redo the above ' . ($n === 1 ? 'migration' : 'migrations') . "?")) { - foreach ($migrations as $migration) { - if (!$this->migrateDown($migration)) { - echo "\nMigration failed. The rest of the migrations are canceled.\n"; - return; - } - } - foreach (array_reverse($migrations) as $migration) { - if (!$this->migrateUp($migration)) { - echo "\nMigration failed. The rest of the migrations migrations are canceled.\n"; - return; - } - } - echo "\nMigration redone successfully.\n"; - } - } - - /** - * Upgrades or downgrades till the specified version of migration. - * - * This command will first revert the specified migrations, and then apply - * them again. For example, - * - * ~~~ - * yiic migrate/to 101129_185401 # using timestamp - * yiic migrate/to m101129_185401_create_user_table # using full name - * ~~~ - * - * @param string $version the version name that the application should be migrated to. - * This can be either the timestamp or the full name of the migration. - * @throws Exception if the version argument is invalid - */ - public function actionTo($version) - { - $originalVersion = $version; - if (preg_match('/^m?(\d{6}_\d{6})(_.*?)?$/', $version, $matches)) { - $version = 'm' . $matches[1]; - } else { - throw new Exception("The version argument must be either a timestamp (e.g. 101129_185401)\nor the full name of a migration (e.g. m101129_185401_create_user_table)."); - } - - // try migrate up - $migrations = $this->getNewMigrations(); - foreach ($migrations as $i => $migration) { - if (strpos($migration, $version . '_') === 0) { - $this->actionUp($i + 1); - return; - } - } - - // try migrate down - $migrations = array_keys($this->getMigrationHistory(-1)); - foreach ($migrations as $i => $migration) { - if (strpos($migration, $version . '_') === 0) { - if ($i === 0) { - echo "Already at '$originalVersion'. Nothing needs to be done.\n"; - } else { - $this->actionDown($i); - } - return; - } - } - - throw new Exception("Unable to find the version '$originalVersion'."); - } - - /** - * Modifies the migration history to the specified version. - * - * No actual migration will be performed. - * - * ~~~ - * yiic migrate/mark 101129_185401 # using timestamp - * yiic migrate/mark m101129_185401_create_user_table # using full name - * ~~~ - * - * @param string $version the version at which the migration history should be marked. - * This can be either the timestamp or the full name of the migration. - * @throws Exception if the version argument is invalid or the version cannot be found. - */ - public function actionMark($version) - { - $originalVersion = $version; - if (preg_match('/^m?(\d{6}_\d{6})(_.*?)?$/', $version, $matches)) { - $version = 'm' . $matches[1]; - } else { - throw new Exception("The version argument must be either a timestamp (e.g. 101129_185401)\nor the full name of a migration (e.g. m101129_185401_create_user_table)."); - } - - // try mark up - $migrations = $this->getNewMigrations(); - foreach ($migrations as $i => $migration) { - if (strpos($migration, $version . '_') === 0) { - if ($this->confirm("Set migration history at $originalVersion?")) { - $command = $this->db->createCommand(); - for ($j = 0; $j <= $i; ++$j) { - $command->insert($this->migrationTable, array( - 'version' => $migrations[$j], - 'apply_time' => time(), - ))->execute(); - } - echo "The migration history is set at $originalVersion.\nNo actual migration was performed.\n"; - } - return; - } - } - - // try mark down - $migrations = array_keys($this->getMigrationHistory(-1)); - foreach ($migrations as $i => $migration) { - if (strpos($migration, $version . '_') === 0) { - if ($i === 0) { - echo "Already at '$originalVersion'. Nothing needs to be done.\n"; - } else { - if ($this->confirm("Set migration history at $originalVersion?")) { - $command = $this->db->createCommand(); - for ($j = 0; $j < $i; ++$j) { - $command->delete($this->migrationTable, array( - 'version' => $migrations[$j], - ))->execute(); - } - echo "The migration history is set at $originalVersion.\nNo actual migration was performed.\n"; - } - } - return; - } - } - - throw new Exception("Unable to find the version '$originalVersion'."); - } - - /** - * Displays the migration history. - * - * This command will show the list of migrations that have been applied - * so far. For example, - * - * ~~~ - * yiic migrate/history # showing the last 10 migrations - * yiic migrate/history 5 # showing the last 5 migrations - * yiic migrate/history 0 # showing the whole history - * ~~~ - * - * @param integer $limit the maximum number of migrations to be displayed. - * If it is 0, the whole migration history will be displayed. - */ - public function actionHistory($limit = 10) - { - $limit = (int)$limit; - $migrations = $this->getMigrationHistory($limit); - if ($migrations === array()) { - echo "No migration has been done before.\n"; - } else { - $n = count($migrations); - if ($limit > 0) { - echo "Showing the last $n applied " . ($n === 1 ? 'migration' : 'migrations') . ":\n"; - } else { - echo "Total $n " . ($n === 1 ? 'migration has' : 'migrations have') . " been applied before:\n"; - } - foreach ($migrations as $version => $time) { - echo " (" . date('Y-m-d H:i:s', $time) . ') ' . $version . "\n"; - } - } - } - - /** - * Displays the un-applied new migrations. - * - * This command will show the new migrations that have not been applied. - * For example, - * - * ~~~ - * yiic migrate/new # showing the first 10 new migrations - * yiic migrate/new 5 # showing the first 5 new migrations - * yiic migrate/new 0 # showing all new migrations - * ~~~ - * - * @param integer $limit the maximum number of new migrations to be displayed. - * If it is 0, all available new migrations will be displayed. - */ - public function actionNew($limit = 10) - { - $limit = (int)$limit; - $migrations = $this->getNewMigrations(); - if ($migrations === array()) { - echo "No new migrations found. Your system is up-to-date.\n"; - } else { - $n = count($migrations); - if ($limit > 0 && $n > $limit) { - $migrations = array_slice($migrations, 0, $limit); - echo "Showing $limit out of $n new " . ($n === 1 ? 'migration' : 'migrations') . ":\n"; - } else { - echo "Found $n new " . ($n === 1 ? 'migration' : 'migrations') . ":\n"; - } - - foreach ($migrations as $migration) { - echo " " . $migration . "\n"; - } - } - } - - /** - * Creates a new migration. - * - * This command creates a new migration using the available migration template. - * After using this command, developers should modify the created migration - * skeleton by filling up the actual migration logic. - * - * ~~~ - * yiic migrate/create create_user_table - * ~~~ - * - * @param string $name the name of the new migration. This should only contain - * letters, digits and/or underscores. - * @throws Exception if the name argument is invalid. - */ - public function actionCreate($name) - { - if (!preg_match('/^\w+$/', $name)) { - throw new Exception("The migration name should contain letters, digits and/or underscore characters only."); - } - - $name = 'm' . gmdate('ymd_His') . '_' . $name; - $file = $this->migrationPath . DIRECTORY_SEPARATOR . $name . '.php'; - - if ($this->confirm("Create new migration '$file'?")) { - $content = $this->renderFile(Yii::getAlias($this->templateFile), array( - 'className' => $name, - )); - file_put_contents($file, $content); - echo "New migration created successfully.\n"; - } - } - - /** - * Upgrades with the specified migration class. - * @param string $class the migration class name - * @return boolean whether the migration is successful - */ - protected function migrateUp($class) - { - if ($class === self::BASE_MIGRATION) { - return true; - } - - echo "*** applying $class\n"; - $start = microtime(true); - $migration = $this->createMigration($class); - if ($migration->up() !== false) { - $this->db->createCommand()->insert($this->migrationTable, array( - 'version' => $class, - 'apply_time' => time(), - ))->execute(); - $time = microtime(true) - $start; - echo "*** applied $class (time: " . sprintf("%.3f", $time) . "s)\n\n"; - return true; - } else { - $time = microtime(true) - $start; - echo "*** failed to apply $class (time: " . sprintf("%.3f", $time) . "s)\n\n"; - return false; - } - } - - /** - * Downgrades with the specified migration class. - * @param string $class the migration class name - * @return boolean whether the migration is successful - */ - protected function migrateDown($class) - { - if ($class === self::BASE_MIGRATION) { - return true; - } - - echo "*** reverting $class\n"; - $start = microtime(true); - $migration = $this->createMigration($class); - if ($migration->down() !== false) { - $this->db->createCommand()->delete($this->migrationTable, array( - 'version' => $class, - ))->execute(); - $time = microtime(true) - $start; - echo "*** reverted $class (time: " . sprintf("%.3f", $time) . "s)\n\n"; - return true; - } else { - $time = microtime(true) - $start; - echo "*** failed to revert $class (time: " . sprintf("%.3f", $time) . "s)\n\n"; - return false; - } - } - - /** - * Creates a new migration instance. - * @param string $class the migration class name - * @return \yii\db\Migration the migration instance - */ - protected function createMigration($class) - { - $file = $this->migrationPath . DIRECTORY_SEPARATOR . $class . '.php'; - require_once($file); - return new $class(array( - 'db' => $this->db, - )); - } - - - /** - * @return Connection the database connection that is used to store the migration history. - * @throws Exception if the database connection ID is invalid. - */ - protected function getDb() - { - if ($this->db !== null) { - return $this->db; - } else { - $this->db = Yii::$app->getComponent($this->connectionID); - if ($this->db instanceof Connection) { - return $this->db; - } else { - throw new Exception("Invalid DB connection: {$this->connectionID}."); - } - } - } - - /** - * Returns the migration history. - * @param integer $limit the maximum number of records in the history to be returned - * @return array the migration history - */ - protected function getMigrationHistory($limit) - { - if ($this->db->schema->getTableSchema($this->migrationTable) === null) { - $this->createMigrationHistoryTable(); - } - $query = new Query; - $rows = $query->select(array('version', 'apply_time')) - ->from($this->migrationTable) - ->orderBy('version DESC') - ->limit($limit) - ->createCommand() - ->queryAll(); - $history = ArrayHelper::map($rows, 'version', 'apply_time'); - unset($history[self::BASE_MIGRATION]); - return $history; - } - - /** - * Creates the migration history table. - */ - protected function createMigrationHistoryTable() - { - echo 'Creating migration history table "' . $this->migrationTable . '"...'; - $this->db->createCommand()->createTable($this->migrationTable, array( - 'version' => 'varchar(255) NOT NULL PRIMARY KEY', - 'apply_time' => 'integer', - ))->execute(); - $this->db->createCommand()->insert($this->migrationTable, array( - 'version' => self::BASE_MIGRATION, - 'apply_time' => time(), - ))->execute(); - echo "done.\n"; - } - - /** - * Returns the migrations that are not applied. - * @return array list of new migrations - */ - protected function getNewMigrations() - { - $applied = array(); - foreach ($this->getMigrationHistory(-1) as $version => $time) { - $applied[substr($version, 1, 13)] = true; - } - - $migrations = array(); - $handle = opendir($this->migrationPath); - while (($file = readdir($handle)) !== false) { - if ($file === '.' || $file === '..') { - continue; - } - $path = $this->migrationPath . DIRECTORY_SEPARATOR . $file; - if (preg_match('/^(m(\d{6}_\d{6})_.*?)\.php$/', $file, $matches) && is_file($path) && !isset($applied[$matches[2]])) { - $migrations[] = $matches[1]; - } - } - closedir($handle); - sort($migrations); - return $migrations; - } -} + + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\console\controllers; + +use Yii; +use yii\console\Exception; +use yii\console\Controller; +use yii\db\Connection; +use yii\db\Query; +use yii\helpers\ArrayHelper; + +/** + * This command manages application migrations. + * + * A migration means a set of persistent changes to the application environment + * that is shared among different developers. For example, in an application + * backed by a database, a migration may refer to a set of changes to + * the database, such as creating a new table, adding a new table column. + * + * This command provides support for tracking the migration history, upgrading + * or downloading with migrations, and creating new migration skeletons. + * + * The migration history is stored in a database table named + * as [[migrationTable]]. The table will be automatically created the first time + * this command is executed, if it does not exist. You may also manually + * create it as follows: + * + * ~~~ + * CREATE TABLE tbl_migration ( + * version varchar(255) PRIMARY KEY, + * apply_time integer + * ) + * ~~~ + * + * Below are some common usages of this command: + * + * ~~~ + * # creates a new migration named 'create_user_table' + * yiic migrate/create create_user_table + * + * # applies ALL new migrations + * yiic migrate + * + * # reverts the last applied migration + * yiic migrate/down + * ~~~ + * + * @author Qiang Xue + * @since 2.0 + */ +class MigrateController extends Controller +{ + /** + * The name of the dummy migration that marks the beginning of the whole migration history. + */ + const BASE_MIGRATION = 'm000000_000000_base'; + + /** + * @var string the default command action. + */ + public $defaultAction = 'up'; + /** + * @var string the directory storing the migration classes. This can be either + * a path alias or a directory. + */ + public $migrationPath = '@app/migrations'; + /** + * @var string the name of the table for keeping applied migration information. + */ + public $migrationTable = 'tbl_migration'; + /** + * @var string the template file for generating new migrations. + * This can be either a path alias (e.g. "@app/migrations/template.php") + * or a file path. + */ + public $templateFile = '@yii/views/migration.php'; + /** + * @var boolean whether to execute the migration in an interactive mode. + */ + public $interactive = true; + /** + * @var Connection|string the DB connection object or the application + * component ID of the DB connection. + */ + public $db = 'db'; + + /** + * Returns the names of the global options for this command. + * @return array the names of the global options for this command. + */ + public function globalOptions() + { + return array('migrationPath', 'migrationTable', 'db', 'templateFile', 'interactive'); + } + + /** + * This method is invoked right before an action is to be executed (after all possible filters.) + * It checks the existence of the [[migrationPath]]. + * @param \yii\base\Action $action the action to be executed. + * @return boolean whether the action should continue to be executed. + * @throws Exception if the migration directory does not exist. + */ + public function beforeAction($action) + { + if (parent::beforeAction($action)) { + $path = Yii::getAlias($this->migrationPath); + if (!is_dir($path)) { + throw new Exception("The migration directory \"{$this->migrationPath}\" does not exist."); + } + $this->migrationPath = $path; + + if (is_string($this->db)) { + $this->db = Yii::$app->getComponent($this->db); + } + if (!$this->db instanceof Connection) { + throw new Exception("The 'db' option must refer to the application component ID of a DB connection."); + } + + $version = Yii::getVersion(); + echo "Yii Migration Tool (based on Yii v{$version})\n\n"; + return true; + } else { + return false; + } + } + + /** + * Upgrades the application by applying new migrations. + * For example, + * + * ~~~ + * yiic migrate # apply all new migrations + * yiic migrate 3 # apply the first 3 new migrations + * ~~~ + * + * @param integer $limit the number of new migrations to be applied. If 0, it means + * applying all available new migrations. + */ + public function actionUp($limit = 0) + { + if (($migrations = $this->getNewMigrations()) === array()) { + echo "No new migration found. Your system is up-to-date.\n"; + Yii::$app->end(); + } + + $total = count($migrations); + $limit = (int)$limit; + if ($limit > 0) { + $migrations = array_slice($migrations, 0, $limit); + } + + $n = count($migrations); + if ($n === $total) { + echo "Total $n new " . ($n === 1 ? 'migration' : 'migrations') . " to be applied:\n"; + } else { + echo "Total $n out of $total new " . ($total === 1 ? 'migration' : 'migrations') . " to be applied:\n"; + } + + foreach ($migrations as $migration) { + echo " $migration\n"; + } + echo "\n"; + + if ($this->confirm('Apply the above ' . ($n === 1 ? 'migration' : 'migrations') . "?")) { + foreach ($migrations as $migration) { + if (!$this->migrateUp($migration)) { + echo "\nMigration failed. The rest of the migrations are canceled.\n"; + return; + } + } + echo "\nMigrated up successfully.\n"; + } + } + + /** + * Downgrades the application by reverting old migrations. + * For example, + * + * ~~~ + * yiic migrate/down # revert the last migration + * yiic migrate/down 3 # revert the last 3 migrations + * ~~~ + * + * @param integer $limit the number of migrations to be reverted. Defaults to 1, + * meaning the last applied migration will be reverted. + * @throws Exception if the number of the steps specified is less than 1. + */ + public function actionDown($limit = 1) + { + $limit = (int)$limit; + if ($limit < 1) { + throw new Exception("The step argument must be greater than 0."); + } + + if (($migrations = $this->getMigrationHistory($limit)) === array()) { + echo "No migration has been done before.\n"; + return; + } + $migrations = array_keys($migrations); + + $n = count($migrations); + echo "Total $n " . ($n === 1 ? 'migration' : 'migrations') . " to be reverted:\n"; + foreach ($migrations as $migration) { + echo " $migration\n"; + } + echo "\n"; + + if ($this->confirm('Revert the above ' . ($n === 1 ? 'migration' : 'migrations') . "?")) { + foreach ($migrations as $migration) { + if (!$this->migrateDown($migration)) { + echo "\nMigration failed. The rest of the migrations are canceled.\n"; + return; + } + } + echo "\nMigrated down successfully.\n"; + } + } + + /** + * Redoes the last few migrations. + * + * This command will first revert the specified migrations, and then apply + * them again. For example, + * + * ~~~ + * yiic migrate/redo # redo the last applied migration + * yiic migrate/redo 3 # redo the last 3 applied migrations + * ~~~ + * + * @param integer $limit the number of migrations to be redone. Defaults to 1, + * meaning the last applied migration will be redone. + * @throws Exception if the number of the steps specified is less than 1. + */ + public function actionRedo($limit = 1) + { + $limit = (int)$limit; + if ($limit < 1) { + throw new Exception("The step argument must be greater than 0."); + } + + if (($migrations = $this->getMigrationHistory($limit)) === array()) { + echo "No migration has been done before.\n"; + return; + } + $migrations = array_keys($migrations); + + $n = count($migrations); + echo "Total $n " . ($n === 1 ? 'migration' : 'migrations') . " to be redone:\n"; + foreach ($migrations as $migration) { + echo " $migration\n"; + } + echo "\n"; + + if ($this->confirm('Redo the above ' . ($n === 1 ? 'migration' : 'migrations') . "?")) { + foreach ($migrations as $migration) { + if (!$this->migrateDown($migration)) { + echo "\nMigration failed. The rest of the migrations are canceled.\n"; + return; + } + } + foreach (array_reverse($migrations) as $migration) { + if (!$this->migrateUp($migration)) { + echo "\nMigration failed. The rest of the migrations migrations are canceled.\n"; + return; + } + } + echo "\nMigration redone successfully.\n"; + } + } + + /** + * Upgrades or downgrades till the specified version. + * + * This command will first revert the specified migrations, and then apply + * them again. For example, + * + * ~~~ + * yiic migrate/to 101129_185401 # using timestamp + * yiic migrate/to m101129_185401_create_user_table # using full name + * ~~~ + * + * @param string $version the version name that the application should be migrated to. + * This can be either the timestamp or the full name of the migration. + * @throws Exception if the version argument is invalid + */ + public function actionTo($version) + { + $originalVersion = $version; + if (preg_match('/^m?(\d{6}_\d{6})(_.*?)?$/', $version, $matches)) { + $version = 'm' . $matches[1]; + } else { + throw new Exception("The version argument must be either a timestamp (e.g. 101129_185401)\nor the full name of a migration (e.g. m101129_185401_create_user_table)."); + } + + // try migrate up + $migrations = $this->getNewMigrations(); + foreach ($migrations as $i => $migration) { + if (strpos($migration, $version . '_') === 0) { + $this->actionUp($i + 1); + return; + } + } + + // try migrate down + $migrations = array_keys($this->getMigrationHistory(-1)); + foreach ($migrations as $i => $migration) { + if (strpos($migration, $version . '_') === 0) { + if ($i === 0) { + echo "Already at '$originalVersion'. Nothing needs to be done.\n"; + } else { + $this->actionDown($i); + } + return; + } + } + + throw new Exception("Unable to find the version '$originalVersion'."); + } + + /** + * Modifies the migration history to the specified version. + * + * No actual migration will be performed. + * + * ~~~ + * yiic migrate/mark 101129_185401 # using timestamp + * yiic migrate/mark m101129_185401_create_user_table # using full name + * ~~~ + * + * @param string $version the version at which the migration history should be marked. + * This can be either the timestamp or the full name of the migration. + * @throws Exception if the version argument is invalid or the version cannot be found. + */ + public function actionMark($version) + { + $originalVersion = $version; + if (preg_match('/^m?(\d{6}_\d{6})(_.*?)?$/', $version, $matches)) { + $version = 'm' . $matches[1]; + } else { + throw new Exception("The version argument must be either a timestamp (e.g. 101129_185401)\nor the full name of a migration (e.g. m101129_185401_create_user_table)."); + } + + // try mark up + $migrations = $this->getNewMigrations(); + foreach ($migrations as $i => $migration) { + if (strpos($migration, $version . '_') === 0) { + if ($this->confirm("Set migration history at $originalVersion?")) { + $command = $this->db->createCommand(); + for ($j = 0; $j <= $i; ++$j) { + $command->insert($this->migrationTable, array( + 'version' => $migrations[$j], + 'apply_time' => time(), + ))->execute(); + } + echo "The migration history is set at $originalVersion.\nNo actual migration was performed.\n"; + } + return; + } + } + + // try mark down + $migrations = array_keys($this->getMigrationHistory(-1)); + foreach ($migrations as $i => $migration) { + if (strpos($migration, $version . '_') === 0) { + if ($i === 0) { + echo "Already at '$originalVersion'. Nothing needs to be done.\n"; + } else { + if ($this->confirm("Set migration history at $originalVersion?")) { + $command = $this->db->createCommand(); + for ($j = 0; $j < $i; ++$j) { + $command->delete($this->migrationTable, array( + 'version' => $migrations[$j], + ))->execute(); + } + echo "The migration history is set at $originalVersion.\nNo actual migration was performed.\n"; + } + } + return; + } + } + + throw new Exception("Unable to find the version '$originalVersion'."); + } + + /** + * Displays the migration history. + * + * This command will show the list of migrations that have been applied + * so far. For example, + * + * ~~~ + * yiic migrate/history # showing the last 10 migrations + * yiic migrate/history 5 # showing the last 5 migrations + * yiic migrate/history 0 # showing the whole history + * ~~~ + * + * @param integer $limit the maximum number of migrations to be displayed. + * If it is 0, the whole migration history will be displayed. + */ + public function actionHistory($limit = 10) + { + $limit = (int)$limit; + $migrations = $this->getMigrationHistory($limit); + if ($migrations === array()) { + echo "No migration has been done before.\n"; + } else { + $n = count($migrations); + if ($limit > 0) { + echo "Showing the last $n applied " . ($n === 1 ? 'migration' : 'migrations') . ":\n"; + } else { + echo "Total $n " . ($n === 1 ? 'migration has' : 'migrations have') . " been applied before:\n"; + } + foreach ($migrations as $version => $time) { + echo " (" . date('Y-m-d H:i:s', $time) . ') ' . $version . "\n"; + } + } + } + + /** + * Displays the un-applied new migrations. + * + * This command will show the new migrations that have not been applied. + * For example, + * + * ~~~ + * yiic migrate/new # showing the first 10 new migrations + * yiic migrate/new 5 # showing the first 5 new migrations + * yiic migrate/new 0 # showing all new migrations + * ~~~ + * + * @param integer $limit the maximum number of new migrations to be displayed. + * If it is 0, all available new migrations will be displayed. + */ + public function actionNew($limit = 10) + { + $limit = (int)$limit; + $migrations = $this->getNewMigrations(); + if ($migrations === array()) { + echo "No new migrations found. Your system is up-to-date.\n"; + } else { + $n = count($migrations); + if ($limit > 0 && $n > $limit) { + $migrations = array_slice($migrations, 0, $limit); + echo "Showing $limit out of $n new " . ($n === 1 ? 'migration' : 'migrations') . ":\n"; + } else { + echo "Found $n new " . ($n === 1 ? 'migration' : 'migrations') . ":\n"; + } + + foreach ($migrations as $migration) { + echo " " . $migration . "\n"; + } + } + } + + /** + * Creates a new migration. + * + * This command creates a new migration using the available migration template. + * After using this command, developers should modify the created migration + * skeleton by filling up the actual migration logic. + * + * ~~~ + * yiic migrate/create create_user_table + * ~~~ + * + * @param string $name the name of the new migration. This should only contain + * letters, digits and/or underscores. + * @throws Exception if the name argument is invalid. + */ + public function actionCreate($name) + { + if (!preg_match('/^\w+$/', $name)) { + throw new Exception("The migration name should contain letters, digits and/or underscore characters only."); + } + + $name = 'm' . gmdate('ymd_His') . '_' . $name; + $file = $this->migrationPath . DIRECTORY_SEPARATOR . $name . '.php'; + + if ($this->confirm("Create new migration '$file'?")) { + $content = $this->renderFile(Yii::getAlias($this->templateFile), array( + 'className' => $name, + )); + file_put_contents($file, $content); + echo "New migration created successfully.\n"; + } + } + + /** + * Upgrades with the specified migration class. + * @param string $class the migration class name + * @return boolean whether the migration is successful + */ + protected function migrateUp($class) + { + if ($class === self::BASE_MIGRATION) { + return true; + } + + echo "*** applying $class\n"; + $start = microtime(true); + $migration = $this->createMigration($class); + if ($migration->up() !== false) { + $this->db->createCommand()->insert($this->migrationTable, array( + 'version' => $class, + 'apply_time' => time(), + ))->execute(); + $time = microtime(true) - $start; + echo "*** applied $class (time: " . sprintf("%.3f", $time) . "s)\n\n"; + return true; + } else { + $time = microtime(true) - $start; + echo "*** failed to apply $class (time: " . sprintf("%.3f", $time) . "s)\n\n"; + return false; + } + } + + /** + * Downgrades with the specified migration class. + * @param string $class the migration class name + * @return boolean whether the migration is successful + */ + protected function migrateDown($class) + { + if ($class === self::BASE_MIGRATION) { + return true; + } + + echo "*** reverting $class\n"; + $start = microtime(true); + $migration = $this->createMigration($class); + if ($migration->down() !== false) { + $this->db->createCommand()->delete($this->migrationTable, array( + 'version' => $class, + ))->execute(); + $time = microtime(true) - $start; + echo "*** reverted $class (time: " . sprintf("%.3f", $time) . "s)\n\n"; + return true; + } else { + $time = microtime(true) - $start; + echo "*** failed to revert $class (time: " . sprintf("%.3f", $time) . "s)\n\n"; + return false; + } + } + + /** + * Creates a new migration instance. + * @param string $class the migration class name + * @return \yii\db\Migration the migration instance + */ + protected function createMigration($class) + { + $file = $this->migrationPath . DIRECTORY_SEPARATOR . $class . '.php'; + require_once($file); + return new $class(array( + 'db' => $this->db, + )); + } + + /** + * Returns the migration history. + * @param integer $limit the maximum number of records in the history to be returned + * @return array the migration history + */ + protected function getMigrationHistory($limit) + { + if ($this->db->schema->getTableSchema($this->migrationTable) === null) { + $this->createMigrationHistoryTable(); + } + $query = new Query; + $rows = $query->select(array('version', 'apply_time')) + ->from($this->migrationTable) + ->orderBy('version DESC') + ->limit($limit) + ->createCommand() + ->queryAll(); + $history = ArrayHelper::map($rows, 'version', 'apply_time'); + unset($history[self::BASE_MIGRATION]); + return $history; + } + + /** + * Creates the migration history table. + */ + protected function createMigrationHistoryTable() + { + echo 'Creating migration history table "' . $this->migrationTable . '"...'; + $this->db->createCommand()->createTable($this->migrationTable, array( + 'version' => 'varchar(255) NOT NULL PRIMARY KEY', + 'apply_time' => 'integer', + ))->execute(); + $this->db->createCommand()->insert($this->migrationTable, array( + 'version' => self::BASE_MIGRATION, + 'apply_time' => time(), + ))->execute(); + echo "done.\n"; + } + + /** + * Returns the migrations that are not applied. + * @return array list of new migrations + */ + protected function getNewMigrations() + { + $applied = array(); + foreach ($this->getMigrationHistory(-1) as $version => $time) { + $applied[substr($version, 1, 13)] = true; + } + + $migrations = array(); + $handle = opendir($this->migrationPath); + while (($file = readdir($handle)) !== false) { + if ($file === '.' || $file === '..') { + continue; + } + $path = $this->migrationPath . DIRECTORY_SEPARATOR . $file; + if (preg_match('/^(m(\d{6}_\d{6})_.*?)\.php$/', $file, $matches) && is_file($path) && !isset($applied[$matches[2]])) { + $migrations[] = $matches[1]; + } + } + closedir($handle); + sort($migrations); + return $migrations; + } +} diff --git a/framework/db/ActiveRecord.php b/framework/db/ActiveRecord.php index 0c15121..45c53fb 100644 --- a/framework/db/ActiveRecord.php +++ b/framework/db/ActiveRecord.php @@ -191,15 +191,12 @@ class ActiveRecord extends Model */ public static function updateAllCounters($counters, $condition = '', $params = array()) { - $db = static::getDb(); $n = 0; foreach ($counters as $name => $value) { - $quotedName = $db->quoteColumnName($name); - $counters[$name] = new Expression("$quotedName+:bp{$n}"); - $params[":bp{$n}"] = $value; + $counters[$name] = new Expression("[[$name]]+:bp{$n}", array(":bp{$n}" => $value)); $n++; } - $command = $db->createCommand(); + $command = static::getDb()->createCommand(); $command->update(static::tableName(), $counters, $condition, $params); return $command->execute(); } @@ -280,6 +277,34 @@ class ActiveRecord extends Model } /** + * Returns the name of the column that stores the lock version for implementing optimistic locking. + * + * Optimistic locking allows multiple users to access the same record for edits and avoids + * potential conflicts. In case when a user attempts to save the record upon some staled data + * (because another user has modified the data), a [[StaleObjectException]] exception will be thrown, + * and the update or deletion is skipped. + * + * Optimized locking is only supported by [[update()]] and [[delete()]]. + * + * To use optimized locking: + * + * 1. Create a column to store the version number of each row. The column type should be `BIGINT DEFAULT 0`. + * Override this method to return the name of this column. + * 2. In the Web form that collects the user input, add a hidden field that stores + * the lock version of the recording being updated. + * 3. In the controller action that does the data updating, try to catch the [[StaleObjectException]] + * and implement necessary business logic (e.g. merging the changes, prompting stated data) + * to resolve the conflict. + * + * @return string the column name that stores the lock version of a table row. + * If null is returned (default implemented), optimistic locking will not be supported. + */ + public function optimisticLock() + { + return null; + } + + /** * PHP getter magic method. * This method is overridden so that attributes and related objects can be accessed like properties. * @param string $name property name @@ -530,8 +555,8 @@ class ActiveRecord extends Model */ public function isAttributeChanged($name) { - if (isset($this->_attribute[$name], $this->_oldAttributes[$name])) { - return $this->_attribute[$name] !== $this->_oldAttributes[$name]; + if (isset($this->_attributes[$name], $this->_oldAttributes[$name])) { + return $this->_attributes[$name] !== $this->_oldAttributes[$name]; } else { return isset($this->_attributes[$name]) || isset($this->_oldAttributes); } @@ -590,7 +615,11 @@ class ActiveRecord extends Model */ public function save($runValidation = true, $attributes = null) { - return $this->getIsNewRecord() ? $this->insert($runValidation, $attributes) : $this->update($runValidation, $attributes); + if ($this->getIsNewRecord()) { + return $this->insert($runValidation, $attributes); + } else { + return $this->update($runValidation, $attributes) !== false; + } } /** @@ -692,11 +721,26 @@ class ActiveRecord extends Model * $customer->update(); * ~~~ * + * Note that it is possible the update does not affect any row in the table. + * In this case, this method will return 0. For this reason, you should use the following + * code to check if update() is successful or not: + * + * ~~~ + * if ($this->update() !== false) { + * // update successful + * } else { + * // update failed + * } + * ~~~ + * * @param boolean $runValidation whether to perform validation before saving the record. * If the validation fails, the record will not be inserted into the database. * @param array $attributes list of attributes that need to be saved. Defaults to null, * meaning all attributes that are loaded from DB will be saved. - * @return boolean whether the attributes are valid and the record is updated successfully. + * @return integer|boolean the number of rows affected, or false if validation fails + * or [[beforeSave()]] stops the updating process. + * @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data + * being updated is outdated. */ public function update($runValidation = true, $attributes = null) { @@ -706,15 +750,31 @@ class ActiveRecord extends Model if ($this->beforeSave(false)) { $values = $this->getDirtyAttributes($attributes); if ($values !== array()) { + $condition = $this->getOldPrimaryKey(true); + $lock = $this->optimisticLock(); + if ($lock !== null) { + if (!isset($values[$lock])) { + $values[$lock] = $this->$lock + 1; + } + $condition[$lock] = $this->$lock; + } // We do not check the return value of updateAll() because it's possible // that the UPDATE statement doesn't change anything and thus returns 0. - $this->updateAll($values, $this->getOldPrimaryKey(true)); + $rows = $this->updateAll($values, $condition); + + if ($lock !== null && !$rows) { + throw new StaleObjectException('The object being updated is outdated.'); + } + foreach ($values as $name => $value) { $this->_oldAttributes[$name] = $this->_attributes[$name]; } + $this->afterSave(false); + return $rows; + } else { + return 0; } - return true; } else { return false; } @@ -763,17 +823,28 @@ class ActiveRecord extends Model * In the above step 1 and 3, events named [[EVENT_BEFORE_DELETE]] and [[EVENT_AFTER_DELETE]] * will be raised by the corresponding methods. * - * @return boolean whether the deletion is successful. + * @return integer|boolean the number of rows deleted, or false if the deletion is unsuccessful for some reason. + * Note that it is possible the number of rows deleted is 0, even though the deletion execution is successful. + * @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data + * being deleted is outdated. */ public function delete() { if ($this->beforeDelete()) { // we do not check the return value of deleteAll() because it's possible // the record is already deleted in the database and thus the method will return 0 - $this->deleteAll($this->getPrimaryKey(true)); + $condition = $this->getOldPrimaryKey(true); + $lock = $this->optimisticLock(); + if ($lock !== null) { + $condition[$lock] = $this->$lock; + } + $rows = $this->deleteAll($condition); + if ($lock !== null && !$rows) { + throw new StaleObjectException('The object being deleted is outdated.'); + } $this->_oldAttributes = null; $this->afterDelete(); - return true; + return $rows; } else { return false; } @@ -847,7 +918,7 @@ class ActiveRecord extends Model */ public function beforeSave($insert) { - $event = new ModelEvent($this); + $event = new ModelEvent; $this->trigger($insert ? self::EVENT_BEFORE_INSERT : self::EVENT_BEFORE_UPDATE, $event); return $event->isValid; } @@ -887,7 +958,7 @@ class ActiveRecord extends Model */ public function beforeDelete() { - $event = new ModelEvent($this); + $event = new ModelEvent; $this->trigger(self::EVENT_BEFORE_DELETE, $event); return $event->isValid; } diff --git a/framework/db/Command.php b/framework/db/Command.php index c2b2e05..dc6c972 100644 --- a/framework/db/Command.php +++ b/framework/db/Command.php @@ -7,7 +7,9 @@ namespace yii\db; +use Yii; use yii\base\NotSupportedException; +use yii\caching\Cache; /** * Command represents a SQL statement to be executed against a database. @@ -82,39 +84,51 @@ class Command extends \yii\base\Component /** * Specifies the SQL statement to be executed. - * Any previous execution will be terminated or cancelled. + * The previous SQL execution (if any) will be cancelled, and [[params]] will be cleared as well. * @param string $sql the SQL statement to be set. * @return Command this command instance */ public function setSql($sql) { if ($sql !== $this->_sql) { - if ($this->db->enableAutoQuoting && $sql != '') { - $sql = $this->expandSql($sql); - } $this->cancel(); - $this->_sql = $sql; + $this->_sql = $this->db->quoteSql($sql); $this->_params = array(); } return $this; } /** - * Expands a SQL statement by quoting table and column names and replacing table prefixes. - * @param string $sql the SQL to be expanded - * @return string the expanded SQL + * Returns the raw SQL by inserting parameter values into the corresponding placeholders in [[sql]]. + * Note that the return value of this method should mainly be used for logging purpose. + * It is likely that this method returns an invalid SQL due to improper replacement of parameter placeholders. + * @return string the raw SQL */ - protected function expandSql($sql) + public function getRawSql() { - $db = $this->db; - return preg_replace_callback('/(\\{\\{(.*?)\\}\\}|\\[\\[(.*?)\\]\\])/', function($matches) use($db) { - if (isset($matches[3])) { - return $db->quoteColumnName($matches[3]); + if ($this->_params === array()) { + return $this->_sql; + } else { + $params = array(); + foreach ($this->_params as $name => $value) { + if (is_string($value)) { + $params[$name] = $this->db->quoteValue($value); + } elseif ($value === null) { + $params[$name] = 'NULL'; + } else { + $params[$name] = $value; + } + } + if (isset($params[1])) { + $sql = ''; + foreach (explode('?', $this->_sql) as $i => $part) { + $sql .= (isset($params[$i]) ? $params[$i] : '') . $part; + } + return $sql; } else { - $name = str_replace('%', $db->tablePrefix, $matches[2]); - return $db->quoteTableName($name); + return strtr($this->_sql, $params); } - }, $sql); + } } /** @@ -132,7 +146,7 @@ class Command extends \yii\base\Component try { $this->pdoStatement = $this->db->pdo->prepare($sql); } catch (\Exception $e) { - \Yii::error($e->getMessage() . "\nFailed to prepare SQL: $sql", __CLASS__); + Yii::error($e->getMessage() . "\nFailed to prepare SQL: $sql", __METHOD__); $errorInfo = $e instanceof \PDOException ? $e->errorInfo : null; throw new Exception($e->getMessage(), $errorInfo, (int)$e->getCode()); } @@ -241,6 +255,7 @@ class Command extends \yii\base\Component 'boolean' => \PDO::PARAM_BOOL, 'integer' => \PDO::PARAM_INT, 'string' => \PDO::PARAM_STR, + 'resource' => \PDO::PARAM_LOB, 'NULL' => \PDO::PARAM_NULL, ); $type = gettype($data); @@ -258,21 +273,18 @@ class Command extends \yii\base\Component { $sql = $this->getSql(); - if ($this->_params === array()) { - $paramLog = ''; - } else { - $paramLog = "\nParameters: " . var_export($this->_params, true); - } + $rawSql = $this->getRawSql(); - \Yii::trace("Executing SQL: {$sql}{$paramLog}", __CLASS__); + Yii::trace("Executing SQL: $rawSql", __METHOD__); if ($sql == '') { return 0; } try { + $token = "SQL: $sql"; if ($this->db->enableProfiling) { - \Yii::beginProfile(__METHOD__ . "($sql)", __CLASS__); + Yii::beginProfile($token, __METHOD__); } $this->prepare(); @@ -280,16 +292,16 @@ class Command extends \yii\base\Component $n = $this->pdoStatement->rowCount(); if ($this->db->enableProfiling) { - \Yii::endProfile(__METHOD__ . "($sql)", __CLASS__); + Yii::endProfile($token, __METHOD__); } return $n; } catch (\Exception $e) { if ($this->db->enableProfiling) { - \Yii::endProfile(__METHOD__ . "($sql)", __CLASS__); + Yii::endProfile($token, __METHOD__); } $message = $e->getMessage(); - \Yii::error("$message\nFailed to execute SQL: {$sql}{$paramLog}", __CLASS__); + Yii::error("$message\nFailed to execute SQL: $rawSql", __METHOD__); $errorInfo = $e instanceof \PDOException ? $e->errorInfo : null; throw new Exception($message, $errorInfo, (int)$e->getCode()); @@ -375,36 +387,32 @@ class Command extends \yii\base\Component { $db = $this->db; $sql = $this->getSql(); - if ($this->_params === array()) { - $paramLog = ''; - } else { - $paramLog = "\nParameters: " . var_export($this->_params, true); - } + $rawSql = $this->getRawSql(); - \Yii::trace("Querying SQL: {$sql}{$paramLog}", __CLASS__); + Yii::trace("Querying SQL: $rawSql", __METHOD__); /** @var $cache \yii\caching\Cache */ if ($db->enableQueryCache && $method !== '') { - $cache = \Yii::$app->getComponent($db->queryCacheID); + $cache = is_string($db->queryCache) ? Yii::$app->getComponent($db->queryCache) : $db->queryCache; } - if (isset($cache)) { + if (isset($cache) && $cache instanceof Cache) { $cacheKey = $cache->buildKey(array( __CLASS__, $db->dsn, $db->username, - $sql, - $paramLog, + $rawSql, )); if (($result = $cache->get($cacheKey)) !== false) { - \Yii::trace('Query result found in cache', __CLASS__); + Yii::trace('Query result served from cache', __METHOD__); return $result; } } try { + $token = "SQL: $sql"; if ($db->enableProfiling) { - \Yii::beginProfile(__METHOD__ . "($sql)", __CLASS__); + Yii::beginProfile($token, __METHOD__); } $this->prepare(); @@ -421,21 +429,21 @@ class Command extends \yii\base\Component } if ($db->enableProfiling) { - \Yii::endProfile(__METHOD__ . "($sql)", __CLASS__); + Yii::endProfile($token, __METHOD__); } - if (isset($cache, $cacheKey)) { + if (isset($cache, $cacheKey) && $cache instanceof Cache) { $cache->set($cacheKey, $result, $db->queryCacheDuration, $db->queryCacheDependency); - \Yii::trace('Saved query result in cache', __CLASS__); + Yii::trace('Saved query result in cache', __METHOD__); } return $result; } catch (\Exception $e) { if ($db->enableProfiling) { - \Yii::endProfile(__METHOD__ . "($sql)", __CLASS__); + Yii::endProfile($token, __METHOD__); } $message = $e->getMessage(); - \Yii::error("$message\nCommand::$method() failed: {$sql}{$paramLog}", __CLASS__); + Yii::error("$message\nCommand::$method() failed: $rawSql", __METHOD__); $errorInfo = $e instanceof \PDOException ? $e->errorInfo : null; throw new Exception($message, $errorInfo, (int)$e->getCode()); } @@ -539,7 +547,7 @@ class Command extends \yii\base\Component */ public function delete($table, $condition = '', $params = array()) { - $sql = $this->db->getQueryBuilder()->delete($table, $condition); + $sql = $this->db->getQueryBuilder()->delete($table, $condition, $params); return $this->setSql($sql)->bindValues($params); } diff --git a/framework/db/Connection.php b/framework/db/Connection.php index 40164a3..695034a 100644 --- a/framework/db/Connection.php +++ b/framework/db/Connection.php @@ -10,6 +10,7 @@ namespace yii\db; use yii\base\Component; use yii\base\InvalidConfigException; use yii\base\NotSupportedException; +use yii\caching\Cache; /** * Connection represents a connection to a database via [PDO](http://www.php.net/manual/en/ref.pdo.php). @@ -136,10 +137,10 @@ class Connection extends Component /** * @var boolean whether to enable schema caching. * Note that in order to enable truly schema caching, a valid cache component as specified - * by [[schemaCacheID]] must be enabled and [[enableSchemaCache]] must be set true. + * by [[schemaCache]] must be enabled and [[enableSchemaCache]] must be set true. * @see schemaCacheDuration * @see schemaCacheExclude - * @see schemaCacheID + * @see schemaCache */ public $enableSchemaCache = false; /** @@ -155,20 +156,20 @@ class Connection extends Component */ public $schemaCacheExclude = array(); /** - * @var string the ID of the cache application component that is used to cache the table metadata. - * Defaults to 'cache'. + * @var Cache|string the cache object or the ID of the cache application component that + * is used to cache the table metadata. * @see enableSchemaCache */ - public $schemaCacheID = 'cache'; + public $schemaCache = 'cache'; /** * @var boolean whether to enable query caching. * Note that in order to enable query caching, a valid cache component as specified - * by [[queryCacheID]] must be enabled and [[enableQueryCache]] must be set true. + * by [[queryCache]] must be enabled and [[enableQueryCache]] must be set true. * * Methods [[beginCache()]] and [[endCache()]] can be used as shortcuts to turn on * and off query caching on the fly. * @see queryCacheDuration - * @see queryCacheID + * @see queryCache * @see queryCacheDependency * @see beginCache() * @see endCache() @@ -176,7 +177,7 @@ class Connection extends Component public $enableQueryCache = false; /** * @var integer number of seconds that query results can remain valid in cache. - * Defaults to 3600, meaning one hour. + * Defaults to 3600, meaning 3600 seconds, or one hour. * Use 0 to indicate that the cached data will never expire. * @see enableQueryCache */ @@ -188,11 +189,11 @@ class Connection extends Component */ public $queryCacheDependency; /** - * @var string the ID of the cache application component that is used for query caching. - * Defaults to 'cache'. + * @var Cache|string the cache object or the ID of the cache application component + * that is used for query caching. * @see enableQueryCache */ - public $queryCacheID = 'cache'; + public $queryCache = 'cache'; /** * @var string the charset used for database connection. The property is only used * for MySQL and PostgreSQL databases. Defaults to null, meaning using default charset @@ -222,21 +223,10 @@ class Connection extends Component * @var string the common prefix or suffix for table names. If a table name is given * as `{{%TableName}}`, then the percentage character `%` will be replaced with this * property value. For example, `{{%post}}` becomes `{{tbl_post}}` if this property is - * set as `"tbl_"`. Note that this property is only effective when [[enableAutoQuoting]] - * is true. - * @see enableAutoQuoting + * set as `"tbl_"`. */ public $tablePrefix; /** - * @var boolean whether to enable automatic quoting of table names and column names. - * Defaults to true. When this property is true, any token enclosed within double curly brackets - * (e.g. `{{post}}`) in a SQL statement will be treated as a table name and will be quoted - * accordingly when the SQL statement is executed; and any token enclosed within double square - * brackets (e.g. `[[name]]`) will be treated as a column name and quoted accordingly. - * @see tablePrefix - */ - public $enableAutoQuoting = true; - /** * @var array mapping between PDO driver names and [[Schema]] classes. * The keys of the array are PDO driver names while the values the corresponding * schema class name or configuration. Please refer to [[\Yii::createObject()]] for @@ -247,15 +237,15 @@ class Connection extends Component * [[Schema]] class to support DBMS that is not supported by Yii. */ public $schemaMap = array( - 'pgsql' => 'yii\db\pgsql\Schema', // PostgreSQL - 'mysqli' => 'yii\db\mysql\Schema', // MySQL - 'mysql' => 'yii\db\mysql\Schema', // MySQL - 'sqlite' => 'yii\db\sqlite\Schema', // sqlite 3 + 'pgsql' => 'yii\db\pgsql\Schema', // PostgreSQL + 'mysqli' => 'yii\db\mysql\Schema', // MySQL + 'mysql' => 'yii\db\mysql\Schema', // MySQL + 'sqlite' => 'yii\db\sqlite\Schema', // sqlite 3 'sqlite2' => 'yii\db\sqlite\Schema', // sqlite 2 'mssql' => 'yi\db\dao\mssql\Schema', // Mssql driver on windows hosts - 'dblib' => 'yii\db\mssql\Schema', // dblib drivers on linux (and maybe others os) hosts - 'sqlsrv' => 'yii\db\mssql\Schema', // Mssql - 'oci' => 'yii\db\oci\Schema', // Oracle driver + 'sqlsrv' => 'yii\db\mssql\Schema', // Mssql + 'oci' => 'yii\db\oci\Schema', // Oracle driver + 'dblib' => 'yii\db\mssql\Schema', // dblib drivers on linux (and maybe others os) hosts ); /** * @var Transaction the currently active transaction @@ -290,7 +280,7 @@ class Connection extends Component * This method is provided as a shortcut to setting two properties that are related * with query caching: [[queryCacheDuration]] and [[queryCacheDependency]]. * @param integer $duration the number of seconds that query results may remain valid in cache. - * See [[queryCacheDuration]] for more details. + * If not set, it will use the value of [[queryCacheDuration]]. See [[queryCacheDuration]] for more details. * @param \yii\caching\Dependency $dependency the dependency for the cached query result. * See [[queryCacheDependency]] for more details. */ @@ -323,12 +313,12 @@ class Connection extends Component throw new InvalidConfigException('Connection::dsn cannot be empty.'); } try { - \Yii::trace('Opening DB connection: ' . $this->dsn, __CLASS__); + \Yii::trace('Opening DB connection: ' . $this->dsn, __METHOD__); $this->pdo = $this->createPdoInstance(); $this->initConnection(); } catch (\PDOException $e) { - \Yii::error("Failed to open DB connection ({$this->dsn}): " . $e->getMessage(), __CLASS__); + \Yii::error("Failed to open DB connection ({$this->dsn}): " . $e->getMessage(), __METHOD__); $message = YII_DEBUG ? 'Failed to open DB connection: ' . $e->getMessage() : 'Failed to open DB connection.'; throw new Exception($message, $e->errorInfo, (int)$e->getCode()); } @@ -342,7 +332,7 @@ class Connection extends Component public function close() { if ($this->pdo !== null) { - \Yii::trace('Closing DB connection: ' . $this->dsn, __CLASS__); + \Yii::trace('Closing DB connection: ' . $this->dsn, __METHOD__); $this->pdo = null; $this->_schema = null; $this->_transaction = null; @@ -517,6 +507,27 @@ class Connection extends Component } /** + * Processes a SQL statement by quoting table and column names that are enclosed within double brackets. + * Tokens enclosed within double curly brackets are treated as table names, while + * tokens enclosed within double square brackets are column names. They will be quoted accordingly. + * Also, the percentage character "%" in a table name will be replaced with [[tablePrefix]]. + * @param string $sql the SQL to be quoted + * @return string the quoted SQL + */ + public function quoteSql($sql) + { + $db = $this; + return preg_replace_callback('/(\\{\\{([\w\-\. ]+)\\}\\}|\\[\\[([\w\-\. ]+)\\]\\])/', + function($matches) use($db) { + if (isset($matches[3])) { + return $db->quoteColumnName($matches[3]); + } else { + return str_replace('%', $this->tablePrefix, $db->quoteTableName($matches[2])); + } + }, $sql); + } + + /** * Returns the name of the DB driver for the current [[dsn]]. * @return string name of the DB driver */ diff --git a/framework/db/QueryBuilder.php b/framework/db/QueryBuilder.php index 75375cc..da43940 100644 --- a/framework/db/QueryBuilder.php +++ b/framework/db/QueryBuilder.php @@ -22,6 +22,11 @@ use yii\base\NotSupportedException; class QueryBuilder extends \yii\base\Object { /** + * The prefix for automatically generated query binding parameters. + */ + const PARAM_PREFIX = ':qp'; + + /** * @var Connection the database connection. */ public $db; @@ -58,11 +63,11 @@ class QueryBuilder extends \yii\base\Object $clauses = array( $this->buildSelect($query->select, $query->distinct, $query->selectOption), $this->buildFrom($query->from), - $this->buildJoin($query->join), - $this->buildWhere($query->where), + $this->buildJoin($query->join, $query->params), + $this->buildWhere($query->where, $query->params), $this->buildGroupBy($query->groupBy), - $this->buildHaving($query->having), - $this->buildUnion($query->union), + $this->buildHaving($query->having, $query->params), + $this->buildUnion($query->union, $query->params), $this->buildOrderBy($query->orderBy), $this->buildLimit($query->limit, $query->offset), ); @@ -92,7 +97,6 @@ class QueryBuilder extends \yii\base\Object { $names = array(); $placeholders = array(); - $count = 0; foreach ($columns as $name => $value) { $names[] = $this->db->quoteColumnName($name); if ($value instanceof Expression) { @@ -101,9 +105,9 @@ class QueryBuilder extends \yii\base\Object $params[$n] = $v; } } else { - $placeholders[] = ':p' . $count; - $params[':p' . $count] = $value; - $count++; + $phName = self::PARAM_PREFIX . count($params); + $placeholders[] = $phName; + $params[$phName] = $value; } } @@ -159,10 +163,9 @@ class QueryBuilder extends \yii\base\Object * so that they can be bound to the DB command later. * @return string the UPDATE SQL */ - public function update($table, $columns, $condition = '', &$params) + public function update($table, $columns, $condition, &$params) { $lines = array(); - $count = 0; foreach ($columns as $name => $value) { if ($value instanceof Expression) { $lines[] = $this->db->quoteColumnName($name) . '=' . $value->expression; @@ -170,17 +173,15 @@ class QueryBuilder extends \yii\base\Object $params[$n] = $v; } } else { - $lines[] = $this->db->quoteColumnName($name) . '=:p' . $count; - $params[':p' . $count] = $value; - $count++; + $phName = self::PARAM_PREFIX . count($params); + $lines[] = $this->db->quoteColumnName($name) . '=' . $phName; + $params[$phName] = $value; } } - $sql = 'UPDATE ' . $this->db->quoteTableName($table) . ' SET ' . implode(', ', $lines); - if (($where = $this->buildCondition($condition)) !== '') { - $sql .= ' WHERE ' . $where; - } - return $sql; + $sql = 'UPDATE ' . $this->db->quoteTableName($table) . ' SET ' . implode(', ', $lines); + $where = $this->buildWhere($condition, $params); + return $where === '' ? $sql : $sql . ' ' . $where; } /** @@ -196,15 +197,15 @@ class QueryBuilder extends \yii\base\Object * @param string $table the table where the data will be deleted from. * @param mixed $condition the condition that will be put in the WHERE part. Please * refer to [[Query::where()]] on how to specify condition. + * @param array $params the binding parameters that will be modified by this method + * so that they can be bound to the DB command later. * @return string the DELETE SQL */ - public function delete($table, $condition = '') + public function delete($table, $condition, &$params) { $sql = 'DELETE FROM ' . $this->db->quoteTableName($table); - if (($where = $this->buildCondition($condition)) !== '') { - $sql .= ' WHERE ' . $where; - } - return $sql; + $where = $this->buildWhere($condition, $params); + return $where === '' ? $sql : $sql . ' ' . $where; } /** @@ -479,200 +480,6 @@ class QueryBuilder extends \yii\base\Object } /** - * Parses the condition specification and generates the corresponding SQL expression. - * @param string|array $condition the condition specification. Please refer to [[Query::where()]] - * on how to specify a condition. - * @return string the generated SQL expression - * @throws \yii\db\Exception if the condition is in bad format - */ - public function buildCondition($condition) - { - static $builders = array( - 'AND' => 'buildAndCondition', - 'OR' => 'buildAndCondition', - 'BETWEEN' => 'buildBetweenCondition', - 'NOT BETWEEN' => 'buildBetweenCondition', - 'IN' => 'buildInCondition', - 'NOT IN' => 'buildInCondition', - 'LIKE' => 'buildLikeCondition', - 'NOT LIKE' => 'buildLikeCondition', - 'OR LIKE' => 'buildLikeCondition', - 'OR NOT LIKE' => 'buildLikeCondition', - ); - - if (!is_array($condition)) { - return (string)$condition; - } elseif ($condition === array()) { - return ''; - } - if (isset($condition[0])) { // operator format: operator, operand 1, operand 2, ... - $operator = strtoupper($condition[0]); - if (isset($builders[$operator])) { - $method = $builders[$operator]; - array_shift($condition); - return $this->$method($operator, $condition); - } else { - throw new Exception('Found unknown operator in query: ' . $operator); - } - } else { // hash format: 'column1'=>'value1', 'column2'=>'value2', ... - return $this->buildHashCondition($condition); - } - } - - private function buildHashCondition($condition) - { - $parts = array(); - foreach ($condition as $column => $value) { - if (is_array($value)) { // IN condition - $parts[] = $this->buildInCondition('in', array($column, $value)); - } else { - if (strpos($column, '(') === false) { - $column = $this->db->quoteColumnName($column); - } - if ($value === null) { - $parts[] = "$column IS NULL"; - } elseif (is_string($value)) { - $parts[] = "$column=" . $this->db->quoteValue($value); - } else { - $parts[] = "$column=$value"; - } - } - } - return count($parts) === 1 ? $parts[0] : '(' . implode(') AND (', $parts) . ')'; - } - - private function buildAndCondition($operator, $operands) - { - $parts = array(); - foreach ($operands as $operand) { - if (is_array($operand)) { - $operand = $this->buildCondition($operand); - } - if ($operand !== '') { - $parts[] = $operand; - } - } - if ($parts !== array()) { - return '(' . implode(") $operator (", $parts) . ')'; - } else { - return ''; - } - } - - private function buildBetweenCondition($operator, $operands) - { - if (!isset($operands[0], $operands[1], $operands[2])) { - throw new Exception("Operator '$operator' requires three operands."); - } - - list($column, $value1, $value2) = $operands; - - if (strpos($column, '(') === false) { - $column = $this->db->quoteColumnName($column); - } - $value1 = is_string($value1) ? $this->db->quoteValue($value1) : (string)$value1; - $value2 = is_string($value2) ? $this->db->quoteValue($value2) : (string)$value2; - - return "$column $operator $value1 AND $value2"; - } - - private function buildInCondition($operator, $operands) - { - if (!isset($operands[0], $operands[1])) { - throw new Exception("Operator '$operator' requires two operands."); - } - - list($column, $values) = $operands; - - $values = (array)$values; - - if ($values === array() || $column === array()) { - return $operator === 'IN' ? '0=1' : ''; - } - - if (count($column) > 1) { - return $this->buildCompositeInCondition($operator, $column, $values); - } elseif (is_array($column)) { - $column = reset($column); - } - foreach ($values as $i => $value) { - if (is_array($value)) { - $value = isset($value[$column]) ? $value[$column] : null; - } - if ($value === null) { - $values[$i] = 'NULL'; - } else { - $values[$i] = is_string($value) ? $this->db->quoteValue($value) : (string)$value; - } - } - if (strpos($column, '(') === false) { - $column = $this->db->quoteColumnName($column); - } - - if (count($values) > 1) { - return "$column $operator (" . implode(', ', $values) . ')'; - } else { - $operator = $operator === 'IN' ? '=' : '<>'; - return "$column$operator{$values[0]}"; - } - } - - protected function buildCompositeInCondition($operator, $columns, $values) - { - foreach ($columns as $i => $column) { - if (strpos($column, '(') === false) { - $columns[$i] = $this->db->quoteColumnName($column); - } - } - $vss = array(); - foreach ($values as $value) { - $vs = array(); - foreach ($columns as $column) { - if (isset($value[$column])) { - $vs[] = is_string($value[$column]) ? $this->db->quoteValue($value[$column]) : (string)$value[$column]; - } else { - $vs[] = 'NULL'; - } - } - $vss[] = '(' . implode(', ', $vs) . ')'; - } - return '(' . implode(', ', $columns) . ") $operator (" . implode(', ', $vss) . ')'; - } - - private function buildLikeCondition($operator, $operands) - { - if (!isset($operands[0], $operands[1])) { - throw new Exception("Operator '$operator' requires two operands."); - } - - list($column, $values) = $operands; - - $values = (array)$values; - - if ($values === array()) { - return $operator === 'LIKE' || $operator === 'OR LIKE' ? '0=1' : ''; - } - - if ($operator === 'LIKE' || $operator === 'NOT LIKE') { - $andor = ' AND '; - } else { - $andor = ' OR '; - $operator = $operator === 'OR LIKE' ? 'LIKE' : 'NOT LIKE'; - } - - if (strpos($column, '(') === false) { - $column = $this->db->quoteColumnName($column); - } - - $parts = array(); - foreach ($values as $value) { - $parts[] = "$column $operator " . $this->db->quoteValue($value); - } - - return implode($andor, $parts); - } - - /** * @param array $columns * @param boolean $distinct * @param string $selectOption @@ -737,10 +544,11 @@ class QueryBuilder extends \yii\base\Object /** * @param string|array $joins + * @param array $params the binding parameters to be populated * @return string the JOIN clause built from [[query]]. * @throws Exception if the $joins parameter is not in proper format */ - public function buildJoin($joins) + public function buildJoin($joins, &$params) { if (empty($joins)) { return ''; @@ -761,9 +569,9 @@ class QueryBuilder extends \yii\base\Object } $joins[$i] = $join[0] . ' ' . $table; if (isset($join[2])) { - $condition = $this->buildCondition($join[2]); + $condition = $this->buildCondition($join[2], $params); if ($condition !== '') { - $joins[$i] .= ' ON ' . $this->buildCondition($join[2]); + $joins[$i] .= ' ON ' . $condition; } } } else { @@ -776,11 +584,12 @@ class QueryBuilder extends \yii\base\Object /** * @param string|array $condition + * @param array $params the binding parameters to be populated * @return string the WHERE clause built from [[query]]. */ - public function buildWhere($condition) + public function buildWhere($condition, &$params) { - $where = $this->buildCondition($condition); + $where = $this->buildCondition($condition, $params); return $where === '' ? '' : 'WHERE ' . $where; } @@ -795,11 +604,12 @@ class QueryBuilder extends \yii\base\Object /** * @param string|array $condition + * @param array $params the binding parameters to be populated * @return string the HAVING clause built from [[query]]. */ - public function buildHaving($condition) + public function buildHaving($condition, &$params) { - $having = $this->buildCondition($condition); + $having = $this->buildCondition($condition, $params); return $having === '' ? '' : 'HAVING ' . $having; } @@ -843,16 +653,19 @@ class QueryBuilder extends \yii\base\Object /** * @param array $unions + * @param array $params the binding parameters to be populated * @return string the UNION clause built from [[query]]. */ - public function buildUnion($unions) + public function buildUnion($unions, &$params) { if (empty($unions)) { return ''; } foreach ($unions as $i => $union) { if ($union instanceof Query) { + $union->addParams($params); $unions[$i] = $this->build($union); + $params = $union->params; } } return "UNION (\n" . implode("\n) UNION (\n", $unions) . "\n)"; @@ -864,7 +677,7 @@ class QueryBuilder extends \yii\base\Object * @param string|array $columns the columns to be processed * @return string the processing result */ - protected function buildColumns($columns) + public function buildColumns($columns) { if (!is_array($columns)) { if (strpos($columns, '(') !== false) { @@ -882,4 +695,218 @@ class QueryBuilder extends \yii\base\Object } return is_array($columns) ? implode(', ', $columns) : $columns; } + + + /** + * Parses the condition specification and generates the corresponding SQL expression. + * @param string|array $condition the condition specification. Please refer to [[Query::where()]] + * on how to specify a condition. + * @param array $params the binding parameters to be populated + * @return string the generated SQL expression + * @throws \yii\db\Exception if the condition is in bad format + */ + public function buildCondition($condition, &$params) + { + static $builders = array( + 'AND' => 'buildAndCondition', + 'OR' => 'buildAndCondition', + 'BETWEEN' => 'buildBetweenCondition', + 'NOT BETWEEN' => 'buildBetweenCondition', + 'IN' => 'buildInCondition', + 'NOT IN' => 'buildInCondition', + 'LIKE' => 'buildLikeCondition', + 'NOT LIKE' => 'buildLikeCondition', + 'OR LIKE' => 'buildLikeCondition', + 'OR NOT LIKE' => 'buildLikeCondition', + ); + + if (!is_array($condition)) { + return (string)$condition; + } elseif ($condition === array()) { + return ''; + } + if (isset($condition[0])) { // operator format: operator, operand 1, operand 2, ... + $operator = strtoupper($condition[0]); + if (isset($builders[$operator])) { + $method = $builders[$operator]; + array_shift($condition); + return $this->$method($operator, $condition, $params); + } else { + throw new Exception('Found unknown operator in query: ' . $operator); + } + } else { // hash format: 'column1'=>'value1', 'column2'=>'value2', ... + return $this->buildHashCondition($condition, $params); + } + } + + private function buildHashCondition($condition, &$params) + { + $parts = array(); + foreach ($condition as $column => $value) { + if (is_array($value)) { // IN condition + $parts[] = $this->buildInCondition('in', array($column, $value), $query); + } else { + if (strpos($column, '(') === false) { + $column = $this->db->quoteColumnName($column); + } + if ($value === null) { + $parts[] = "$column IS NULL"; + } elseif ($value instanceof Expression) { + $parts[] = "$column=" . $value->expression; + foreach ($value->params as $n => $v) { + $params[$n] = $v; + } + } else { + $phName = self::PARAM_PREFIX . count($params); + $parts[] = "$column=$phName"; + $params[$phName] = $value; + } + } + } + return count($parts) === 1 ? $parts[0] : '(' . implode(') AND (', $parts) . ')'; + } + + private function buildAndCondition($operator, $operands, &$params) + { + $parts = array(); + foreach ($operands as $operand) { + if (is_array($operand)) { + $operand = $this->buildCondition($operand, $params); + } + if ($operand !== '') { + $parts[] = $operand; + } + } + if ($parts !== array()) { + return '(' . implode(") $operator (", $parts) . ')'; + } else { + return ''; + } + } + + private function buildBetweenCondition($operator, $operands, &$params) + { + if (!isset($operands[0], $operands[1], $operands[2])) { + throw new Exception("Operator '$operator' requires three operands."); + } + + list($column, $value1, $value2) = $operands; + + if (strpos($column, '(') === false) { + $column = $this->db->quoteColumnName($column); + } + $phName1 = self::PARAM_PREFIX . count($params); + $phName2 = self::PARAM_PREFIX . count($params); + $params[$phName1] = $value1; + $params[$phName2] = $value2; + + return "$column $operator $phName1 AND $phName2"; + } + + private function buildInCondition($operator, $operands, &$params) + { + if (!isset($operands[0], $operands[1])) { + throw new Exception("Operator '$operator' requires two operands."); + } + + list($column, $values) = $operands; + + $values = (array)$values; + + if ($values === array() || $column === array()) { + return $operator === 'IN' ? '0=1' : ''; + } + + if (count($column) > 1) { + return $this->buildCompositeInCondition($operator, $column, $values, $params); + } elseif (is_array($column)) { + $column = reset($column); + } + foreach ($values as $i => $value) { + if (is_array($value)) { + $value = isset($value[$column]) ? $value[$column] : null; + } + if ($value === null) { + $values[$i] = 'NULL'; + } elseif ($value instanceof Expression) { + $values[$i] = $value->expression; + foreach ($value->params as $n => $v) { + $params[$n] = $v; + } + } else { + $phName = self::PARAM_PREFIX . count($params); + $params[$phName] = $value; + $values[$i] = $phName; + } + } + if (strpos($column, '(') === false) { + $column = $this->db->quoteColumnName($column); + } + + if (count($values) > 1) { + return "$column $operator (" . implode(', ', $values) . ')'; + } else { + $operator = $operator === 'IN' ? '=' : '<>'; + return "$column$operator{$values[0]}"; + } + } + + protected function buildCompositeInCondition($operator, $columns, $values, &$params) + { + foreach ($columns as $i => $column) { + if (strpos($column, '(') === false) { + $columns[$i] = $this->db->quoteColumnName($column); + } + } + $vss = array(); + foreach ($values as $value) { + $vs = array(); + foreach ($columns as $column) { + if (isset($value[$column])) { + $phName = self::PARAM_PREFIX . count($params); + $params[$phName] = $value[$column]; + $vs[] = $phName; + } else { + $vs[] = 'NULL'; + } + } + $vss[] = '(' . implode(', ', $vs) . ')'; + } + return '(' . implode(', ', $columns) . ") $operator (" . implode(', ', $vss) . ')'; + } + + private function buildLikeCondition($operator, $operands, &$params) + { + if (!isset($operands[0], $operands[1])) { + throw new Exception("Operator '$operator' requires two operands."); + } + + list($column, $values) = $operands; + + $values = (array)$values; + + if ($values === array()) { + return $operator === 'LIKE' || $operator === 'OR LIKE' ? '0=1' : ''; + } + + if ($operator === 'LIKE' || $operator === 'NOT LIKE') { + $andor = ' AND '; + } else { + $andor = ' OR '; + $operator = $operator === 'OR LIKE' ? 'LIKE' : 'NOT LIKE'; + } + + if (strpos($column, '(') === false) { + $column = $this->db->quoteColumnName($column); + } + + $parts = array(); + foreach ($values as $value) { + $phName = self::PARAM_PREFIX . count($params); + $params[$phName] = $value; + $parts[] = "$column $operator $phName"; + } + + return implode($andor, $parts); + } } diff --git a/framework/db/Schema.php b/framework/db/Schema.php index 5fe6121..9538e4c 100644 --- a/framework/db/Schema.php +++ b/framework/db/Schema.php @@ -7,6 +7,7 @@ namespace yii\db; +use Yii; use yii\base\NotSupportedException; use yii\base\InvalidCallException; use yii\caching\Cache; @@ -82,23 +83,23 @@ abstract class Schema extends \yii\base\Object } $db = $this->db; - $realName = $this->getRealTableName($name); + $realName = $this->getRawTableName($name); - /** @var $cache Cache */ - if ($db->enableSchemaCache && ($cache = \Yii::$app->getComponent($db->schemaCacheID)) !== null && !in_array($name, $db->schemaCacheExclude, true)) { - $key = $this->getCacheKey($cache, $name); - if ($refresh || ($table = $cache->get($key)) === false) { - $table = $this->loadTableSchema($realName); - if ($table !== null) { - $cache->set($key, $table, $db->schemaCacheDuration); + if ($db->enableSchemaCache && !in_array($name, $db->schemaCacheExclude, true)) { + /** @var $cache Cache */ + $cache = is_string($db->schemaCache) ? Yii::$app->getComponent($db->schemaCache) : $db->schemaCache; + if ($cache instanceof Cache) { + $key = $this->getCacheKey($cache, $name); + if ($refresh || ($table = $cache->get($key)) === false) { + $table = $this->loadTableSchema($realName); + if ($table !== null) { + $cache->set($key, $table, $db->schemaCacheDuration); + } } + return $this->_tables[$name] = $table; } - $this->_tables[$name] = $table; - } else { - $this->_tables[$name] = $table = $this->loadTableSchema($realName); } - - return $table; + return $this->_tables[$name] = $table = $this->loadTableSchema($realName); } /** @@ -173,8 +174,9 @@ abstract class Schema extends \yii\base\Object */ public function refresh() { - /** @var $cache \yii\caching\Cache */ - if ($this->db->enableSchemaCache && ($cache = \Yii::$app->getComponent($this->db->schemaCacheID)) !== null) { + /** @var $cache Cache */ + $cache = is_string($this->db->schemaCache) ? Yii::$app->getComponent($this->db->schemaCache) : $this->db->schemaCache; + if ($this->db->enableSchemaCache && $cache instanceof Cache) { foreach ($this->_tables as $name => $table) { $cache->delete($this->getCacheKey($cache, $name)); } @@ -246,7 +248,7 @@ abstract class Schema extends \yii\base\Object /** * Quotes a table name for use in a query. * If the table name contains schema prefix, the prefix will also be properly quoted. - * If the table name is already quoted or contains special characters including '(', '[[' and '{{', + * If the table name is already quoted or contains '(' or '{{', * then this method will do nothing. * @param string $name table name * @return string the properly quoted table name @@ -254,7 +256,7 @@ abstract class Schema extends \yii\base\Object */ public function quoteTableName($name) { - if (strpos($name, '(') !== false || strpos($name, '[[') !== false || strpos($name, '{{') !== false) { + if (strpos($name, '(') !== false || strpos($name, '{{') !== false) { return $name; } if (strpos($name, '.') === false) { @@ -271,7 +273,7 @@ abstract class Schema extends \yii\base\Object /** * Quotes a column name for use in a query. * If the column name contains prefix, the prefix will also be properly quoted. - * If the column name is already quoted or contains special characters including '(', '[[' and '{{', + * If the column name is already quoted or contains '(', '[[' or '{{', * then this method will do nothing. * @param string $name column name * @return string the properly quoted column name @@ -316,15 +318,15 @@ abstract class Schema extends \yii\base\Object } /** - * Returns the real name of a table name. + * Returns the actual name of a given table name. * This method will strip off curly brackets from the given table name - * and replace the percentage character in the name with [[Connection::tablePrefix]]. + * and replace the percentage character '%' with [[Connection::tablePrefix]]. * @param string $name the table name to be converted * @return string the real name of the given table name */ - public function getRealTableName($name) + public function getRawTableName($name) { - if ($this->db->enableAutoQuoting && strpos($name, '{{') !== false) { + if (strpos($name, '{{') !== false) { $name = preg_replace('/\\{\\{(.*?)\\}\\}/', '\1', $name); return str_replace('%', $this->db->tablePrefix, $name); } else { diff --git a/framework/db/StaleObjectException.php b/framework/db/StaleObjectException.php new file mode 100644 index 0000000..860c9fc --- /dev/null +++ b/framework/db/StaleObjectException.php @@ -0,0 +1,23 @@ + + * @since 2.0 + */ +class StaleObjectException extends Exception +{ + /** + * @return string the user-friendly name of this exception + */ + public function getName() + { + return \Yii::t('yii|Stale Object Exception'); + } +} \ No newline at end of file diff --git a/framework/db/Transaction.php b/framework/db/Transaction.php index 177d2cb..d66c38e 100644 --- a/framework/db/Transaction.php +++ b/framework/db/Transaction.php @@ -66,7 +66,7 @@ class Transaction extends \yii\base\Object if ($this->db === null) { throw new InvalidConfigException('Transaction::db must be set.'); } - \Yii::trace('Starting transaction', __CLASS__); + \Yii::trace('Starting transaction', __METHOD__); $this->db->open(); $this->db->pdo->beginTransaction(); $this->_active = true; @@ -80,7 +80,7 @@ class Transaction extends \yii\base\Object public function commit() { if ($this->_active && $this->db && $this->db->isActive) { - \Yii::trace('Committing transaction', __CLASS__); + \Yii::trace('Committing transaction', __METHOD__); $this->db->pdo->commit(); $this->_active = false; } else { @@ -95,7 +95,7 @@ class Transaction extends \yii\base\Object public function rollback() { if ($this->_active && $this->db && $this->db->isActive) { - \Yii::trace('Rolling back transaction', __CLASS__); + \Yii::trace('Rolling back transaction', __METHOD__); $this->db->pdo->rollBack(); $this->_active = false; } else { diff --git a/framework/helpers/Html.php b/framework/helpers/Html.php index b004885..b2ca576 100644 --- a/framework/helpers/Html.php +++ b/framework/helpers/Html.php @@ -949,11 +949,10 @@ class Html * If the input parameter * * - is an empty string: the currently requested URL will be returned; - * - is a non-empty string: it will be processed by [[Yii::getAlias()]] which, if the string is an alias, - * will be resolved into a URL; + * - is a non-empty string: it will be processed by [[Yii::getAlias()]] and returned; * - is an array: the first array element is considered a route, while the rest of the name-value - * pairs are considered as the parameters to be used for URL creation using [[\yii\base\Application::createUrl()]]. - * Here are some examples: `array('post/index', 'page' => 2)`, `array('index')`. + * pairs are treated as the parameters to be used for URL creation using [[\yii\web\Controller::createUrl()]]. + * For example: `array('post/index', 'page' => 2)`, `array('index')`. * * @param array|string $url the parameter to be used to generate a valid URL * @return string the normalized URL @@ -963,7 +962,13 @@ class Html { if (is_array($url)) { if (isset($url[0])) { - return Yii::$app->createUrl($url[0], array_splice($url, 1)); + $route = $url[0]; + $params = array_splice($url, 1); + if (Yii::$app->controller !== null) { + return Yii::$app->controller->createUrl($route, $params); + } else { + return Yii::$app->getUrlManager()->createUrl($route, $params); + } } else { throw new InvalidParamException('The array specifying a URL must contain at least one element.'); } diff --git a/framework/i18n/I18N.php b/framework/i18n/I18N.php index 0409da3..8667abc 100644 --- a/framework/i18n/I18N.php +++ b/framework/i18n/I18N.php @@ -1,11 +1,23 @@ + * @since 2.0 + */ class I18N extends Component { /** @@ -13,11 +25,36 @@ class I18N extends Component * categories, and the array values are the corresponding [[MessageSource]] objects or the configurations * for creating the [[MessageSource]] objects. The message categories can contain the wildcard '*' at the end * to match multiple categories with the same prefix. For example, 'app\*' matches both 'app\cat1' and 'app\cat2'. + * + * This property may be modified on the fly by extensions who want to have their own message sources + * registered under their own namespaces. + * + * The category "yii" and "app" are always defined. The former refers to the messages used in the Yii core + * framework code, while the latter refers to the default message category for custom application code. + * By default, both of these categories use [[PhpMessageSource]] and the corresponding message files are + * stored under "@yii/messages" and "@app/messages", respectively. + * + * You may override the configuration of both categories. */ public $translations; + /** + * @var string the path or path alias of the file that contains the plural rules. + * By default, this refers to a file shipped with the Yii distribution. The file is obtained + * by converting from the data file in the CLDR project. + * + * If the default rule file does not contain the expected rules, you may copy and modify it + * for your application, and then configure this property to point to your modified copy. + * + * @see http://www.unicode.org/cldr/charts/supplemental/language_plural_rules.html + */ + public $pluralRuleFile = '@yii/i18n/data/plurals.php'; + /** + * Initializes the component by configuring the default message categories. + */ public function init() { + parent::init(); if (!isset($this->translations['yii'])) { $this->translations['yii'] = array( 'class' => 'yii\i18n\PhpMessageSource', @@ -34,6 +71,16 @@ class I18N extends Component } } + /** + * Translates a message to the specified language. + * If the first parameter in `$params` is a number and it is indexed by 0, appropriate plural rules + * will be applied to the translated message. + * @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`). If this is null, the current + * [[\yii\base\Application::language|application language]] will be used. + * @return string the translated message. + */ public function translate($message, $params = array(), $language = null) { if ($language === null) { @@ -55,7 +102,7 @@ class I18N extends Component } if (isset($params[0])) { - $message = $this->getPluralForm($message, $params[0], $language); + $message = $this->applyPluralRules($message, $params[0], $language); if (!isset($params['{n}'])) { $params['{n}'] = $params[0]; } @@ -65,6 +112,12 @@ class I18N extends Component return $params === array() ? $message : strtr($message, $params); } + /** + * Returns the message source for the given category. + * @param string $category the category name. + * @return MessageSource the message source for the given category. + * @throws InvalidConfigException if there is no message source available for the specified category. + */ public function getMessageSource($category) { if (isset($this->translations[$category])) { @@ -85,18 +138,21 @@ class I18N extends Component } } - public function getLocale($language) - { - - } - - protected function getPluralForm($message, $number, $language) + /** + * Applies appropriate plural rules to the given message. + * @param string $message the message to be applied with plural rules + * @param mixed $number the number by which plural rules will be applied + * @param string $language the language code that determines which set of plural rules to be applied. + * @return string the message that has applied plural rules + */ + protected function applyPluralRules($message, $number, $language) { if (strpos($message, '|') === false) { return $message; } $chunks = explode('|', $message); - $rules = $this->getLocale($language)->getPluralRules(); + + $rules = $this->getPluralRules($language); foreach ($rules as $i => $rule) { if (isset($chunks[$i]) && $this->evaluate($rule, $number)) { return $chunks[$i]; @@ -106,6 +162,29 @@ class I18N extends Component return isset($chunks[$n]) ? $chunks[$n] : $chunks[0]; } + private $_pluralRules = array(); // language => rule set + + /** + * Returns the plural rules for the given language code. + * @param string $language the language code (e.g. `en_US`, `en`). + * @return array the plural rules + * @throws InvalidParamException if the language code is invalid. + */ + protected function getPluralRules($language) + { + if (isset($this->_pluralRules[$language])) { + return $this->_pluralRules; + } + $allRules = require(Yii::getAlias($this->pluralRuleFile)); + if (isset($allRules[$language])) { + return $this->_pluralRules[$language] = $allRules[$language]; + } elseif (preg_match('/^[a-z]+/', strtolower($language), $matches)) { + return $this->_pluralRules[$language] = isset($allRules[$matches[0]]) ? $allRules[$matches[0]] : array(); + } else { + throw new InvalidParamException("Invalid language code: $language"); + } + } + /** * Evaluates a PHP expression with the given number value. * @param string $expression the PHP expression @@ -114,6 +193,6 @@ class I18N extends Component */ protected function evaluate($expression, $n) { - return @eval("return $expression;"); + return eval("return $expression;"); } } diff --git a/framework/i18n/PhpMessageSource.php b/framework/i18n/PhpMessageSource.php index 6b12353..1ada44a 100644 --- a/framework/i18n/PhpMessageSource.php +++ b/framework/i18n/PhpMessageSource.php @@ -72,7 +72,7 @@ class PhpMessageSource extends MessageSource } return $messages; } else { - Yii::error("The message file for category '$category' does not exist: $messageFile", __CLASS__); + Yii::error("The message file for category '$category' does not exist: $messageFile", __METHOD__); return array(); } } diff --git a/framework/i18n/data/plurals.php b/framework/i18n/data/plurals.php new file mode 100644 index 0000000..52c733b --- /dev/null +++ b/framework/i18n/data/plurals.php @@ -0,0 +1,627 @@ + + array ( + 0 => '$n==0', + 1 => '$n==1', + 2 => '$n==2', + 3 => 'in_array(fmod($n,100),range(3,10))', + 4 => 'in_array(fmod($n,100),range(11,99))', + ), + 'asa' => + array ( + 0 => '$n==1', + ), + 'af' => + array ( + 0 => '$n==1', + ), + 'bem' => + array ( + 0 => '$n==1', + ), + 'bez' => + array ( + 0 => '$n==1', + ), + 'bg' => + array ( + 0 => '$n==1', + ), + 'bn' => + array ( + 0 => '$n==1', + ), + 'brx' => + array ( + 0 => '$n==1', + ), + 'ca' => + array ( + 0 => '$n==1', + ), + 'cgg' => + array ( + 0 => '$n==1', + ), + 'chr' => + array ( + 0 => '$n==1', + ), + 'da' => + array ( + 0 => '$n==1', + ), + 'de' => + array ( + 0 => '$n==1', + ), + 'dv' => + array ( + 0 => '$n==1', + ), + 'ee' => + array ( + 0 => '$n==1', + ), + 'el' => + array ( + 0 => '$n==1', + ), + 'en' => + array ( + 0 => '$n==1', + ), + 'eo' => + array ( + 0 => '$n==1', + ), + 'es' => + array ( + 0 => '$n==1', + ), + 'et' => + array ( + 0 => '$n==1', + ), + 'eu' => + array ( + 0 => '$n==1', + ), + 'fi' => + array ( + 0 => '$n==1', + ), + 'fo' => + array ( + 0 => '$n==1', + ), + 'fur' => + array ( + 0 => '$n==1', + ), + 'fy' => + array ( + 0 => '$n==1', + ), + 'gl' => + array ( + 0 => '$n==1', + ), + 'gsw' => + array ( + 0 => '$n==1', + ), + 'gu' => + array ( + 0 => '$n==1', + ), + 'ha' => + array ( + 0 => '$n==1', + ), + 'haw' => + array ( + 0 => '$n==1', + ), + 'he' => + array ( + 0 => '$n==1', + ), + 'is' => + array ( + 0 => '$n==1', + ), + 'it' => + array ( + 0 => '$n==1', + ), + 'jmc' => + array ( + 0 => '$n==1', + ), + 'kaj' => + array ( + 0 => '$n==1', + ), + 'kcg' => + array ( + 0 => '$n==1', + ), + 'kk' => + array ( + 0 => '$n==1', + ), + 'kl' => + array ( + 0 => '$n==1', + ), + 'ksb' => + array ( + 0 => '$n==1', + ), + 'ku' => + array ( + 0 => '$n==1', + ), + 'lb' => + array ( + 0 => '$n==1', + ), + 'lg' => + array ( + 0 => '$n==1', + ), + 'mas' => + array ( + 0 => '$n==1', + ), + 'ml' => + array ( + 0 => '$n==1', + ), + 'mn' => + array ( + 0 => '$n==1', + ), + 'mr' => + array ( + 0 => '$n==1', + ), + 'nah' => + array ( + 0 => '$n==1', + ), + 'nb' => + array ( + 0 => '$n==1', + ), + 'nd' => + array ( + 0 => '$n==1', + ), + 'ne' => + array ( + 0 => '$n==1', + ), + 'nl' => + array ( + 0 => '$n==1', + ), + 'nn' => + array ( + 0 => '$n==1', + ), + 'no' => + array ( + 0 => '$n==1', + ), + 'nr' => + array ( + 0 => '$n==1', + ), + 'ny' => + array ( + 0 => '$n==1', + ), + 'nyn' => + array ( + 0 => '$n==1', + ), + 'om' => + array ( + 0 => '$n==1', + ), + 'or' => + array ( + 0 => '$n==1', + ), + 'pa' => + array ( + 0 => '$n==1', + ), + 'pap' => + array ( + 0 => '$n==1', + ), + 'ps' => + array ( + 0 => '$n==1', + ), + 'pt' => + array ( + 0 => '$n==1', + ), + 'rof' => + array ( + 0 => '$n==1', + ), + 'rm' => + array ( + 0 => '$n==1', + ), + 'rwk' => + array ( + 0 => '$n==1', + ), + 'saq' => + array ( + 0 => '$n==1', + ), + 'seh' => + array ( + 0 => '$n==1', + ), + 'sn' => + array ( + 0 => '$n==1', + ), + 'so' => + array ( + 0 => '$n==1', + ), + 'sq' => + array ( + 0 => '$n==1', + ), + 'ss' => + array ( + 0 => '$n==1', + ), + 'ssy' => + array ( + 0 => '$n==1', + ), + 'st' => + array ( + 0 => '$n==1', + ), + 'sv' => + array ( + 0 => '$n==1', + ), + 'sw' => + array ( + 0 => '$n==1', + ), + 'syr' => + array ( + 0 => '$n==1', + ), + 'ta' => + array ( + 0 => '$n==1', + ), + 'te' => + array ( + 0 => '$n==1', + ), + 'teo' => + array ( + 0 => '$n==1', + ), + 'tig' => + array ( + 0 => '$n==1', + ), + 'tk' => + array ( + 0 => '$n==1', + ), + 'tn' => + array ( + 0 => '$n==1', + ), + 'ts' => + array ( + 0 => '$n==1', + ), + 'ur' => + array ( + 0 => '$n==1', + ), + 'wae' => + array ( + 0 => '$n==1', + ), + 've' => + array ( + 0 => '$n==1', + ), + 'vun' => + array ( + 0 => '$n==1', + ), + 'xh' => + array ( + 0 => '$n==1', + ), + 'xog' => + array ( + 0 => '$n==1', + ), + 'zu' => + array ( + 0 => '$n==1', + ), + 'ak' => + array ( + 0 => '($n==0||$n==1)', + ), + 'am' => + array ( + 0 => '($n==0||$n==1)', + ), + 'bh' => + array ( + 0 => '($n==0||$n==1)', + ), + 'fil' => + array ( + 0 => '($n==0||$n==1)', + ), + 'tl' => + array ( + 0 => '($n==0||$n==1)', + ), + 'guw' => + array ( + 0 => '($n==0||$n==1)', + ), + 'hi' => + array ( + 0 => '($n==0||$n==1)', + ), + 'ln' => + array ( + 0 => '($n==0||$n==1)', + ), + 'mg' => + array ( + 0 => '($n==0||$n==1)', + ), + 'nso' => + array ( + 0 => '($n==0||$n==1)', + ), + 'ti' => + array ( + 0 => '($n==0||$n==1)', + ), + 'wa' => + array ( + 0 => '($n==0||$n==1)', + ), + 'ff' => + array ( + 0 => '($n>=0&&$n<=2)&&$n!=2', + ), + 'fr' => + array ( + 0 => '($n>=0&&$n<=2)&&$n!=2', + ), + 'kab' => + array ( + 0 => '($n>=0&&$n<=2)&&$n!=2', + ), + 'lv' => + array ( + 0 => '$n==0', + 1 => 'fmod($n,10)==1&&fmod($n,100)!=11', + ), + 'iu' => + array ( + 0 => '$n==1', + 1 => '$n==2', + ), + 'kw' => + array ( + 0 => '$n==1', + 1 => '$n==2', + ), + 'naq' => + array ( + 0 => '$n==1', + 1 => '$n==2', + ), + 'se' => + array ( + 0 => '$n==1', + 1 => '$n==2', + ), + 'sma' => + array ( + 0 => '$n==1', + 1 => '$n==2', + ), + 'smi' => + array ( + 0 => '$n==1', + 1 => '$n==2', + ), + 'smj' => + array ( + 0 => '$n==1', + 1 => '$n==2', + ), + 'smn' => + array ( + 0 => '$n==1', + 1 => '$n==2', + ), + 'sms' => + array ( + 0 => '$n==1', + 1 => '$n==2', + ), + 'ga' => + array ( + 0 => '$n==1', + 1 => '$n==2', + 2 => 'in_array($n,array(3,4,5,6))', + 3 => 'in_array($n,array(7,8,9,10))', + ), + 'ro' => + array ( + 0 => '$n==1', + 1 => '$n==0||$n!=1&&in_array(fmod($n,100),range(1,19))', + ), + 'mo' => + array ( + 0 => '$n==1', + 1 => '$n==0||$n!=1&&in_array(fmod($n,100),range(1,19))', + ), + 'lt' => + array ( + 0 => 'fmod($n,10)==1&&!in_array(fmod($n,100),range(11,19))', + 1 => 'in_array(fmod($n,10),range(2,9))&&!in_array(fmod($n,100),range(11,19))', + ), + 'be' => + array ( + 0 => 'fmod($n,10)==1&&fmod($n,100)!=11', + 1 => 'in_array(fmod($n,10),array(2,3,4))&&!in_array(fmod($n,100),array(12,13,14))', + 2 => 'fmod($n,10)==0||in_array(fmod($n,10),array(5,6,7,8,9))||in_array(fmod($n,100),array(11,12,13,14))', + ), + 'bs' => + array ( + 0 => 'fmod($n,10)==1&&fmod($n,100)!=11', + 1 => 'in_array(fmod($n,10),array(2,3,4))&&!in_array(fmod($n,100),array(12,13,14))', + 2 => 'fmod($n,10)==0||in_array(fmod($n,10),array(5,6,7,8,9))||in_array(fmod($n,100),array(11,12,13,14))', + ), + 'hr' => + array ( + 0 => 'fmod($n,10)==1&&fmod($n,100)!=11', + 1 => 'in_array(fmod($n,10),array(2,3,4))&&!in_array(fmod($n,100),array(12,13,14))', + 2 => 'fmod($n,10)==0||in_array(fmod($n,10),array(5,6,7,8,9))||in_array(fmod($n,100),array(11,12,13,14))', + ), + 'ru' => + array ( + 0 => 'fmod($n,10)==1&&fmod($n,100)!=11', + 1 => 'in_array(fmod($n,10),array(2,3,4))&&!in_array(fmod($n,100),array(12,13,14))', + 2 => 'fmod($n,10)==0||in_array(fmod($n,10),array(5,6,7,8,9))||in_array(fmod($n,100),array(11,12,13,14))', + ), + 'sh' => + array ( + 0 => 'fmod($n,10)==1&&fmod($n,100)!=11', + 1 => 'in_array(fmod($n,10),array(2,3,4))&&!in_array(fmod($n,100),array(12,13,14))', + 2 => 'fmod($n,10)==0||in_array(fmod($n,10),array(5,6,7,8,9))||in_array(fmod($n,100),array(11,12,13,14))', + ), + 'sr' => + array ( + 0 => 'fmod($n,10)==1&&fmod($n,100)!=11', + 1 => 'in_array(fmod($n,10),array(2,3,4))&&!in_array(fmod($n,100),array(12,13,14))', + 2 => 'fmod($n,10)==0||in_array(fmod($n,10),array(5,6,7,8,9))||in_array(fmod($n,100),array(11,12,13,14))', + ), + 'uk' => + array ( + 0 => 'fmod($n,10)==1&&fmod($n,100)!=11', + 1 => 'in_array(fmod($n,10),array(2,3,4))&&!in_array(fmod($n,100),array(12,13,14))', + 2 => 'fmod($n,10)==0||in_array(fmod($n,10),array(5,6,7,8,9))||in_array(fmod($n,100),array(11,12,13,14))', + ), + 'cs' => + array ( + 0 => '$n==1', + 1 => 'in_array($n,array(2,3,4))', + ), + 'sk' => + array ( + 0 => '$n==1', + 1 => 'in_array($n,array(2,3,4))', + ), + 'pl' => + array ( + 0 => '$n==1', + 1 => 'in_array(fmod($n,10),array(2,3,4))&&!in_array(fmod($n,100),array(12,13,14))', + 2 => '$n!=1&&in_array(fmod($n,10),array(0,1))||in_array(fmod($n,10),array(5,6,7,8,9))||in_array(fmod($n,100),array(12,13,14))', + ), + 'sl' => + array ( + 0 => 'fmod($n,100)==1', + 1 => 'fmod($n,100)==2', + 2 => 'in_array(fmod($n,100),array(3,4))', + ), + 'mt' => + array ( + 0 => '$n==1', + 1 => '$n==0||in_array(fmod($n,100),range(2,10))', + 2 => 'in_array(fmod($n,100),range(11,19))', + ), + 'mk' => + array ( + 0 => 'fmod($n,10)==1&&$n!=11', + ), + 'cy' => + array ( + 0 => '$n==0', + 1 => '$n==1', + 2 => '$n==2', + 3 => '$n==3', + 4 => '$n==6', + ), + 'lag' => + array ( + 0 => '$n==0', + 1 => '($n>=0&&$n<=2)&&$n!=0&&$n!=2', + ), + 'shi' => + array ( + 0 => '($n>=0&&$n<=1)', + 1 => 'in_array($n,range(2,10))', + ), + 'br' => + array ( + 0 => 'fmod($n,10)==1&&!in_array(fmod($n,100),array(11,71,91))', + 1 => 'fmod($n,10)==2&&!in_array(fmod($n,100),array(12,72,92))', + 2 => 'in_array(fmod($n,10),array(3,4,9))&&!in_array(fmod($n,100),array_merge(range(10,19),range(70,79),range(90,99)))', + 3 => 'fmod($n,1000000)==0&&$n!=0', + ), + 'ksh' => + array ( + 0 => '$n==0', + 1 => '$n==1', + ), + 'tzm' => + array ( + 0 => '($n==0||$n==1)||in_array($n,range(11,99))', + ), + 'gv' => + array ( + 0 => 'in_array(fmod($n,10),array(1,2))||fmod($n,20)==0', + ), +); \ No newline at end of file diff --git a/framework/i18n/data/plurals.xml b/framework/i18n/data/plurals.xml new file mode 100644 index 0000000..9227dc6 --- /dev/null +++ b/framework/i18n/data/plurals.xml @@ -0,0 +1,109 @@ + + + + + + + + + + n is 0 + n is 1 + n is 2 + n mod 100 in 3..10 + n mod 100 in 11..99 + + + n is 1 + + + n in 0..1 + + + n within 0..2 and n is not 2 + + + n is 0 + n mod 10 is 1 and n mod 100 is not 11 + + + n is 1 + n is 2 + + + n is 1 + n is 2 + n in 3..6 + n in 7..10 + + + n is 1 + n is 0 OR n is not 1 AND n mod 100 in 1..19 + + + n mod 10 is 1 and n mod 100 not in 11..19 + n mod 10 in 2..9 and n mod 100 not in 11..19 + + + n mod 10 is 1 and n mod 100 is not 11 + n mod 10 in 2..4 and n mod 100 not in 12..14 + n mod 10 is 0 or n mod 10 in 5..9 or n mod 100 in 11..14 + + + + n is 1 + n in 2..4 + + + n is 1 + n mod 10 in 2..4 and n mod 100 not in 12..14 + n is not 1 and n mod 10 in 0..1 or n mod 10 in 5..9 or n mod 100 in 12..14 + + + + + n mod 100 is 1 + n mod 100 is 2 + n mod 100 in 3..4 + + + n is 1 + n is 0 or n mod 100 in 2..10 + n mod 100 in 11..19 + + + n mod 10 is 1 and n is not 11 + + + n is 0 + n is 1 + n is 2 + n is 3 + n is 6 + + + n is 0 + n within 0..2 and n is not 0 and n is not 2 + + + n within 0..1 + n in 2..10 + + + n mod 10 is 1 and n mod 100 not in 11,71,91 + n mod 10 is 2 and n mod 100 not in 12,72,92 + n mod 10 in 3..4,9 and n mod 100 not in 10..19,70..79,90..99 + n mod 1000000 is 0 and n is not 0 + + + n is 0 + n is 1 + + + n in 0..1 or n in 11..99 + + + n mod 10 in 1..2 or n mod 20 is 0 + + + diff --git a/framework/logging/DbTarget.php b/framework/logging/DbTarget.php index 364b5a4..ce9d843 100644 --- a/framework/logging/DbTarget.php +++ b/framework/logging/DbTarget.php @@ -7,16 +7,15 @@ namespace yii\logging; +use Yii; use yii\db\Connection; use yii\base\InvalidConfigException; /** * DbTarget stores log messages in a database table. * - * By default, DbTarget will use the database specified by [[connectionID]] and save - * messages into a table named by [[tableName]]. Please refer to [[tableName]] for the required - * table structure. Note that this table must be created beforehand. Otherwise an exception - * will be thrown when DbTarget is saving messages into DB. + * By default, DbTarget stores the log messages in a DB table named 'tbl_log'. This table + * must be pre-created. The table name can be changed by setting [[logTable]]. * * @author Qiang Xue * @since 2.0 @@ -24,20 +23,18 @@ use yii\base\InvalidConfigException; class DbTarget extends Target { /** - * @var string the ID of [[Connection]] application component. - * Defaults to 'db'. Please make sure that your database contains a table - * whose name is as specified in [[tableName]] and has the required table structure. - * @see tableName + * @var Connection|string the DB connection object or the application component ID of the DB connection. + * After the DbTarget object is created, if you want to change this property, you should only assign it + * with a DB connection object. */ - public $connectionID = 'db'; + public $db = 'db'; /** - * @var string the name of the DB table that stores log messages. Defaults to 'tbl_log'. - * - * The DB table should have the following structure: + * @var string name of the DB table to store cache content. + * The table should be pre-created as follows: * * ~~~ * CREATE TABLE tbl_log ( - * id INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY, + * id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, * level INTEGER, * category VARCHAR(255), * log_time INTEGER, @@ -48,42 +45,29 @@ class DbTarget extends Target * ~~~ * * Note that the 'id' column must be created as an auto-incremental column. - * The above SQL shows the syntax of MySQL. If you are using other DBMS, you need + * The above SQL uses the MySQL syntax. If you are using other DBMS, you need * to adjust it accordingly. For example, in PostgreSQL, it should be `id SERIAL PRIMARY KEY`. * * The indexes declared above are not required. They are mainly used to improve the performance * of some queries about message levels and categories. Depending on your actual needs, you may - * want to create additional indexes (e.g. index on log_time). + * want to create additional indexes (e.g. index on `log_time`). */ - public $tableName = 'tbl_log'; - - private $_db; + public $logTable = 'tbl_log'; /** - * Returns the DB connection used for saving log messages. - * @return Connection the DB connection instance - * @throws InvalidConfigException if [[connectionID]] does not point to a valid application component. + * Initializes the DbTarget component. + * This method will initialize the [[db]] property to make sure it refers to a valid DB connection. + * @throws InvalidConfigException if [[db]] is invalid. */ - public function getDb() + public function init() { - if ($this->_db === null) { - $db = \Yii::$app->getComponent($this->connectionID); - if ($db instanceof Connection) { - $this->_db = $db; - } else { - throw new InvalidConfigException("DbTarget::connectionID must refer to the ID of a DB application component."); - } + parent::init(); + if (is_string($this->db)) { + $this->db = Yii::$app->getComponent($this->db); + } + if (!$this->db instanceof Connection) { + throw new InvalidConfigException("DbTarget::db must be either a DB connection instance or the application component ID of a DB connection."); } - return $this->_db; - } - - /** - * Sets the DB connection used by the cache component. - * @param Connection $value the DB connection instance - */ - public function setDb($value) - { - $this->_db = $value; } /** @@ -93,10 +77,10 @@ class DbTarget extends Target */ public function export($messages) { - $db = $this->getDb(); - $tableName = $db->quoteTableName($this->tableName); - $sql = "INSERT INTO $tableName (level, category, log_time, message) VALUES (:level, :category, :log_time, :message)"; - $command = $db->createCommand($sql); + $tableName = $this->db->quoteTableName($this->logTable); + $sql = "INSERT INTO $tableName ([[level]], [[category]], [[log_time]], [[message]]) + VALUES (:level, :category, :log_time, :message)"; + $command = $this->db->createCommand($sql); foreach ($messages as $message) { $command->bindValues(array( ':level' => $message[1], diff --git a/framework/logging/Target.php b/framework/logging/Target.php index b88e78d..e76e8ac 100644 --- a/framework/logging/Target.php +++ b/framework/logging/Target.php @@ -238,6 +238,7 @@ abstract class Target extends \yii\base\Component if (!is_string($text)) { $text = var_export($text, true); } - return date('Y/m/d H:i:s', $timestamp) . " [$level] [$category] $text\n"; + $ip = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '127.0.0.1'; + return date('Y/m/d H:i:s', $timestamp) . " [$ip] [$level] [$category] $text\n"; } } diff --git a/framework/web/AccessControl.php b/framework/web/AccessControl.php new file mode 100644 index 0000000..793fb05 --- /dev/null +++ b/framework/web/AccessControl.php @@ -0,0 +1,104 @@ + + * @since 2.0 + */ +class AccessControl extends ActionFilter +{ + /** + * @var callback a callback that will be called if the access should be denied + * to the current user. If not set, [[denyAccess()]] will be called. + * + * The signature of the callback should be as follows: + * + * ~~~ + * function ($rule, $action) + * ~~~ + * + * where `$rule` is this rule, and `$action` is the current [[Action|action]] object. + */ + public $denyCallback; + /** + * @var string the default class of the access rules. This is used when + * a rule is configured without specifying a class in [[rules]]. + */ + public $defaultRuleClass = 'yii\web\AccessRule'; + /** + * @var array a list of access rule objects or configurations for creating the rule objects. + */ + public $rules = array(); + + /** + * Initializes the [[rules]] array by instantiating rule objects from configurations. + */ + public function init() + { + parent::init(); + foreach ($this->rules as $i => $rule) { + if (is_array($rule)) { + if (!isset($rule['class'])) { + $rule['class'] = $this->defaultRuleClass; + } + $this->rules[$i] = Yii::createObject($rule); + } + } + } + + /** + * This method is invoked right before an action is to be executed (after all possible filters.) + * You may override this method to do last-minute preparation for the action. + * @param Action $action the action to be executed. + * @return boolean whether the action should continue to be executed. + */ + public function beforeAction($action) + { + $user = Yii::$app->getUser(); + $request = Yii::$app->getRequest(); + /** @var $rule AccessRule */ + foreach ($this->rules as $rule) { + if ($allow = $rule->allows($action, $user, $request)) { + break; + } elseif ($allow === false) { + if (isset($rule->denyCallback)) { + call_user_func($rule->denyCallback, $rule); + } elseif (isset($this->denyCallback)) { + call_user_func($this->denyCallback, $rule); + } else { + $this->denyAccess($user); + } + return false; + } + } + return true; + } + + /** + * Denies the access of the user. + * The default implementation will redirect the user to the login page if he is a guest; + * if the user is already logged, a 403 HTTP exception will be thrown. + * @param User $user the current user + * @throws HttpException if the user is already logged in. + */ + protected function denyAccess($user) + { + if ($user->getIsGuest()) { + $user->loginRequired(); + } else { + throw new HttpException(403, Yii::t('yii|You are not allowed to perform this action.')); + } + } +} \ No newline at end of file diff --git a/framework/web/AccessRule.php b/framework/web/AccessRule.php new file mode 100644 index 0000000..3f8c057 --- /dev/null +++ b/framework/web/AccessRule.php @@ -0,0 +1,188 @@ + + * @since 2.0 + */ +class AccessRule extends Component +{ + /** + * @var boolean whether this is an 'allow' rule or 'deny' rule. + */ + public $allow; + /** + * @var array list of action IDs that this rule applies to. The comparison is case-sensitive. + * If not set or empty, it means this rule applies to all actions. + */ + public $actions; + /** + * @var array list of controller IDs that this rule applies to. The comparison is case-sensitive. + * If not set or empty, it means this rule applies to all controllers. + */ + public $controllers; + /** + * @var array list of roles that this rule applies to. Two special roles are recognized, and + * they are checked via [[User::isGuest]]: + * + * - `?`: matches a guest user (not authenticated yet) + * - `@`: matches an authenticated user + * + * Using additional role names requires RBAC (Role-Based Access Control), and + * [[User::hasAccess()]] will be called. + * + * If this property is not set or empty, it means this rule applies to all roles. + */ + public $roles; + /** + * @var array list of user IP addresses that this rule applies to. An IP address + * can contain the wildcard `*` at the end so that it matches IP addresses with the same prefix. + * For example, '192.168.*' matches all IP addresses in the segment '192.168.'. + * If not set or empty, it means this rule applies to all IP addresses. + * @see Request::userIP + */ + public $ips; + /** + * @var array list of request methods (e.g. `GET`, `POST`) that this rule applies to. + * The request methods must be specified in uppercase. + * If not set or empty, it means this rule applies to all request methods. + * @see Request::requestMethod + */ + public $verbs; + /** + * @var callback a callback that will be called to determine if the rule should be applied. + * The signature of the callback should be as follows: + * + * ~~~ + * function ($rule, $action) + * ~~~ + * + * where `$rule` is this rule, and `$action` is the current [[Action|action]] object. + * The callback should return a boolean value indicating whether this rule should be applied. + */ + public $matchCallback; + /** + * @var callback a callback that will be called if this rule determines the access to + * the current action should be denied. If not set, the behavior will be determined by + * [[AccessControl]]. + * + * The signature of the callback should be as follows: + * + * ~~~ + * function ($rule, $action) + * ~~~ + * + * where `$rule` is this rule, and `$action` is the current [[Action|action]] object. + */ + public $denyCallback; + + + /** + * Checks whether the Web user is allowed to perform the specified action. + * @param Action $action the action to be performed + * @param User $user the user object + * @param Request $request + * @return boolean|null true if the user is allowed, false if the user is denied, null if the rule does not apply to the user + */ + public function allows($action, $user, $request) + { + if ($this->matchAction($action) + && $this->matchRole($user) + && $this->matchIP($request->getUserIP()) + && $this->matchVerb($request->getRequestMethod()) + && $this->matchController($action->controller) + && $this->matchCustom($action) + ) { + return $this->allow ? true : false; + } else { + return null; + } + } + + /** + * @param Action $action the action + * @return boolean whether the rule applies to the action + */ + protected function matchAction($action) + { + return empty($this->actions) || in_array($action->id, $this->actions, true); + } + + /** + * @param Controller $controller the controller + * @return boolean whether the rule applies to the controller + */ + protected function matchController($controller) + { + return empty($this->controllers) || in_array($controller->id, $this->controllers, true); + } + + /** + * @param User $user the user object + * @return boolean whether the rule applies to the role + */ + protected function matchRole($user) + { + if (empty($this->roles)) { + return true; + } + foreach ($this->roles as $role) { + if ($role === '?' && $user->getIsGuest()) { + return true; + } elseif ($role === '@' && !$user->getIsGuest()) { + return true; + } elseif ($user->hasAccess($role)) { + return true; + } + } + return false; + } + + /** + * @param string $ip the IP address + * @return boolean whether the rule applies to the IP address + */ + protected function matchIP($ip) + { + if (empty($this->ips)) { + return true; + } + foreach ($this->ips as $rule) { + if ($rule === '*' || $rule === $ip || (($pos = strpos($rule, '*')) !== false && !strncmp($ip, $rule, $pos))) { + return true; + } + } + return false; + } + + /** + * @param string $verb the request method + * @return boolean whether the rule applies to the request + */ + protected function matchVerb($verb) + { + return empty($this->verbs) || in_array($verb, $this->verbs, true); + } + + /** + * @param Action $action the action to be performed + * @return boolean whether the rule should be applied + */ + protected function matchCustom($action) + { + return empty($this->matchCallback) || call_user_func($this->matchCallback, $this, $action); + } +} \ No newline at end of file diff --git a/framework/web/Application.php b/framework/web/Application.php index 6e0cc73..b839d92 100644 --- a/framework/web/Application.php +++ b/framework/web/Application.php @@ -7,7 +7,7 @@ namespace yii\web; -use yii\base\InvalidParamException; +use Yii; /** * Application is the base class for all application classes. @@ -28,7 +28,7 @@ class Application extends \yii\base\Application public function registerDefaultAliases() { parent::registerDefaultAliases(); - \Yii::$aliases['@webroot'] = dirname($_SERVER['SCRIPT_FILENAME']); + Yii::$aliases['@webroot'] = dirname($_SERVER['SCRIPT_FILENAME']); } /** @@ -41,6 +41,32 @@ class Application extends \yii\base\Application return $this->runAction($route, $params); } + private $_homeUrl; + + /** + * @return string the homepage URL + */ + public function getHomeUrl() + { + if ($this->_homeUrl === null) { + if ($this->getUrlManager()->showScriptName) { + return $this->getRequest()->getScriptUrl(); + } else { + return $this->getRequest()->getBaseUrl() . '/'; + } + } else { + return $this->_homeUrl; + } + } + + /** + * @param string $value the homepage URL + */ + public function setHomeUrl($value) + { + $this->_homeUrl = $value; + } + /** * Returns the request component. * @return Request the request component @@ -69,48 +95,12 @@ class Application extends \yii\base\Application } /** - * Creates a URL using the given route and parameters. - * - * This method first normalizes the given route by converting a relative route into an absolute one. - * A relative route is a route without a leading slash. It is considered to be relative to the currently - * requested route. If the route is an empty string, it stands for the route of the currently active - * [[controller]]. Otherwise, the [[Controller::uniqueId]] will be prepended to the route. - * - * After normalizing the route, this method calls [[\yii\web\UrlManager::createUrl()]] - * to create a relative URL. - * - * @param string $route the route. This can be either an absolute or a relative route. - * @param array $params the parameters (name-value pairs) to be included in the generated URL - * @return string the created URL - * @throws InvalidParamException if a relative route is given and there is no active controller. - * @see createAbsoluteUrl + * Returns the user component. + * @return User the user component */ - public function createUrl($route, $params = array()) + public function getUser() { - if (strncmp($route, '/', 1) !== 0) { - // a relative route - if ($this->controller !== null) { - $route = $route === '' ? $this->controller->route : $this->controller->uniqueId . '/' . $route; - } else { - throw new InvalidParamException('Relative route cannot be handled because there is no active controller.'); - } - } - return $this->getUrlManager()->createUrl($route, $params); - } - - /** - * Creates an absolute URL using the given route and parameters. - * This method first calls [[createUrl()]] to create a relative URL. - * It then prepends [[\yii\web\UrlManager::hostInfo]] to the URL to form an absolute one. - * @param string $route the route. This can be either an absolute or a relative route. - * See [[createUrl()]] for more details. - * @param array $params the parameters (name-value pairs) - * @return string the created URL - * @see createUrl - */ - public function createAbsoluteUrl($route, $params = array()) - { - return $this->getUrlManager()->getHostInfo() . $this->createUrl($route, $params); + return $this->getComponent('user'); } /** @@ -130,6 +120,9 @@ class Application extends \yii\base\Application 'session' => array( 'class' => 'yii\web\Session', ), + 'user' => array( + 'class' => 'yii\web\User', + ), )); } } diff --git a/framework/web/CacheSession.php b/framework/web/CacheSession.php index d7882a6..c125f01 100644 --- a/framework/web/CacheSession.php +++ b/framework/web/CacheSession.php @@ -15,7 +15,7 @@ use yii\base\InvalidConfigException; * CacheSession implements a session component using cache as storage medium. * * The cache being used can be any cache application component. - * The ID of the cache application component is specified via [[cacheID]], which defaults to 'cache'. + * The ID of the cache application component is specified via [[cache]], which defaults to 'cache'. * * Beware, by definition cache storage are volatile, which means the data stored on them * may be swapped out and get lost. Therefore, you must make sure the cache used by this component @@ -27,14 +27,27 @@ use yii\base\InvalidConfigException; class CacheSession extends Session { /** - * @var string the ID of the cache application component. Defaults to 'cache' (the primary cache application component.) + * @var Cache|string the cache object or the application component ID of the cache object. + * The session data will be stored using this cache object. + * + * After the CacheSession object is created, if you want to change this property, + * you should only assign it with a cache object. */ - public $cacheID = 'cache'; + public $cache = 'cache'; /** - * @var Cache the cache component + * Initializes the application component. */ - private $_cache; + public function init() + { + parent::init(); + if (is_string($this->cache)) { + $this->cache = Yii::$app->getComponent($this->cache); + } + if (!$this->cache instanceof Cache) { + throw new InvalidConfigException('CacheSession::cache must refer to the application component ID of a cache object.'); + } + } /** * Returns a value indicating whether to use custom session storage. @@ -47,33 +60,6 @@ class CacheSession extends Session } /** - * Returns the cache instance used for storing session data. - * @return Cache the cache instance - * @throws InvalidConfigException if [[cacheID]] does not point to a valid application component. - */ - public function getCache() - { - if ($this->_cache === null) { - $cache = Yii::$app->getComponent($this->cacheID); - if ($cache instanceof Cache) { - $this->_cache = $cache; - } else { - throw new InvalidConfigException('CacheSession::cacheID must refer to the ID of a cache application component.'); - } - } - return $this->_cache; - } - - /** - * Sets the cache instance used by the session component. - * @param Cache $value the cache instance - */ - public function setCache($value) - { - $this->_cache = $value; - } - - /** * Session read handler. * Do not call this method directly. * @param string $id session ID @@ -81,7 +67,7 @@ class CacheSession extends Session */ public function readSession($id) { - $data = $this->getCache()->get($this->calculateKey($id)); + $data = $this->cache->get($this->calculateKey($id)); return $data === false ? '' : $data; } @@ -94,7 +80,7 @@ class CacheSession extends Session */ public function writeSession($id, $data) { - return $this->getCache()->set($this->calculateKey($id), $data, $this->getTimeout()); + return $this->cache->set($this->calculateKey($id), $data, $this->getTimeout()); } /** @@ -105,7 +91,7 @@ class CacheSession extends Session */ public function destroySession($id) { - return $this->getCache()->delete($this->calculateKey($id)); + return $this->cache->delete($this->calculateKey($id)); } /** @@ -115,6 +101,6 @@ class CacheSession extends Session */ protected function calculateKey($id) { - return $this->getCache()->buildKey(array(__CLASS__, $id)); + return $this->cache->buildKey(array(__CLASS__, $id)); } } diff --git a/framework/web/Controller.php b/framework/web/Controller.php index 2779c35..8049299 100644 --- a/framework/web/Controller.php +++ b/framework/web/Controller.php @@ -7,6 +7,9 @@ namespace yii\web; +use Yii; +use yii\helpers\Html; + /** * Controller is the base class of Web controllers. * @@ -16,4 +19,26 @@ namespace yii\web; */ class Controller extends \yii\base\Controller { + /** + * Creates a URL using the given route and parameters. + * + * This method enhances [[UrlManager::createUrl()]] by supporting relative routes. + * A relative route is a route without a slash, such as "view". If the route is an empty + * string, [[route]] will be used; Otherwise, [[uniqueId]] will be prepended to a relative route. + * + * After this route conversion, the method This method calls [[UrlManager::createUrl()]] + * to create a URL. + * + * @param string $route the route. This can be either an absolute route or a relative route. + * @param array $params the parameters (name-value pairs) to be included in the generated URL + * @return string the created URL + */ + public function createUrl($route, $params = array()) + { + if (strpos($route, '/') === false) { + // a relative route + $route = $route === '' ? $this->getRoute() : $this->getUniqueId() . '/' . $route; + } + return Yii::$app->getUrlManager()->createUrl($route, $params); + } } \ No newline at end of file diff --git a/framework/web/DbSession.php b/framework/web/DbSession.php index 812185a..2910b40 100644 --- a/framework/web/DbSession.php +++ b/framework/web/DbSession.php @@ -15,58 +15,70 @@ use yii\base\InvalidConfigException; /** * DbSession extends [[Session]] by using database as session data storage. * - * DbSession uses a DB application component to perform DB operations. The ID of the DB application - * component is specified via [[connectionID]] which defaults to 'db'. - * * By default, DbSession stores session data in a DB table named 'tbl_session'. This table - * must be pre-created. The table name can be changed by setting [[sessionTableName]]. - * The table should have the following structure: - * + * must be pre-created. The table name can be changed by setting [[sessionTable]]. + * + * The following example shows how you can configure the application to use DbSession: + * * ~~~ - * CREATE TABLE tbl_session - * ( - * id CHAR(32) PRIMARY KEY, - * expire INTEGER, - * data BLOB + * 'session' => array( + * 'class' => 'yii\web\DbSession', + * // 'db' => 'mydb', + * // 'sessionTable' => 'my_session', * ) * ~~~ * - * where 'BLOB' refers to the BLOB-type of your preferred database. Below are the BLOB type - * that can be used for some popular databases: - * - * - MySQL: LONGBLOB - * - PostgreSQL: BYTEA - * - MSSQL: BLOB - * - * When using DbSession in a production server, we recommend you create a DB index for the 'expire' - * column in the session table to improve the performance. - * * @author Qiang Xue * @since 2.0 */ class DbSession extends Session { /** - * @var string the ID of a {@link CDbConnection} application component. If not set, a SQLite database - * will be automatically created and used. The SQLite database file is - * is protected/runtime/session-YiiVersion.db. + * @var Connection|string the DB connection object or the application component ID of the DB connection. + * After the DbSession object is created, if you want to change this property, you should only assign it + * with a DB connection object. */ - public $connectionID; + public $db = 'db'; /** - * @var string the name of the DB table to store session content. - * Note, if {@link autoCreateSessionTable} is false and you want to create the DB table manually by yourself, - * you need to make sure the DB table is of the following structure: - *
-	 * (id CHAR(32) PRIMARY KEY, expire INTEGER, data BLOB)
-	 * 
- * @see autoCreateSessionTable + * @var string the name of the DB table that stores the session data. + * The table should be pre-created as follows: + * + * ~~~ + * CREATE TABLE tbl_session + * ( + * id CHAR(40) NOT NULL PRIMARY KEY, + * expire INTEGER, + * data BLOB + * ) + * ~~~ + * + * where 'BLOB' refers to the BLOB-type of your preferred DBMS. Below are the BLOB type + * that can be used for some popular DBMS: + * + * - MySQL: LONGBLOB + * - PostgreSQL: BYTEA + * - MSSQL: BLOB + * + * When using DbSession in a production server, we recommend you create a DB index for the 'expire' + * column in the session table to improve the performance. */ - public $sessionTableName = 'tbl_session'; + public $sessionTable = 'tbl_session'; + /** - * @var Connection the DB connection instance + * Initializes the DbSession component. + * This method will initialize the [[db]] property to make sure it refers to a valid DB connection. + * @throws InvalidConfigException if [[db]] is invalid. */ - private $_db; - + public function init() + { + parent::init(); + if (is_string($this->db)) { + $this->db = Yii::$app->getComponent($this->db); + } + if (!$this->db instanceof Connection) { + throw new InvalidConfigException("DbSession::db must be either a DB connection instance or the application component ID of a DB connection."); + } + } /** * Returns a value indicating whether to use custom session storage. @@ -94,56 +106,31 @@ class DbSession extends Session parent::regenerateID(false); $newID = session_id(); - $db = $this->getDb(); $query = new Query; - $row = $query->from($this->sessionTableName) + $row = $query->from($this->sessionTable) ->where(array('id' => $oldID)) - ->createCommand($db) + ->createCommand($this->db) ->queryRow(); if ($row !== false) { if ($deleteOldSession) { - $db->createCommand()->update($this->sessionTableName, array( - 'id' => $newID - ), array('id' => $oldID))->execute(); + $this->db->createCommand() + ->update($this->sessionTable, array('id' => $newID), array('id' => $oldID)) + ->execute(); } else { $row['id'] = $newID; - $db->createCommand()->insert($this->sessionTableName, $row)->execute(); + $this->db->createCommand() + ->insert($this->sessionTable, $row) + ->execute(); } } else { // shouldn't reach here normally - $db->createCommand()->insert($this->sessionTableName, array( - 'id' => $newID, - 'expire' => time() + $this->getTimeout(), - ))->execute(); - } - } - - /** - * Returns the DB connection instance used for storing session data. - * @return Connection the DB connection instance - * @throws InvalidConfigException if [[connectionID]] does not point to a valid application component. - */ - public function getDb() - { - if ($this->_db === null) { - $db = Yii::$app->getComponent($this->connectionID); - if ($db instanceof Connection) { - $this->_db = $db; - } else { - throw new InvalidConfigException("DbSession::connectionID must refer to the ID of a DB application component."); - } + $this->db->createCommand() + ->insert($this->sessionTable, array( + 'id' => $newID, + 'expire' => time() + $this->getTimeout(), + ))->execute(); } - return $this->_db; - } - - /** - * Sets the DB connection used by the session component. - * @param Connection $value the DB connection instance - */ - public function setDb($value) - { - $this->_db = $value; } /** @@ -156,9 +143,9 @@ class DbSession extends Session { $query = new Query; $data = $query->select(array('data')) - ->from($this->sessionTableName) - ->where('expire>:expire AND id=:id', array(':expire' => time(), ':id' => $id)) - ->createCommand($this->getDb()) + ->from($this->sessionTable) + ->where('[[expire]]>:expire AND [[id]]=:id', array(':expire' => time(), ':id' => $id)) + ->createCommand($this->db) ->queryScalar(); return $data === false ? '' : $data; } @@ -176,24 +163,23 @@ class DbSession extends Session // http://us.php.net/manual/en/function.session-set-save-handler.php try { $expire = time() + $this->getTimeout(); - $db = $this->getDb(); $query = new Query; $exists = $query->select(array('id')) - ->from($this->sessionTableName) + ->from($this->sessionTable) ->where(array('id' => $id)) - ->createCommand($db) + ->createCommand($this->db) ->queryScalar(); if ($exists === false) { - $db->createCommand()->insert($this->sessionTableName, array( - 'id' => $id, - 'data' => $data, - 'expire' => $expire, - ))->execute(); + $this->db->createCommand() + ->insert($this->sessionTable, array( + 'id' => $id, + 'data' => $data, + 'expire' => $expire, + ))->execute(); } else { - $db->createCommand()->update($this->sessionTableName, array( - 'data' => $data, - 'expire' => $expire - ), array('id' => $id))->execute(); + $this->db->createCommand() + ->update($this->sessionTable, array('data' => $data, 'expire' => $expire), array('id' => $id)) + ->execute(); } } catch (\Exception $e) { if (YII_DEBUG) { @@ -213,8 +199,8 @@ class DbSession extends Session */ public function destroySession($id) { - $this->getDb()->createCommand() - ->delete($this->sessionTableName, array('id' => $id)) + $this->db->createCommand() + ->delete($this->sessionTable, array('id' => $id)) ->execute(); return true; } @@ -227,8 +213,8 @@ class DbSession extends Session */ public function gcSession($maxLifetime) { - $this->getDb()->createCommand() - ->delete($this->sessionTableName, 'expire<:expire', array(':expire' => time())) + $this->db->createCommand() + ->delete($this->sessionTable, '[[expire]]<:expire', array(':expire' => time())) ->execute(); return true; } diff --git a/framework/web/HttpCache.php b/framework/web/HttpCache.php new file mode 100644 index 0000000..f64b37f --- /dev/null +++ b/framework/web/HttpCache.php @@ -0,0 +1,131 @@ + + * @author Qiang Xue + * @since 2.0 + */ +class HttpCache extends ActionFilter +{ + /** + * @var callback a PHP callback that returns the UNIX timestamp of the last modification time. + * The callback's signature should be: + * + * ~~~ + * function ($action, $params) + * ~~~ + * + * where `$action` is the [[Action]] object that this filter is currently handling; + * `$params` takes the value of [[params]]. The callback should return a UNIX timestamp. + */ + public $lastModified; + /** + * @var callback a PHP callback that generates the Etag seed string. + * The callback's signature should be: + * + * ~~~ + * function ($action, $params) + * ~~~ + * + * where `$action` is the [[Action]] object that this filter is currently handling; + * `$params` takes the value of [[params]]. The callback should return a string serving + * as the seed for generating an Etag. + */ + public $etagSeed; + /** + * @var mixed additional parameters that should be passed to the [[lastModified]] and [[etagSeed]] callbacks. + */ + public $params; + /** + * @var string HTTP cache control header. If null, the header will not be sent. + */ + public $cacheControlHeader = 'Cache-Control: max-age=3600, public'; + + /** + * This method is invoked right before an action is to be executed (after all possible filters.) + * You may override this method to do last-minute preparation for the action. + * @param Action $action the action to be executed. + * @return boolean whether the action should continue to be executed. + */ + public function beforeAction($action) + { + $verb = Yii::$app->request->getRequestMethod(); + if ($verb !== 'GET' && $verb !== 'HEAD' || $this->lastModified === null && $this->etagSeed === null) { + return true; + } + + $lastModified = $etag = null; + if ($this->lastModified !== null) { + $lastModified = call_user_func($this->lastModified, $action, $this->params); + } + if ($this->etagSeed !== null) { + $seed = call_user_func($this->etagSeed, $action, $this->params); + $etag = $this->generateEtag($seed); + } + + $this->sendCacheControlHeader(); + if ($etag !== null) { + header("ETag: $etag"); + } + + if ($this->validateCache($lastModified, $etag)) { + header('HTTP/1.1 304 Not Modified'); + return false; + } + + if ($lastModified !== null) { + header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $lastModified) . ' GMT'); + } + return true; + } + + /** + * Validates if the HTTP cache contains valid content. + * @param integer $lastModified the calculated Last-Modified value in terms of a UNIX timestamp. + * If null, the Last-Modified header will not be validated. + * @param string $etag the calculated ETag value. If null, the ETag header will not be validated. + * @return boolean whether the HTTP cache is still valid. + */ + protected function validateCache($lastModified, $etag) + { + if ($lastModified !== null && (!isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) || @strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) < $lastModified)) { + return false; + } else { + return $etag === null || isset($_SERVER['HTTP_IF_NONE_MATCH']) && $_SERVER['HTTP_IF_NONE_MATCH'] === $etag; + } + } + + /** + * Sends the cache control header to the client + * @see cacheControl + */ + protected function sendCacheControlHeader() + { + session_cache_limiter('public'); + header('Pragma:', true); + if ($this->cacheControlHeader !== null) { + header($this->cacheControlHeader, true); + } + } + + /** + * Generates an Etag from the given seed string. + * @param string $seed Seed for the ETag + * @return string the generated Etag + */ + protected function generateEtag($seed) + { + return '"' . base64_encode(sha1($seed, true)) . '"'; + } +} \ No newline at end of file diff --git a/framework/web/Identity.php b/framework/web/Identity.php new file mode 100644 index 0000000..6d67bc0 --- /dev/null +++ b/framework/web/Identity.php @@ -0,0 +1,81 @@ +id; + * } + * + * public function getAuthKey() + * { + * return $this->authKey; + * } + * + * public function validateAuthKey($authKey) + * { + * return $this->authKey === $authKey; + * } + * } + * ~~~ + * + * @author Qiang Xue + * @since 2.0 + */ +interface Identity +{ + /** + * Finds an identity by the given ID. + * @param string|integer $id the ID to be looked for + * @return Identity the identity object that matches the given ID. + * Null should be returned if such an identity cannot be found + * or the identity is not in an active state (disabled, deleted, etc.) + */ + public static function findIdentity($id); + /** + * Returns an ID that can uniquely identify a user identity. + * @return string|integer an ID that uniquely identifies a user identity. + */ + public function getId(); + /** + * Returns a key that can be used to check the validity of a given identity ID. + * + * The key should be unique for each individual user, and should be persistent + * so that it can be used to check the validity of the user identity. + * + * The space of such keys should be big enough to defeat potential identity attacks. + * + * This is required if [[User::enableAutoLogin]] is enabled. + * @return string a key that is used to check the validity of a given identity ID. + * @see validateAuthKey() + */ + public function getAuthKey(); + /** + * Validates the given auth key. + * + * This is required if [[User::enableAutoLogin]] is enabled. + * @param string $authKey the given auth key + * @return boolean whether the given auth key is valid. + * @see getAuthKey() + */ + public function validateAuthKey($authKey); +} \ No newline at end of file diff --git a/framework/web/PageCache.php b/framework/web/PageCache.php index 24cddea..5a50825 100644 --- a/framework/web/PageCache.php +++ b/framework/web/PageCache.php @@ -1,110 +1,104 @@ - - * @since 2.0 - */ -class PageCache extends ActionFilter -{ - /** - * @var boolean whether the content being cached should be differentiated according to the route. - * A route consists of the requested controller ID and action ID. Defaults to true. - */ - public $varyByRoute = true; - /** - * @var View the view object that is used to create the fragment cache widget to implement page caching. - * If not set, the view registered with the application will be used. - */ - public $view; - - /** - * @var string the ID of the cache application component. Defaults to 'cache' (the primary cache application component.) - */ - public $cacheID = 'cache'; - /** - * @var integer number of seconds that the data can remain valid in cache. - * Use 0 to indicate that the cached data will never expire. - */ - public $duration = 60; - /** - * @var array|Dependency the dependency that the cached content depends on. - * This can be either a [[Dependency]] object or a configuration array for creating the dependency object. - * For example, - * - * ~~~ - * array( - * 'class' => 'yii\caching\DbDependency', - * 'sql' => 'SELECT MAX(lastModified) FROM Post', - * ) - * ~~~ - * - * would make the output cache depends on the last modified time of all posts. - * If any post has its modification time changed, the cached content would be invalidated. - */ - public $dependency; - /** - * @var array list of factors that would cause the variation of the content being cached. - * Each factor is a string representing a variation (e.g. the language, a GET parameter). - * The following variation setting will cause the content to be cached in different versions - * according to the current application language: - * - * ~~~ - * array( - * Yii::$app->language, - * ) - */ - public $variations; - /** - * @var boolean whether to enable the fragment cache. You may use this property to turn on and off - * the fragment cache according to specific setting (e.g. enable fragment cache only for GET requests). - */ - public $enabled = true; - - - public function init() - { - parent::init(); - if ($this->view === null) { - $this->view = Yii::$app->getView(); - } - } - - /** - * This method is invoked right before an action is to be executed (after all possible filters.) - * You may override this method to do last-minute preparation for the action. - * @param Action $action the action to be executed. - * @return boolean whether the action should continue to be executed. - */ - public function beforeAction($action) - { - $properties = array(); - foreach (array('cacheID', 'duration', 'dependency', 'variations', 'enabled') as $name) { - $properties[$name] = $this->$name; - } - $id = $this->varyByRoute ? $action->getUniqueId() : __CLASS__; - return $this->view->beginCache($id, $properties); - } - - /** - * This method is invoked right after an action is executed. - * You may override this method to do some postprocessing for the action. - * @param Action $action the action just executed. - */ - public function afterAction($action) - { - $this->view->endCache(); - } + + * @since 2.0 + */ +class PageCache extends ActionFilter +{ + /** + * @var boolean whether the content being cached should be differentiated according to the route. + * A route consists of the requested controller ID and action ID. Defaults to true. + */ + public $varyByRoute = true; + /** + * @var string the application component ID of the [[\yii\caching\Cache|cache]] object. + */ + public $cache = 'cache'; + /** + * @var integer number of seconds that the data can remain valid in cache. + * Use 0 to indicate that the cached data will never expire. + */ + public $duration = 60; + /** + * @var array|Dependency the dependency that the cached content depends on. + * This can be either a [[Dependency]] object or a configuration array for creating the dependency object. + * For example, + * + * ~~~ + * array( + * 'class' => 'yii\caching\DbDependency', + * 'sql' => 'SELECT MAX(lastModified) FROM Post', + * ) + * ~~~ + * + * would make the output cache depends on the last modified time of all posts. + * If any post has its modification time changed, the cached content would be invalidated. + */ + public $dependency; + /** + * @var array list of factors that would cause the variation of the content being cached. + * Each factor is a string representing a variation (e.g. the language, a GET parameter). + * The following variation setting will cause the content to be cached in different versions + * according to the current application language: + * + * ~~~ + * array( + * Yii::$app->language, + * ) + */ + public $variations; + /** + * @var boolean whether to enable the fragment cache. You may use this property to turn on and off + * the fragment cache according to specific setting (e.g. enable fragment cache only for GET requests). + */ + public $enabled = true; + + + public function init() + { + parent::init(); + if ($this->view === null) { + $this->view = Yii::$app->getView(); + } + } + + /** + * This method is invoked right before an action is to be executed (after all possible filters.) + * You may override this method to do last-minute preparation for the action. + * @param Action $action the action to be executed. + * @return boolean whether the action should continue to be executed. + */ + public function beforeAction($action) + { + $properties = array(); + foreach (array('cache', 'duration', 'dependency', 'variations', 'enabled') as $name) { + $properties[$name] = $this->$name; + } + $id = $this->varyByRoute ? $action->getUniqueId() : __CLASS__; + return $this->view->beginCache($id, $properties); + } + + /** + * This method is invoked right after an action is executed. + * You may override this method to do some postprocessing for the action. + * @param Action $action the action just executed. + */ + public function afterAction($action) + { + $this->view->endCache(); + } } \ No newline at end of file diff --git a/framework/web/Request.php b/framework/web/Request.php index c7899cf..093a394 100644 --- a/framework/web/Request.php +++ b/framework/web/Request.php @@ -530,7 +530,7 @@ class Request extends \yii\base\Request * Returns the user IP address. * @return string user IP address */ - public function getUserHostAddress() + public function getUserIP() { return isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '127.0.0.1'; } diff --git a/framework/web/Response.php b/framework/web/Response.php index d6659cf..da2482f 100644 --- a/framework/web/Response.php +++ b/framework/web/Response.php @@ -7,7 +7,9 @@ namespace yii\web; +use Yii; use yii\helpers\FileHelper; +use yii\helpers\Html; /** * @author Qiang Xue @@ -16,6 +18,14 @@ use yii\helpers\FileHelper; class Response extends \yii\base\Response { /** + * @var integer the HTTP status code that should be used when redirecting in AJAX mode. + * This is used by [[redirect()]]. A 2xx code should normally be used for this purpose + * so that the AJAX handler will treat the response as a success. + * @see redirect + */ + public $ajaxRedirectCode = 278; + + /** * Sends a file to user. * @param string $fileName file name * @param string $content content to be set. @@ -106,57 +116,85 @@ class Response extends \yii\base\Response *
  • addHeaders: an array of additional http headers in header-value pairs (available since version 1.1.10)
  • * */ - public function xSendFile($filePath, $options=array()) + public function xSendFile($filePath, $options = array()) { - if(!isset($options['forceDownload']) || $options['forceDownload']) - $disposition='attachment'; - else - $disposition='inline'; + if (!isset($options['forceDownload']) || $options['forceDownload']) { + $disposition = 'attachment'; + } else { + $disposition = 'inline'; + } - if(!isset($options['saveName'])) - $options['saveName']=basename($filePath); + if (!isset($options['saveName'])) { + $options['saveName'] = basename($filePath); + } - if(!isset($options['mimeType'])) - { - if(($options['mimeType']=CFileHelper::getMimeTypeByExtension($filePath))===null) - $options['mimeType']='text/plain'; + if (!isset($options['mimeType'])) { + if (($options['mimeType'] = CFileHelper::getMimeTypeByExtension($filePath)) === null) { + $options['mimeType'] = 'text/plain'; + } } - if(!isset($options['xHeader'])) - $options['xHeader']='X-Sendfile'; + if (!isset($options['xHeader'])) { + $options['xHeader'] = 'X-Sendfile'; + } - if($options['mimeType'] !== null) - header('Content-type: '.$options['mimeType']); - header('Content-Disposition: '.$disposition.'; filename="'.$options['saveName'].'"'); - if(isset($options['addHeaders'])) - { - foreach($options['addHeaders'] as $header=>$value) - header($header.': '.$value); + if ($options['mimeType'] !== null) { + header('Content-type: ' . $options['mimeType']); } - header(trim($options['xHeader']).': '.$filePath); + header('Content-Disposition: ' . $disposition . '; filename="' . $options['saveName'] . '"'); + if (isset($options['addHeaders'])) { + foreach ($options['addHeaders'] as $header => $value) { + header($header . ': ' . $value); + } + } + header(trim($options['xHeader']) . ': ' . $filePath); - if(!isset($options['terminate']) || $options['terminate']) - Yii::app()->end(); + if (!isset($options['terminate']) || $options['terminate']) { + Yii::$app->end(); + } } /** * Redirects the browser to the specified URL. - * @param string $url URL to be redirected to. Note that when URL is not - * absolute (not starting with "/") it will be relative to current request URL. + * This method will send out a "Location" header to achieve the redirection. + * In AJAX mode, this normally will not work as expected unless there are some + * client-side JavaScript code handling the redirection. To help achieve this goal, + * this method will use [[ajaxRedirectCode]] as the HTTP status code when performing + * redirection in AJAX mode. The following JavaScript code may be used on the client + * side to handle the redirection response: + * + * ~~~ + * $(document).ajaxSuccess(function(event, xhr, settings) { + * if (xhr.status == 278) { + * window.location = xhr.getResponseHeader('Location'); + * } + * }); + * ~~~ + * + * @param array|string $url the URL to be redirected to. [[\yii\helpers\Html::url()]] + * will be used to normalize the URL. If the resulting URL is still a relative URL + * (one without host info), the current request host info will be used. * @param boolean $terminate whether to terminate the current application - * @param integer $statusCode the HTTP status code. Defaults to 302. See {@link http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html} + * @param integer $statusCode the HTTP status code. Defaults to 302. + * See [[http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html]] * for details about HTTP status code. + * Note that if the request is an AJAX request, [[ajaxRedirectCode]] will be used instead. */ - public function redirect($url,$terminate=true,$statusCode=302) + public function redirect($url, $terminate = true, $statusCode = 302) { - if(strpos($url,'/')===0 && strpos($url,'//')!==0) - $url=$this->getHostInfo().$url; - header('Location: '.$url, true, $statusCode); - if($terminate) - Yii::app()->end(); + $url = Html::url($url); + if (strpos($url, '/') === 0 && strpos($url, '//') !== 0) { + $url = Yii::$app->getRequest()->getHostInfo() . $url; + } + if (Yii::$app->getRequest()->getIsAjaxRequest()) { + $statusCode = $this->ajaxRedirectCode; + } + header('Location: ' . $url, true, $statusCode); + if ($terminate) { + Yii::$app->end(); + } } - /** * Returns the cookie collection. * Through the returned cookie collection, you add or remove cookies as follows, @@ -178,6 +216,6 @@ class Response extends \yii\base\Response */ public function getCookies() { - return \Yii::$app->getRequest()->getCookies(); + return Yii::$app->getRequest()->getCookies(); } } diff --git a/framework/web/Session.php b/framework/web/Session.php index 5697679..4c0505f 100644 --- a/framework/web/Session.php +++ b/framework/web/Session.php @@ -12,13 +12,15 @@ use yii\base\Component; use yii\base\InvalidParamException; /** - * Session provides session-level data management and the related configurations. + * Session provides session data management and the related configurations. * + * Session is a Web application component that can be accessed via `Yii::$app->session`. + * To start the session, call [[open()]]; To complete and send out session data, call [[close()]]; * To destroy the session, call [[destroy()]]. * - * If [[autoStart]] is set true, the session will be started automatically - * when the application component is initialized by the application. + * By default, [[autoStart]] is true which means the session will be started automatically + * when the session component is accessed the first time. * * Session can be used like an array to set and get session data. For example, * @@ -37,22 +39,11 @@ use yii\base\InvalidParamException; * [[openSession()]], [[closeSession()]], [[readSession()]], [[writeSession()]], * [[destroySession()]] and [[gcSession()]]. * - * Session is a Web application component that can be accessed via - * `Yii::$app->session`. - * - * @property boolean $useCustomStorage read-only. Whether to use custom storage. - * @property boolean $isActive Whether the session has started. - * @property string $id The current session ID. - * @property string $name The current session name. - * @property string $savePath The current session save path, defaults to '/tmp'. - * @property array $cookieParams The session cookie parameters. - * @property string $cookieMode How to use cookie to store session ID. Defaults to 'Allow'. - * @property float $gcProbability The probability (percentage) that the gc (garbage collection) process is started on every session initialization. - * @property boolean $useTransparentSessionID Whether transparent sid support is enabled or not, defaults to false. - * @property integer $timeout The number of seconds after which data will be seen as 'garbage' and cleaned up, defaults to 1440 seconds. - * @property SessionIterator $iterator An iterator for traversing the session variables. - * @property integer $count The number of session variables. - * @property array $keys The list of session variable names. + * Session also supports a special type of session data, called *flash messages*. + * A flash message is available only in the current request and the next request. + * After that, it will be deleted automatically. Flash messages are particularly + * useful for displaying confirmation messages. To use flash messages, simply + * call methods such as [[setFlash()]], [[getFlash()]]. * * @author Qiang Xue * @since 2.0 @@ -63,6 +54,17 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co * @var boolean whether the session should be automatically started when the session component is initialized. */ public $autoStart = true; + /** + * @var string the name of the session variable that stores the flash message data. + */ + public $flashVar = '__flash'; + + /** + * @var array parameter-value pairs to override default session cookie parameters + */ + public $cookieParams = array( + 'httponly' => true + ); /** * Initializes the application component. @@ -89,6 +91,8 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co return false; } + private $_opened = false; + /** * Starts the session. */ @@ -97,27 +101,36 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co // this is available in PHP 5.4.0+ if (function_exists('session_status')) { if (session_status() == PHP_SESSION_ACTIVE) { + $this->_opened = true; return; } } - if ($this->getUseCustomStorage()) { - @session_set_save_handler( - array($this, 'openSession'), - array($this, 'closeSession'), - array($this, 'readSession'), - array($this, 'writeSession'), - array($this, 'destroySession'), - array($this, 'gcSession') - ); - } + if (!$this->_opened) { + if ($this->getUseCustomStorage()) { + @session_set_save_handler( + array($this, 'openSession'), + array($this, 'closeSession'), + array($this, 'readSession'), + array($this, 'writeSession'), + array($this, 'destroySession'), + array($this, 'gcSession') + ); + } + + $this->setCookieParams($this->cookieParams); - @session_start(); + @session_start(); - if (session_id() == '') { - $error = error_get_last(); - $message = isset($error['message']) ? $error['message'] : 'Failed to start session.'; - Yii::warning($message, __CLASS__); + if (session_id() == '') { + $this->_opened = false; + $error = error_get_last(); + $message = isset($error['message']) ? $error['message'] : 'Failed to start session.'; + Yii::error($message, __METHOD__); + } else { + $this->_opened = true; + $this->updateFlashCounters(); + } } } @@ -126,6 +139,7 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co */ public function close() { + $this->_opened = false; if (session_id() !== '') { @session_write_close(); } @@ -152,7 +166,7 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co return session_status() == PHP_SESSION_ACTIVE; } else { // this is not very reliable - return session_id() !== ''; + return $this->_opened && session_id() !== ''; } } @@ -462,18 +476,18 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co /** * Adds a session variable. - * Note, if the specified name already exists, the old value will be removed first. - * @param mixed $key session variable name + * If the specified name already exists, the old value will be overwritten. + * @param string $key session variable name * @param mixed $value session variable value */ - public function add($key, $value) + public function set($key, $value) { $_SESSION[$key] = $value; } /** * Removes a session variable. - * @param mixed $key the name of the session variable to be removed + * @param string $key the name of the session variable to be removed * @return mixed the removed value, null if no such session variable. */ public function remove($key) @@ -490,7 +504,7 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co /** * Removes all session variables */ - public function clear() + public function removeAll() { foreach (array_keys($_SESSION) as $key) { unset($_SESSION[$key]); @@ -501,7 +515,7 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co * @param mixed $key session variable name * @return boolean whether there is the named session variable */ - public function contains($key) + public function has($key) { return isset($_SESSION[$key]); } @@ -515,6 +529,115 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co } /** + * Updates the counters for flash messages and removes outdated flash messages. + * This method should only be called once in [[init()]]. + */ + protected function updateFlashCounters() + { + $counters = $this->get($this->flashVar, array()); + if (is_array($counters)) { + foreach ($counters as $key => $count) { + if ($count) { + unset($counters[$key], $_SESSION[$key]); + } else { + $counters[$key]++; + } + } + $_SESSION[$this->flashVar] = $counters; + } else { + // fix the unexpected problem that flashVar doesn't return an array + unset($_SESSION[$this->flashVar]); + } + } + + /** + * Returns a flash message. + * A flash message is available only in the current request and the next request. + * @param string $key the key identifying the flash message + * @param mixed $defaultValue value to be returned if the flash message does not exist. + * @return mixed the flash message + */ + public function getFlash($key, $defaultValue = null) + { + $counters = $this->get($this->flashVar, array()); + return isset($counters[$key]) ? $this->get($key, $defaultValue) : $defaultValue; + } + + /** + * Returns all flash messages. + * @return array flash messages (key => message). + */ + public function getAllFlashes() + { + $counters = $this->get($this->flashVar, array()); + $flashes = array(); + foreach (array_keys($counters) as $key) { + if (isset($_SESSION[$key])) { + $flashes[$key] = $_SESSION[$key]; + } + } + return $flashes; + } + + /** + * Stores a flash message. + * A flash message is available only in the current request and the next request. + * @param string $key the key identifying the flash message. Note that flash messages + * and normal session variables share the same name space. If you have a normal + * session variable using the same name, its value will be overwritten by this method. + * @param mixed $value flash message + */ + public function setFlash($key, $value) + { + $counters = $this->get($this->flashVar, array()); + $counters[$key] = 0; + $_SESSION[$key] = $value; + $_SESSION[$this->flashVar] = $counters; + } + + /** + * Removes a flash message. + * Note that flash messages will be automatically removed after the next request. + * @param string $key the key identifying the flash message. Note that flash messages + * and normal session variables share the same name space. If you have a normal + * session variable using the same name, it will be removed by this method. + * @return mixed the removed flash message. Null if the flash message does not exist. + */ + public function removeFlash($key) + { + $counters = $this->get($this->flashVar, array()); + $value = isset($_SESSION[$key], $counters[$key]) ? $_SESSION[$key] : null; + unset($counters[$key], $_SESSION[$key]); + $_SESSION[$this->flashVar] = $counters; + return $value; + } + + /** + * Removes all flash messages. + * Note that flash messages and normal session variables share the same name space. + * If you have a normal session variable using the same name, it will be removed + * by this method. + */ + public function removeAllFlashes() + { + $counters = $this->get($this->flashVar, array()); + foreach (array_keys($counters) as $key) { + unset($_SESSION[$key]); + } + unset($_SESSION[$this->flashVar]); + } + + /** + * Returns a value indicating whether there is a flash message associated with the specified key. + * @param string $key key identifying the flash message + * @return boolean whether the specified flash message exists + */ + public function hasFlash($key) + { + return $this->getFlash($key) !== null; + } + + /** * This method is required by the interface ArrayAccess. * @param mixed $offset the offset to check on * @return boolean diff --git a/framework/web/UrlManager.php b/framework/web/UrlManager.php index e736cc6..459e8e8 100644 --- a/framework/web/UrlManager.php +++ b/framework/web/UrlManager.php @@ -9,6 +9,7 @@ namespace yii\web; use Yii; use yii\base\Component; +use yii\caching\Cache; /** * UrlManager handles HTTP request parsing and creation of URLs based on a set of rules. @@ -49,11 +50,14 @@ class UrlManager extends Component */ public $routeVar = 'r'; /** - * @var string the ID of the cache component that is used to cache the parsed URL rules. - * Defaults to 'cache' which refers to the primary cache component registered with the application. - * Set this property to false if you do not want to cache the URL rules. + * @var Cache|string the cache object or the application component ID of the cache object. + * Compiled URL rules will be cached through this cache object, if it is available. + * + * After the UrlManager object is created, if you want to change this property, + * you should only assign it with a cache object. + * Set this property to null if you do not want to cache the URL rules. */ - public $cacheID = 'cache'; + public $cache = 'cache'; /** * @var string the default class name for creating URL rule instances * when it is not specified in [[rules]]. @@ -65,11 +69,14 @@ class UrlManager extends Component /** - * Initializes the application component. + * Initializes UrlManager. */ public function init() { parent::init(); + if (is_string($this->cache)) { + $this->cache = Yii::$app->getComponent($this->cache); + } $this->compileRules(); } @@ -81,13 +88,10 @@ class UrlManager extends Component if (!$this->enablePrettyUrl || $this->rules === array()) { return; } - /** - * @var $cache \yii\caching\Cache - */ - if ($this->cacheID !== false && ($cache = Yii::$app->getComponent($this->cacheID)) !== null) { - $key = $cache->buildKey(__CLASS__); + if ($this->cache instanceof Cache) { + $key = $this->cache->buildKey(__CLASS__); $hash = md5(json_encode($this->rules)); - if (($data = $cache->get($key)) !== false && isset($data[1]) && $data[1] === $hash) { + if (($data = $this->cache->get($key)) !== false && isset($data[1]) && $data[1] === $hash) { $this->rules = $data[0]; return; } @@ -100,8 +104,8 @@ class UrlManager extends Component $this->rules[$i] = Yii::createObject($rule); } - if (isset($cache)) { - $cache->set($key, array($this->rules, $hash)); + if ($this->cache instanceof Cache) { + $this->cache->set($key, array($this->rules, $hash)); } } diff --git a/framework/web/User.php b/framework/web/User.php index ba0d37b..4dc2607 100644 --- a/framework/web/User.php +++ b/framework/web/User.php @@ -7,802 +7,444 @@ namespace yii\web; +use Yii; use yii\base\Component; +use yii\base\HttpException; +use yii\base\InvalidConfigException; /** - * CWebUser represents the persistent state for a Web application user. + * User is the class for the "user" application component that manages the user authentication status. * - * CWebUser is used as an application component whose ID is 'user'. - * Therefore, at any place one can access the user state via - * Yii::app()->user. + * In particular, [[User::isGuest]] returns a value indicating whether the current user is a guest or not. + * Through methods [[login()]] and [[logout()]], you can change the user authentication status. * - * CWebUser should be used together with an {@link IUserIdentity identity} - * which implements the actual authentication algorithm. - * - * A typical authentication process using CWebUser is as follows: - *
      - *
    1. The user provides information needed for authentication.
    2. - *
    3. An {@link IUserIdentity identity instance} is created with the user-provided information.
    4. - *
    5. Call {@link IUserIdentity::authenticate} to check if the identity is valid.
    6. - *
    7. If valid, call {@link CWebUser::login} to login the user, and - * Redirect the user browser to {@link returnUrl}.
    8. - *
    9. If not valid, retrieve the error code or message from the identity - * instance and display it.
    10. - *
    - * - * The property {@link id} and {@link name} are both identifiers - * for the user. The former is mainly used internally (e.g. primary key), while - * the latter is for display purpose (e.g. username). The {@link id} property - * is a unique identifier for a user that is persistent - * during the whole user session. It can be a username, or something else, - * depending on the implementation of the {@link IUserIdentity identity class}. - * - * Both {@link id} and {@link name} are persistent during the user session. - * Besides, an identity may have additional persistent data which can - * be accessed by calling {@link getState}. - * Note, when {@link allowAutoLogin cookie-based authentication} is enabled, - * all these persistent data will be stored in cookie. Therefore, do not - * store password or other sensitive data in the persistent storage. Instead, - * you should store them directly in session on the server side if needed. - * - * @property boolean $isGuest Whether the current application user is a guest. - * @property mixed $id The unique identifier for the user. If null, it means the user is a guest. - * @property string $name The user name. If the user is not logged in, this will be {@link guestName}. - * @property string $returnUrl The URL that the user should be redirected to after login. - * @property string $stateKeyPrefix A prefix for the name of the session variables storing user session data. - * @property array $flashes Flash messages (key => message). + * User works with a class implementing the [[Identity]] interface. This class implements + * the actual user authentication logic and is often backed by a user database table. * * @author Qiang Xue * @since 2.0 */ class User extends Component { - const FLASH_KEY_PREFIX = 'Yii.CWebUser.flash.'; - const FLASH_COUNTERS = 'Yii.CWebUser.flashcounters'; - const STATES_VAR = '__states'; - const AUTH_TIMEOUT_VAR = '__timeout'; + const EVENT_BEFORE_LOGIN = 'beforeLogin'; + const EVENT_AFTER_LOGIN = 'afterLogin'; + const EVENT_BEFORE_LOGOUT = 'beforeLogout'; + const EVENT_AFTER_LOGOUT = 'afterLogout'; /** - * @var boolean whether to enable cookie-based login. Defaults to false. + * @var string the class name of the [[identity]] object. */ - public $allowAutoLogin = false; + public $identityClass; /** - * @var string the name for a guest user. Defaults to 'Guest'. - * This is used by {@link getName} when the current user is a guest (not authenticated). + * @var boolean whether to enable cookie-based login. Defaults to false. */ - public $guestName = 'Guest'; + public $enableAutoLogin = false; /** - * @var string|array the URL for login. If using array, the first element should be - * the route to the login action, and the rest name-value pairs are GET parameters - * to construct the login URL (e.g. array('/site/login')). If this property is null, - * a 403 HTTP exception will be raised instead. - * @see CController::createUrl + * @var string|array the URL for login when [[loginRequired()]] is called. + * If an array is given, [[UrlManager::createUrl()]] will be called to create the corresponding URL. + * The first element of the array should be the route to the login action, and the rest of + * the name-value pairs are GET parameters used to construct the login URL. For example, + * + * ~~~ + * array('site/login', 'ref' => 1) + * ~~~ + * + * If this property is null, a 403 HTTP exception will be raised when [[loginRequired()]] is called. */ - public $loginUrl = array('/site/login'); + public $loginUrl = array('site/login'); /** - * @var array the property values (in name-value pairs) used to initialize the identity cookie. - * Any property of {@link CHttpCookie} may be initialized. - * This property is effective only when {@link allowAutoLogin} is true. + * @var array the configuration of the identity cookie. This property is used only when [[enableAutoLogin]] is true. + * @see Cookie */ - public $identityCookie; + public $identityCookie = array('name' => '__identity', 'httponly' => true); /** - * @var integer timeout in seconds after which user is logged out if inactive. - * If this property is not set, the user will be logged out after the current session expires - * (c.f. {@link CHttpSession::timeout}). - * @since 1.1.7 + * @var integer the number of seconds in which the user will be logged out automatically if he + * remains inactive. If this property is not set, the user will be logged out after + * the current session expires (c.f. [[Session::timeout]]). */ public $authTimeout; /** * @var boolean whether to automatically renew the identity cookie each time a page is requested. - * Defaults to false. This property is effective only when {@link allowAutoLogin} is true. + * This property is effective only when [[enableAutoLogin]] is true. * When this is false, the identity cookie will expire after the specified duration since the user * is initially logged in. When this is true, the identity cookie will expire after the specified duration * since the user visits the site the last time. - * @see allowAutoLogin - * @since 1.1.0 + * @see enableAutoLogin */ - public $autoRenewCookie = false; + public $autoRenewCookie = true; /** - * @var boolean whether to automatically update the validity of flash messages. - * Defaults to true, meaning flash messages will be valid only in the current and the next requests. - * If this is set false, you will be responsible for ensuring a flash message is deleted after usage. - * (This can be achieved by calling {@link getFlash} with the 3rd parameter being true). - * @since 1.1.7 + * @var string the session variable name used to store the value of [[id]]. */ - public $autoUpdateFlash = true; + public $idVar = '__id'; /** - * @var string value that will be echoed in case that user session has expired during an ajax call. - * When a request is made and user session has expired, {@link loginRequired} redirects to {@link loginUrl} for login. - * If that happens during an ajax call, the complete HTML login page is returned as the result of that ajax call. That could be - * a problem if the ajax call expects the result to be a json array or a predefined string, as the login page is ignored in that case. - * To solve this, set this property to the desired return value. - * - * If this property is set, this value will be returned as the result of the ajax call in case that the user session has expired. - * @since 1.1.9 - * @see loginRequired + * @var string the session variable name used to store the value of expiration timestamp of the authenticated state. + * This is used when [[authTimeout]] is set. */ - public $loginRequiredAjaxResponse; - - private $_keyPrefix; - private $_access = array(); - + public $authTimeoutVar = '__expire'; /** - * PHP magic method. - * This method is overriden so that persistent states can be accessed like properties. - * @param string $name property name - * @return mixed property value + * @var string the session variable name used to store the value of [[returnUrl]]. */ - public function __get($name) - { - if ($this->hasState($name)) { - return $this->getState($name); - } else { - return parent::__get($name); - } - } + public $returnUrlVar = '__returnUrl'; + /** - * PHP magic method. - * This method is overriden so that persistent states can be set like properties. - * @param string $name property name - * @param mixed $value property value + * Initializes the application component. */ - public function __set($name, $value) + public function init() { - if ($this->hasState($name)) { - $this->setState($name, $value); - } else { - parent::__set($name, $value); + parent::init(); + + if ($this->identityClass === null) { + throw new InvalidConfigException('User::identityClass must be set.'); + } + if ($this->enableAutoLogin && !isset($this->identityCookie['name'])) { + throw new InvalidConfigException('User::identityCookie must contain the "name" element.'); } - } - /** - * PHP magic method. - * This method is overriden so that persistent states can also be checked for null value. - * @param string $name property name - * @return boolean - */ - public function __isset($name) - { - if ($this->hasState($name)) { - return $this->getState($name) !== null; - } else { - return parent::__isset($name); + Yii::$app->getSession()->open(); + + $this->renewAuthStatus(); + + if ($this->enableAutoLogin) { + if ($this->getIsGuest()) { + $this->loginByCookie(); + } elseif ($this->autoRenewCookie) { + $this->renewIdentityCookie(); + } } } + private $_identity = false; + /** - * PHP magic method. - * This method is overriden so that persistent states can also be unset. - * @param string $name property name - * @throws CException if the property is read only. + * Returns the identity object associated with the currently logged user. + * @return Identity the identity object associated with the currently logged user. + * Null is returned if the user is not logged in (not authenticated). + * @see login + * @see logout */ - public function __unset($name) + public function getIdentity() { - if ($this->hasState($name)) { - $this->setState($name, null); - } else { - parent::__unset($name); + if ($this->_identity === false) { + $id = $this->getId(); + if ($id === null) { + $this->_identity = null; + } else { + /** @var $class Identity */ + $class = $this->identityClass; + $this->_identity = $class::findIdentity($id); + } } + return $this->_identity; } /** - * Initializes the application component. - * This method overrides the parent implementation by starting session, - * performing cookie-based authentication if enabled, and updating the flash variables. + * Sets the identity object. + * This method should be mainly be used by the User component or its child class + * to maintain the identity object. + * + * You should normally update the user identity via methods [[login()]], [[logout()]] + * or [[switchIdentity()]]. + * + * @param Identity $identity the identity object associated with the currently logged user. */ - public function init() + public function setIdentity($identity) { - parent::init(); - Yii::app()->getSession()->open(); - if ($this->getIsGuest() && $this->allowAutoLogin) { - $this->restoreFromCookie(); - } elseif ($this->autoRenewCookie && $this->allowAutoLogin) { - $this->renewCookie(); - } - if ($this->autoUpdateFlash) { - $this->updateFlash(); - } - - $this->updateAuthStatus(); + $this->_identity = $identity; } /** * Logs in a user. * - * The user identity information will be saved in storage that is - * persistent during the user session. By default, the storage is simply - * the session storage. If the duration parameter is greater than 0, - * a cookie will be sent to prepare for cookie-based login in future. + * This method stores the necessary session information to keep track + * of the user identity information. If `$duration` is greater than 0 + * and [[enableAutoLogin]] is true, it will also send out an identity + * cookie to support cookie-based login. * - * Note, you have to set {@link allowAutoLogin} to true - * if you want to allow user to be authenticated based on the cookie information. - * - * @param IUserIdentity $identity the user identity (which should already be authenticated) - * @param integer $duration number of seconds that the user can remain in logged-in status. Defaults to 0, meaning login till the user closes the browser. - * If greater than 0, cookie-based login will be used. In this case, {@link allowAutoLogin} - * must be set true, otherwise an exception will be thrown. + * @param Identity $identity the user identity (which should already be authenticated) + * @param integer $duration number of seconds that the user can remain in logged-in status. + * Defaults to 0, meaning login till the user closes the browser or the session is manually destroyed. + * If greater than 0 and [[enableAutoLogin]] is true, cookie-based login will be supported. * @return boolean whether the user is logged in */ public function login($identity, $duration = 0) { - $id = $identity->getId(); - $states = $identity->getPersistentStates(); - if ($this->beforeLogin($id, $states, false)) { - $this->changeIdentity($id, $identity->getName(), $states); + if ($this->beforeLogin($identity, false)) { + $this->switchIdentity($identity, $duration); + $this->afterLogin($identity, false); + } + return !$this->getIsGuest(); + } - if ($duration > 0) { - if ($this->allowAutoLogin) { - $this->saveToCookie($duration); - } else { - throw new CException(Yii::t('yii', '{class}.allowAutoLogin must be set true in order to use cookie-based authentication.', - array('{class}' => get_class($this)))); + /** + * Logs in a user by cookie. + * + * This method attempts to log in a user using the ID and authKey information + * provided by the given cookie. + */ + protected function loginByCookie() + { + $name = $this->identityCookie['name']; + $value = Yii::$app->getRequest()->getCookies()->getValue($name); + if ($value !== null) { + $data = json_decode($value, true); + if (count($data) === 3 && isset($data[0], $data[1], $data[2])) { + list ($id, $authKey, $duration) = $data; + /** @var $class Identity */ + $class = $this->identityClass; + $identity = $class::findIdentity($id); + if ($identity !== null && $identity->validateAuthKey($authKey)) { + if ($this->beforeLogin($identity, true)) { + $this->switchIdentity($identity, $this->autoRenewCookie ? $duration : 0); + $this->afterLogin($identity, true); + } + } elseif ($identity !== null) { + Yii::warning("Invalid auth key attempted for user '$id': $authKey", __METHOD__); } } - - $this->afterLogin(false); } - return !$this->getIsGuest(); } /** * Logs out the current user. * This will remove authentication-related session data. - * If the parameter is true, the whole session will be destroyed as well. - * @param boolean $destroySession whether to destroy the whole session. Defaults to true. If false, - * then {@link clearStates} will be called, which removes only the data stored via {@link setState}. + * If `$destroySession` is true, all session data will be removed. + * @param boolean $destroySession whether to destroy the whole session. Defaults to true. */ public function logout($destroySession = true) { - if ($this->beforeLogout()) { - if ($this->allowAutoLogin) { - Yii::app()->getRequest()->getCookies()->remove($this->getStateKeyPrefix()); - if ($this->identityCookie !== null) { - $cookie = $this->createIdentityCookie($this->getStateKeyPrefix()); - $cookie->value = null; - $cookie->expire = 0; - Yii::app()->getRequest()->getCookies()->add($cookie->name, $cookie); - } - } + $identity = $this->getIdentity(); + if ($identity !== null && $this->beforeLogout($identity)) { + $this->switchIdentity(null); if ($destroySession) { - Yii::app()->getSession()->destroy(); - } else { - $this->clearStates(); + Yii::$app->getSession()->destroy(); } - $this->_access = array(); - $this->afterLogout(); + $this->afterLogout($identity); } } /** * Returns a value indicating whether the user is a guest (not authenticated). - * @return boolean whether the current application user is a guest. + * @return boolean whether the current user is a guest. */ public function getIsGuest() { - return $this->getState('__id') === null; + return $this->getIdentity() === null; } /** * Returns a value that uniquely represents the user. - * @return mixed the unique identifier for the user. If null, it means the user is a guest. + * @return string|integer the unique identifier for the user. If null, it means the user is a guest. */ public function getId() { - return $this->getState('__id'); - } - - /** - * @param mixed $value the unique identifier for the user. If null, it means the user is a guest. - */ - public function setId($value) - { - $this->setState('__id', $value); - } - - /** - * Returns the unique identifier for the user (e.g. username). - * This is the unique identifier that is mainly used for display purpose. - * @return string the user name. If the user is not logged in, this will be {@link guestName}. - */ - public function getName() - { - if (($name = $this->getState('__name')) !== null) { - return $name; - } else { - return $this->guestName; - } - } - - /** - * Sets the unique identifier for the user (e.g. username). - * @param string $value the user name. - * @see getName - */ - public function setName($value) - { - $this->setState('__name', $value); + return Yii::$app->getSession()->get($this->idVar); } /** * Returns the URL that the user should be redirected to after successful login. * This property is usually used by the login action. If the login is successful, * the action should read this property and use it to redirect the user browser. - * @param string $defaultUrl the default return URL in case it was not set previously. If this is null, - * the application entry URL will be considered as the default return URL. + * @param string|array $defaultUrl the default return URL in case it was not set previously. + * If this is null, it means [[Application::homeUrl]] will be redirected to. + * Please refer to [[\yii\helpers\Html::url()]] on acceptable URL formats. * @return string the URL that the user should be redirected to after login. * @see loginRequired */ public function getReturnUrl($defaultUrl = null) { - if ($defaultUrl === null) { - $defaultReturnUrl = Yii::app()->getUrlManager()->showScriptName ? Yii::app()->getRequest()->getScriptUrl() : Yii::app()->getRequest()->getBaseUrl() . '/'; - } else { - $defaultReturnUrl = CHtml::normalizeUrl($defaultUrl); - } - return $this->getState('__returnUrl', $defaultReturnUrl); + $url = Yii::$app->getSession()->get($this->returnUrlVar, $defaultUrl); + return $url === null ? Yii::$app->getHomeUrl() : $url; } /** - * @param string $value the URL that the user should be redirected to after login. + * @param string|array $url the URL that the user should be redirected to after login. + * Please refer to [[\yii\helpers\Html::url()]] on acceptable URL formats. */ - public function setReturnUrl($value) + public function setReturnUrl($url) { - $this->setState('__returnUrl', $value); + Yii::$app->getSession()->set($this->returnUrlVar, $url); } /** * Redirects the user browser to the login page. * Before the redirection, the current URL (if it's not an AJAX url) will be - * kept in {@link returnUrl} so that the user browser may be redirected back - * to the current page after successful login. Make sure you set {@link loginUrl} + * kept as [[returnUrl]] so that the user browser may be redirected back + * to the current page after successful login. Make sure you set [[loginUrl]] * so that the user browser can be redirected to the specified login URL after * calling this method. * After calling this method, the current request processing will be terminated. */ public function loginRequired() { - $app = Yii::app(); - $request = $app->getRequest(); - + $request = Yii::$app->getRequest(); if (!$request->getIsAjaxRequest()) { $this->setReturnUrl($request->getUrl()); - } elseif (isset($this->loginRequiredAjaxResponse)) { - echo $this->loginRequiredAjaxResponse; - Yii::app()->end(); } - - if (($url = $this->loginUrl) !== null) { - if (is_array($url)) { - $route = isset($url[0]) ? $url[0] : $app->defaultController; - $url = $app->createUrl($route, array_splice($url, 1)); - } - $request->redirect($url); + if ($this->loginUrl !== null) { + Yii::$app->getResponse()->redirect($this->loginUrl); } else { - throw new CHttpException(403, Yii::t('yii', 'Login Required')); + throw new HttpException(403, Yii::t('yii|Login Required')); } } /** * This method is called before logging in a user. - * You may override this method to provide additional security check. - * For example, when the login is cookie-based, you may want to verify - * that the user ID together with a random token in the states can be found - * in the database. This will prevent hackers from faking arbitrary - * identity cookies even if they crack down the server private key. - * @param mixed $id the user ID. This is the same as returned by {@link getId()}. - * @param array $states a set of name-value pairs that are provided by the user identity. - * @param boolean $fromCookie whether the login is based on cookie - * @return boolean whether the user should be logged in - * @since 1.1.3 + * The default implementation will trigger the [[EVENT_BEFORE_LOGIN]] event. + * If you override this method, make sure you call the parent implementation + * so that the event is triggered. + * @param Identity $identity the user identity information + * @param boolean $cookieBased whether the login is cookie-based + * @return boolean whether the user should continue to be logged in */ - protected function beforeLogin($id, $states, $fromCookie) + protected function beforeLogin($identity, $cookieBased) { - return true; + $event = new UserEvent(array( + 'identity' => $identity, + 'cookieBased' => $cookieBased, + )); + $this->trigger(self::EVENT_BEFORE_LOGIN, $event); + return $event->isValid; } /** * This method is called after the user is successfully logged in. - * You may override this method to do some postprocessing (e.g. log the user - * login IP and time; load the user permission information). - * @param boolean $fromCookie whether the login is based on cookie. - * @since 1.1.3 + * The default implementation will trigger the [[EVENT_AFTER_LOGIN]] event. + * If you override this method, make sure you call the parent implementation + * so that the event is triggered. + * @param Identity $identity the user identity information + * @param boolean $cookieBased whether the login is cookie-based */ - protected function afterLogin($fromCookie) + protected function afterLogin($identity, $cookieBased) { + $this->trigger(self::EVENT_AFTER_LOGIN, new UserEvent(array( + 'identity' => $identity, + 'cookieBased' => $cookieBased, + ))); } /** - * This method is invoked when calling {@link logout} to log out a user. - * If this method return false, the logout action will be cancelled. - * You may override this method to provide additional check before - * logging out a user. - * @return boolean whether to log out the user - * @since 1.1.3 + * This method is invoked when calling [[logout()]] to log out a user. + * The default implementation will trigger the [[EVENT_BEFORE_LOGOUT]] event. + * If you override this method, make sure you call the parent implementation + * so that the event is triggered. + * @param Identity $identity the user identity information + * @return boolean whether the user should continue to be logged out */ - protected function beforeLogout() + protected function beforeLogout($identity) { - return true; + $event = new UserEvent(array( + 'identity' => $identity, + )); + $this->trigger(self::EVENT_BEFORE_LOGOUT, $event); + return $event->isValid; } /** - * This method is invoked right after a user is logged out. - * You may override this method to do some extra cleanup work for the user. - * @since 1.1.3 + * This method is invoked right after a user is logged out via [[logout()]]. + * The default implementation will trigger the [[EVENT_AFTER_LOGOUT]] event. + * If you override this method, make sure you call the parent implementation + * so that the event is triggered. + * @param Identity $identity the user identity information */ - protected function afterLogout() + protected function afterLogout($identity) { - } - - /** - * Populates the current user object with the information obtained from cookie. - * This method is used when automatic login ({@link allowAutoLogin}) is enabled. - * The user identity information is recovered from cookie. - * Sufficient security measures are used to prevent cookie data from being tampered. - * @see saveToCookie - */ - protected function restoreFromCookie() - { - $app = Yii::app(); - $request = $app->getRequest(); - $cookie = $request->getCookies()->itemAt($this->getStateKeyPrefix()); - if ($cookie && !empty($cookie->value) && is_string($cookie->value) && ($data = $app->getSecurityManager()->validateData($cookie->value)) !== false) { - $data = @unserialize($data); - if (is_array($data) && isset($data[0], $data[1], $data[2], $data[3])) { - list($id, $name, $duration, $states) = $data; - if ($this->beforeLogin($id, $states, true)) { - $this->changeIdentity($id, $name, $states); - if ($this->autoRenewCookie) { - $cookie->expire = time() + $duration; - $request->getCookies()->add($cookie->name, $cookie); - } - $this->afterLogin(true); - } - } - } + $this->trigger(self::EVENT_AFTER_LOGOUT, new UserEvent(array( + 'identity' => $identity, + ))); } /** * Renews the identity cookie. * This method will set the expiration time of the identity cookie to be the current time * plus the originally specified cookie duration. - * @since 1.1.3 */ - protected function renewCookie() - { - $request = Yii::app()->getRequest(); - $cookies = $request->getCookies(); - $cookie = $cookies->itemAt($this->getStateKeyPrefix()); - if ($cookie && !empty($cookie->value) && ($data = Yii::app()->getSecurityManager()->validateData($cookie->value)) !== false) { - $data = @unserialize($data); - if (is_array($data) && isset($data[0], $data[1], $data[2], $data[3])) { - $cookie->expire = time() + $data[2]; - $cookies->add($cookie->name, $cookie); + protected function renewIdentityCookie() + { + $name = $this->identityCookie['name']; + $value = Yii::$app->getRequest()->getCookies()->getValue($name); + if ($value !== null) { + $data = json_decode($value, true); + if (is_array($data) && isset($data[2])) { + $cookie = new Cookie($this->identityCookie); + $cookie->value = $value; + $cookie->expire = time() + (int)$data[2]; + Yii::$app->getResponse()->getCookies()->add($cookie); } } } /** - * Saves necessary user data into a cookie. - * This method is used when automatic login ({@link allowAutoLogin}) is enabled. - * This method saves user ID, username, other identity states and a validation key to cookie. - * These information are used to do authentication next time when user visits the application. - * @param integer $duration number of seconds that the user can remain in logged-in status. Defaults to 0, meaning login till the user closes the browser. - * @see restoreFromCookie + * Sends an identity cookie. + * This method is used when [[enableAutoLogin]] is true. + * It saves [[id]], [[Identity::getAuthKey()|auth key]], and the duration of cookie-based login + * information in the cookie. + * @param Identity $identity + * @param integer $duration number of seconds that the user can remain in logged-in status. + * @see loginByCookie */ - protected function saveToCookie($duration) + protected function sendIdentityCookie($identity, $duration) { - $app = Yii::app(); - $cookie = $this->createIdentityCookie($this->getStateKeyPrefix()); - $cookie->expire = time() + $duration; - $data = array( - $this->getId(), - $this->getName(), + $cookie = new Cookie($this->identityCookie); + $cookie->value = json_encode(array( + $identity->getId(), + $identity->getAuthKey(), $duration, - $this->saveIdentityStates(), - ); - $cookie->value = $app->getSecurityManager()->hashData(serialize($data)); - $app->getRequest()->getCookies()->add($cookie->name, $cookie); - } - - /** - * Creates a cookie to store identity information. - * @param string $name the cookie name - * @return CHttpCookie the cookie used to store identity information - */ - protected function createIdentityCookie($name) - { - $cookie = new CHttpCookie($name, ''); - if (is_array($this->identityCookie)) { - foreach ($this->identityCookie as $name => $value) { - $cookie->$name = $value; - } - } - return $cookie; - } - - /** - * @return string a prefix for the name of the session variables storing user session data. - */ - public function getStateKeyPrefix() - { - if ($this->_keyPrefix !== null) { - return $this->_keyPrefix; - } else { - return $this->_keyPrefix = md5('Yii.' . get_class($this) . '.' . Yii::app()->getId()); - } - } - - /** - * @param string $value a prefix for the name of the session variables storing user session data. - */ - public function setStateKeyPrefix($value) - { - $this->_keyPrefix = $value; + )); + $cookie->expire = time() + $duration; + Yii::$app->getResponse()->getCookies()->add($cookie); } /** - * Returns the value of a variable that is stored in user session. + * Switches to a new identity for the current user. * - * This function is designed to be used by CWebUser descendant classes - * who want to store additional user information in user session. - * A variable, if stored in user session using {@link setState} can be - * retrieved back using this function. + * This method will save necessary session information to keep track of the user authentication status. + * If `$duration` is provided, it will also send out appropriate identity cookie + * to support cookie-based login. * - * @param string $key variable name - * @param mixed $defaultValue default value - * @return mixed the value of the variable. If it doesn't exist in the session, - * the provided default value will be returned - * @see setState - */ - public function getState($key, $defaultValue = null) - { - $key = $this->getStateKeyPrefix() . $key; - return isset($_SESSION[$key]) ? $_SESSION[$key] : $defaultValue; - } - - /** - * Stores a variable in user session. - * - * This function is designed to be used by CWebUser descendant classes - * who want to store additional user information in user session. - * By storing a variable using this function, the variable may be retrieved - * back later using {@link getState}. The variable will be persistent - * across page requests during a user session. + * This method is mainly called by [[login()]], [[logout()]] and [[loginByCookie()]] + * when the current user needs to be associated with the corresponding identity information. * - * @param string $key variable name - * @param mixed $value variable value - * @param mixed $defaultValue default value. If $value===$defaultValue, the variable will be - * removed from the session - * @see getState - */ - public function setState($key, $value, $defaultValue = null) - { - $key = $this->getStateKeyPrefix() . $key; - if ($value === $defaultValue) { - unset($_SESSION[$key]); - } else { - $_SESSION[$key] = $value; - } - } - - /** - * Returns a value indicating whether there is a state of the specified name. - * @param string $key state name - * @return boolean whether there is a state of the specified name. - */ - public function hasState($key) - { - $key = $this->getStateKeyPrefix() . $key; - return isset($_SESSION[$key]); - } - - /** - * Clears all user identity information from persistent storage. - * This will remove the data stored via {@link setState}. - */ - public function clearStates() - { - $keys = array_keys($_SESSION); - $prefix = $this->getStateKeyPrefix(); - $n = strlen($prefix); - foreach ($keys as $key) { - if (!strncmp($key, $prefix, $n)) { - unset($_SESSION[$key]); - } - } - } - - /** - * Returns all flash messages. - * This method is similar to {@link getFlash} except that it returns all - * currently available flash messages. - * @param boolean $delete whether to delete the flash messages after calling this method. - * @return array flash messages (key => message). - * @since 1.1.3 - */ - public function getFlashes($delete = true) - { - $flashes = array(); - $prefix = $this->getStateKeyPrefix() . self::FLASH_KEY_PREFIX; - $keys = array_keys($_SESSION); - $n = strlen($prefix); - foreach ($keys as $key) { - if (!strncmp($key, $prefix, $n)) { - $flashes[substr($key, $n)] = $_SESSION[$key]; - if ($delete) { - unset($_SESSION[$key]); - } - } - } - if ($delete) { - $this->setState(self::FLASH_COUNTERS, array()); - } - return $flashes; - } - - /** - * Returns a flash message. - * A flash message is available only in the current and the next requests. - * @param string $key key identifying the flash message - * @param mixed $defaultValue value to be returned if the flash message is not available. - * @param boolean $delete whether to delete this flash message after accessing it. - * Defaults to true. - * @return mixed the message message - */ - public function getFlash($key, $defaultValue = null, $delete = true) - { - $value = $this->getState(self::FLASH_KEY_PREFIX . $key, $defaultValue); - if ($delete) { - $this->setFlash($key, null); - } - return $value; - } - - /** - * Stores a flash message. - * A flash message is available only in the current and the next requests. - * @param string $key key identifying the flash message - * @param mixed $value flash message - * @param mixed $defaultValue if this value is the same as the flash message, the flash message - * will be removed. (Therefore, you can use setFlash('key',null) to remove a flash message.) - */ - public function setFlash($key, $value, $defaultValue = null) - { - $this->setState(self::FLASH_KEY_PREFIX . $key, $value, $defaultValue); - $counters = $this->getState(self::FLASH_COUNTERS, array()); - if ($value === $defaultValue) { - unset($counters[$key]); - } else { - $counters[$key] = 0; - } - $this->setState(self::FLASH_COUNTERS, $counters, array()); - } - - /** - * @param string $key key identifying the flash message - * @return boolean whether the specified flash message exists - */ - public function hasFlash($key) - { - return $this->getFlash($key, null, false) !== null; - } - - /** - * Changes the current user with the specified identity information. - * This method is called by {@link login} and {@link restoreFromCookie} - * when the current user needs to be populated with the corresponding - * identity information. Derived classes may override this method - * by retrieving additional user-related information. Make sure the - * parent implementation is called first. - * @param mixed $id a unique identifier for the user - * @param string $name the display name for the user - * @param array $states identity states - */ - protected function changeIdentity($id, $name, $states) - { - Yii::app()->getSession()->regenerateID(true); - $this->setId($id); - $this->setName($name); - $this->loadIdentityStates($states); - } - - /** - * Retrieves identity states from persistent storage and saves them as an array. - * @return array the identity states - */ - protected function saveIdentityStates() - { - $states = array(); - foreach ($this->getState(self::STATES_VAR, array()) as $name => $dummy) { - $states[$name] = $this->getState($name); - } - return $states; - } - - /** - * Loads identity states from an array and saves them to persistent storage. - * @param array $states the identity states - */ - protected function loadIdentityStates($states) - { - $names = array(); - if (is_array($states)) { - foreach ($states as $name => $value) { - $this->setState($name, $value); - $names[$name] = true; + * @param Identity $identity the identity information to be associated with the current user. + * If null, it means switching to be a guest. + * @param integer $duration number of seconds that the user can remain in logged-in status. + * This parameter is used only when `$identity` is not null. + */ + public function switchIdentity($identity, $duration = 0) + { + $session = Yii::$app->getSession(); + $session->regenerateID(true); + $this->setIdentity($identity); + $session->remove($this->idVar); + $session->remove($this->authTimeoutVar); + if ($identity instanceof Identity) { + $session->set($this->idVar, $identity->getId()); + if ($this->authTimeout !== null) { + $session->set($this->authTimeoutVar, time() + $this->authTimeout); } - } - $this->setState(self::STATES_VAR, $names); - } - - /** - * Updates the internal counters for flash messages. - * This method is internally used by {@link CWebApplication} - * to maintain the availability of flash messages. - */ - protected function updateFlash() - { - $counters = $this->getState(self::FLASH_COUNTERS); - if (!is_array($counters)) { - return; - } - foreach ($counters as $key => $count) { - if ($count) { - unset($counters[$key]); - $this->setState(self::FLASH_KEY_PREFIX . $key, null); - } else { - $counters[$key]++; + if ($duration > 0 && $this->enableAutoLogin) { + $this->sendIdentityCookie($identity, $duration); } + } elseif ($this->enableAutoLogin) { + Yii::$app->getResponse()->getCookies()->remove(new Cookie($this->identityCookie)); } - $this->setState(self::FLASH_COUNTERS, $counters, array()); } /** - * Updates the authentication status according to {@link authTimeout}. - * If the user has been inactive for {@link authTimeout} seconds, - * he will be automatically logged out. - * @since 1.1.7 + * Updates the authentication status according to [[authTimeout]]. + * This method is called during [[init()]]. + * It will update the user's authentication status if it has not outdated yet. + * Otherwise, it will logout the user. */ - protected function updateAuthStatus() + protected function renewAuthStatus() { if ($this->authTimeout !== null && !$this->getIsGuest()) { - $expires = $this->getState(self::AUTH_TIMEOUT_VAR); - if ($expires !== null && $expires < time()) { + $expire = Yii::$app->getSession()->get($this->authTimeoutVar); + if ($expire !== null && $expire < time()) { $this->logout(false); } else { - $this->setState(self::AUTH_TIMEOUT_VAR, time() + $this->authTimeout); + Yii::$app->getSession()->set($this->authTimeoutVar, time() + $this->authTimeout); } } } - - /** - * Performs access check for this user. - * @param string $operation the name of the operation that need access check. - * @param array $params name-value pairs that would be passed to business rules associated - * with the tasks and roles assigned to the user. - * Since version 1.1.11 a param with name 'userId' is added to this array, which holds the value of - * {@link getId()} when {@link CDbAuthManager} or {@link CPhpAuthManager} is used. - * @param boolean $allowCaching whether to allow caching the result of access check. - * When this parameter - * is true (default), if the access check of an operation was performed before, - * its result will be directly returned when calling this method to check the same operation. - * If this parameter is false, this method will always call {@link CAuthManager::checkAccess} - * to obtain the up-to-date access result. Note that this caching is effective - * only within the same request and only works when $params=array(). - * @return boolean whether the operations can be performed by this user. - */ - public function checkAccess($operation, $params = array(), $allowCaching = true) - { - if ($allowCaching && $params === array() && isset($this->_access[$operation])) { - return $this->_access[$operation]; - } - - $access = Yii::app()->getAuthManager()->checkAccess($operation, $this->getId(), $params); - if ($allowCaching && $params === array()) { - $this->_access[$operation] = $access; - } - - return $access; - } } diff --git a/framework/web/UserEvent.php b/framework/web/UserEvent.php new file mode 100644 index 0000000..7a5d23d --- /dev/null +++ b/framework/web/UserEvent.php @@ -0,0 +1,34 @@ + + * @since 2.0 + */ +class UserEvent extends Event +{ + /** + * @var Identity the identity object associated with this event + */ + public $identity; + /** + * @var boolean whether the login is cookie-based. This property is only meaningful + * for [[User::EVENT_BEFORE_LOGIN]] and [[User::EVENT_AFTER_LOGIN]] events. + */ + public $cookieBased; + /** + * @var boolean whether the login or logout should proceed. + * Event handlers may modify this property to determine whether the login or logout should proceed. + * This property is only meaningful for [[User::EVENT_BEFORE_LOGIN]] and [[User::EVENT_BEFORE_LOGOUT]] events. + */ + public $isValid = true; +} \ No newline at end of file diff --git a/framework/widgets/ActiveForm.php b/framework/widgets/ActiveForm.php index 2c965e7..8ac5365 100644 --- a/framework/widgets/ActiveForm.php +++ b/framework/widgets/ActiveForm.php @@ -110,8 +110,7 @@ class ActiveForm extends Widget */ public function error($model, $attribute, $options = array()) { - $attribute = $this->normalizeAttributeName($attribute); - $this->getInputName($model, $attribute); + $attribute = $this->getAttributeName($attribute); $tag = isset($options['tag']) ? $options['tag'] : 'div'; unset($options['tag']); $error = $model->getFirstError($attribute); @@ -126,15 +125,19 @@ class ActiveForm extends Widget */ public function label($model, $attribute, $options = array()) { - $attribute = $this->normalizeAttributeName($attribute); - $label = $model->getAttributeLabel($attribute); - return Html::label(Html::encode($label), isset($options['for']) ? $options['for'] : null, $options); + $attribute = $this->getAttributeName($attribute); + $label = isset($options['label']) ? $options['label'] : Html::encode($model->getAttributeLabel($attribute)); + $for = array_key_exists('for', $options) ? $options['for'] : $this->getInputId($model, $attribute); + return Html::label($label, $for, $options); } public function input($type, $model, $attribute, $options = array()) { $value = $this->getAttributeValue($model, $attribute); $name = $this->getInputName($model, $attribute); + if (!array_key_exists('id', $options)) { + $options['id'] = $this->getInputId($model, $attribute); + } return Html::input($type, $name, $value, $options); } @@ -162,6 +165,9 @@ class ActiveForm extends Widget { $value = $this->getAttributeValue($model, $attribute); $name = $this->getInputName($model, $attribute); + if (!array_key_exists('id', $options)) { + $options['id'] = $this->getInputId($model, $attribute); + } return Html::textarea($name, $value, $options); } @@ -172,6 +178,9 @@ class ActiveForm extends Widget if (!array_key_exists('uncheck', $options)) { $options['unchecked'] = '0'; } + if (!array_key_exists('id', $options)) { + $options['id'] = $this->getInputId($model, $attribute); + } return Html::radio($name, $checked, $value, $options); } @@ -182,6 +191,9 @@ class ActiveForm extends Widget if (!array_key_exists('uncheck', $options)) { $options['unchecked'] = '0'; } + if (!array_key_exists('id', $options)) { + $options['id'] = $this->getInputId($model, $attribute); + } return Html::checkbox($name, $checked, $value, $options); } @@ -189,6 +201,9 @@ class ActiveForm extends Widget { $checked = $this->getAttributeValue($model, $attribute); $name = $this->getInputName($model, $attribute); + if (!array_key_exists('id', $options)) { + $options['id'] = $this->getInputId($model, $attribute); + } return Html::dropDownList($name, $checked, $items, $options); } @@ -199,6 +214,9 @@ class ActiveForm extends Widget if (!array_key_exists('unselect', $options)) { $options['unselect'] = '0'; } + if (!array_key_exists('id', $options)) { + $options['id'] = $this->getInputId($model, $attribute); + } return Html::listBox($name, $checked, $items, $options); } @@ -228,7 +246,7 @@ class ActiveForm extends Widget if (isset($this->modelMap[$class])) { $class = $this->modelMap[$class]; } elseif (($pos = strrpos($class, '\\')) !== false) { - $class = substr($class, $pos); + $class = substr($class, $pos + 1); } if (!preg_match('/(^|.*\])(\w+)(\[.*|$)/', $attribute, $matches)) { throw new InvalidParamException('Attribute name must contain word characters only.'); @@ -245,6 +263,12 @@ class ActiveForm extends Widget } } + public function getInputId($model, $attribute) + { + $name = $this->getInputName($model, $attribute); + return str_replace(array('[]', '][', '[', ']', ' '), array('', '-', '-', '', '-'), $name); + } + public function getAttributeValue($model, $attribute) { if (!preg_match('/(^|.*\])(\w+)(\[.*|$)/', $attribute, $matches)) { @@ -267,7 +291,7 @@ class ActiveForm extends Widget } } - public function normalizeAttributeName($attribute) + public function getAttributeName($attribute) { if (preg_match('/(^|.*\])(\w+)(\[.*|$)/', $attribute, $matches)) { return $matches[2]; diff --git a/framework/widgets/Clip.php b/framework/widgets/Clip.php index d540b24..f321209 100644 --- a/framework/widgets/Clip.php +++ b/framework/widgets/Clip.php @@ -22,11 +22,6 @@ class Clip extends Widget */ public $id; /** - * @var View the view object for keeping the clip. If not set, the view registered with the application - * will be used. - */ - public $view; - /** * @var boolean whether to render the clip content in place. Defaults to false, * meaning the captured clip will not be displayed. */ @@ -51,7 +46,6 @@ class Clip extends Widget if ($this->renderClip) { echo $clip; } - $view = $this->view !== null ? $this->view : Yii::$app->getView(); - $view->clips[$this->id] = $clip; + $this->view->clips[$this->id] = $clip; } } \ No newline at end of file diff --git a/framework/widgets/ContentDecorator.php b/framework/widgets/ContentDecorator.php index 4c3ae70..3f63621 100644 --- a/framework/widgets/ContentDecorator.php +++ b/framework/widgets/ContentDecorator.php @@ -7,10 +7,8 @@ namespace yii\widgets; -use Yii; use yii\base\InvalidConfigException; use yii\base\Widget; -use yii\base\View; /** * @author Qiang Xue @@ -19,15 +17,10 @@ use yii\base\View; class ContentDecorator extends Widget { /** - * @var View the view object for rendering [[viewName]]. If not set, the view registered with the application - * will be used. + * @var string the view file that will be used to decorate the content enclosed by this widget. + * This can be specified as either the view file path or path alias. */ - public $view; - /** - * @var string the name of the view that will be used to decorate the content enclosed by this widget. - * Please refer to [[View::findViewFile()]] on how to set this property. - */ - public $viewName; + public $viewFile; /** * @var array the parameters (name=>value) to be extracted and made available in the decorative view. */ @@ -38,8 +31,8 @@ class ContentDecorator extends Widget */ public function init() { - if ($this->viewName === null) { - throw new InvalidConfigException('ContentDecorator::viewName must be set.'); + if ($this->viewFile === null) { + throw new InvalidConfigException('ContentDecorator::viewFile must be set.'); } ob_start(); ob_implicit_flush(false); @@ -53,7 +46,7 @@ class ContentDecorator extends Widget { $params = $this->params; $params['content'] = ob_get_clean(); - $view = $this->view !== null ? $this->view : Yii::$app->getView(); - echo $view->render($this->viewName, $params); + // render under the existing context + echo $this->view->renderFile($this->viewFile, $params); } } diff --git a/framework/widgets/FragmentCache.php b/framework/widgets/FragmentCache.php index d5185f8..637d115 100644 --- a/framework/widgets/FragmentCache.php +++ b/framework/widgets/FragmentCache.php @@ -1,213 +1,174 @@ - - * @since 2.0 - */ -class FragmentCache extends Widget -{ - /** - * @var string the ID of the cache application component. Defaults to 'cache' (the primary cache application component.) - */ - public $cacheID = 'cache'; - /** - * @var integer number of seconds that the data can remain valid in cache. - * Use 0 to indicate that the cached data will never expire. - */ - public $duration = 60; - /** - * @var array|Dependency the dependency that the cached content depends on. - * This can be either a [[Dependency]] object or a configuration array for creating the dependency object. - * For example, - * - * ~~~ - * array( - * 'class' => 'yii\caching\DbDependency', - * 'sql' => 'SELECT MAX(lastModified) FROM Post', - * ) - * ~~~ - * - * would make the output cache depends on the last modified time of all posts. - * If any post has its modification time changed, the cached content would be invalidated. - */ - public $dependency; - /** - * @var array list of factors that would cause the variation of the content being cached. - * Each factor is a string representing a variation (e.g. the language, a GET parameter). - * The following variation setting will cause the content to be cached in different versions - * according to the current application language: - * - * ~~~ - * array( - * Yii::$app->language, - * ) - */ - public $variations; - /** - * @var boolean whether to enable the fragment cache. You may use this property to turn on and off - * the fragment cache according to specific setting (e.g. enable fragment cache only for GET requests). - */ - public $enabled = true; - /** - * @var \yii\base\View the view object within which this widget is used. If not set, - * the view registered with the application will be used. This is mainly used by dynamic content feature. - */ - public $view; - /** - * @var array a list of placeholders for embedding dynamic contents. This property - * is used internally to implement the content caching feature. Do not modify it. - */ - public $dynamicPlaceholders; - - - /** - * Marks the start of content to be cached. - * Content displayed after this method call and before {@link endCache()} - * will be captured and saved in cache. - * This method does nothing if valid content is already found in cache. - */ - public function init() - { - if ($this->view === null) { - $this->view = Yii::$app->getView(); - } - if ($this->getCache() !== null && $this->getCachedContent() === false) { - $this->view->cacheStack[] = $this; - ob_start(); - ob_implicit_flush(false); - } - } - - /** - * Marks the end of content to be cached. - * Content displayed before this method call and after {@link init()} - * will be captured and saved in cache. - * This method does nothing if valid content is already found in cache. - */ - public function run() - { - if (($content = $this->getCachedContent()) !== false) { - echo $content; - } elseif (($cache = $this->getCache()) !== null) { - $content = ob_get_clean(); - array_pop($this->view->cacheStack); - if (is_array($this->dependency)) { - $this->dependency = Yii::createObject($this->dependency); - } - $data = array($content, $this->dynamicPlaceholders); - $cache->set($this->calculateKey(), $data, $this->duration, $this->dependency); - - if ($this->view->cacheStack === array() && !empty($this->dynamicPlaceholders)) { - $content = $this->updateDynamicContent($content, $this->dynamicPlaceholders); - } - echo $content; - } - } - - /** - * @var string|boolean the cached content. False if the content is not cached. - */ - private $_content; - - /** - * Returns the cached content if available. - * @return string|boolean the cached content. False is returned if valid content is not found in the cache. - */ - public function getCachedContent() - { - if ($this->_content === null) { - $this->_content = false; - if (($cache = $this->getCache()) !== null) { - $key = $this->calculateKey(); - $data = $cache->get($key); - if (is_array($data) && count($data) === 2) { - list ($content, $placeholders) = $data; - if (is_array($placeholders) && count($placeholders) > 0) { - if ($this->view->cacheStack === array()) { - // outermost cache: replace placeholder with dynamic content - $content = $this->updateDynamicContent($content, $placeholders); - } - foreach ($placeholders as $name => $statements) { - $this->view->addDynamicPlaceholder($name, $statements); - } - } - $this->_content = $content; - } - } - } - return $this->_content; - } - - protected function updateDynamicContent($content, $placeholders) - { - foreach ($placeholders as $name => $statements) { - $placeholders[$name] = $this->view->evaluateDynamicContent($statements); - } - return strtr($content, $placeholders); - } - - /** - * Generates a unique key used for storing the content in cache. - * The key generated depends on both [[id]] and [[variations]]. - * @return string a valid cache key - */ - protected function calculateKey() - { - $factors = array(__CLASS__, $this->getId()); - if (is_array($this->variations)) { - foreach ($this->variations as $factor) { - $factors[] = $factor; - } - } - return $this->getCache()->buildKey($factors); - } - - /** - * @var Cache - */ - private $_cache; - - /** - * Returns the cache instance used for storing content. - * @return Cache the cache instance. Null is returned if the cache component is not available - * or [[enabled]] is false. - * @throws InvalidConfigException if [[cacheID]] does not point to a valid application component. - */ - public function getCache() - { - if (!$this->enabled) { - return null; - } - if ($this->_cache === null) { - $cache = Yii::$app->getComponent($this->cacheID); - if ($cache instanceof Cache) { - $this->_cache = $cache; - } else { - throw new InvalidConfigException('FragmentCache::cacheID must refer to the ID of a cache application component.'); - } - } - return $this->_cache; - } - - /** - * Sets the cache instance used by the session component. - * @param Cache $value the cache instance - */ - public function setCache($value) - { - $this->_cache = $value; - } + + * @since 2.0 + */ +class FragmentCache extends Widget +{ + /** + * @var Cache|string the cache object or the application component ID of the cache object. + * After the FragmentCache object is created, if you want to change this property, + * you should only assign it with a cache object. + */ + public $cache = 'cache'; + /** + * @var integer number of seconds that the data can remain valid in cache. + * Use 0 to indicate that the cached data will never expire. + */ + public $duration = 60; + /** + * @var array|Dependency the dependency that the cached content depends on. + * This can be either a [[Dependency]] object or a configuration array for creating the dependency object. + * For example, + * + * ~~~ + * array( + * 'class' => 'yii\caching\DbDependency', + * 'sql' => 'SELECT MAX(lastModified) FROM Post', + * ) + * ~~~ + * + * would make the output cache depends on the last modified time of all posts. + * If any post has its modification time changed, the cached content would be invalidated. + */ + public $dependency; + /** + * @var array list of factors that would cause the variation of the content being cached. + * Each factor is a string representing a variation (e.g. the language, a GET parameter). + * The following variation setting will cause the content to be cached in different versions + * according to the current application language: + * + * ~~~ + * array( + * Yii::$app->language, + * ) + */ + public $variations; + /** + * @var boolean whether to enable the fragment cache. You may use this property to turn on and off + * the fragment cache according to specific setting (e.g. enable fragment cache only for GET requests). + */ + public $enabled = true; + /** + * @var array a list of placeholders for embedding dynamic contents. This property + * is used internally to implement the content caching feature. Do not modify it. + */ + public $dynamicPlaceholders; + + /** + * Initializes the FragmentCache object. + */ + public function init() + { + parent::init(); + + if (!$this->enabled) { + $this->cache = null; + } elseif (is_string($this->cache)) { + $this->cache = Yii::$app->getComponent($this->cache); + } + + if ($this->getCachedContent() === false) { + $this->view->cacheStack[] = $this; + ob_start(); + ob_implicit_flush(false); + } + } + + /** + * Marks the end of content to be cached. + * Content displayed before this method call and after {@link init()} + * will be captured and saved in cache. + * This method does nothing if valid content is already found in cache. + */ + public function run() + { + if (($content = $this->getCachedContent()) !== false) { + echo $content; + } elseif ($this->cache instanceof Cache) { + $content = ob_get_clean(); + array_pop($this->view->cacheStack); + if (is_array($this->dependency)) { + $this->dependency = Yii::createObject($this->dependency); + } + $data = array($content, $this->dynamicPlaceholders); + $this->cache->set($this->calculateKey(), $data, $this->duration, $this->dependency); + + if ($this->view->cacheStack === array() && !empty($this->dynamicPlaceholders)) { + $content = $this->updateDynamicContent($content, $this->dynamicPlaceholders); + } + echo $content; + } + } + + /** + * @var string|boolean the cached content. False if the content is not cached. + */ + private $_content; + + /** + * Returns the cached content if available. + * @return string|boolean the cached content. False is returned if valid content is not found in the cache. + */ + public function getCachedContent() + { + if ($this->_content === null) { + $this->_content = false; + if ($this->cache instanceof Cache) { + $key = $this->calculateKey(); + $data = $this->cache->get($key); + if (is_array($data) && count($data) === 2) { + list ($content, $placeholders) = $data; + if (is_array($placeholders) && count($placeholders) > 0) { + if ($this->view->cacheStack === array()) { + // outermost cache: replace placeholder with dynamic content + $content = $this->updateDynamicContent($content, $placeholders); + } + foreach ($placeholders as $name => $statements) { + $this->view->addDynamicPlaceholder($name, $statements); + } + } + $this->_content = $content; + } + } + } + return $this->_content; + } + + protected function updateDynamicContent($content, $placeholders) + { + foreach ($placeholders as $name => $statements) { + $placeholders[$name] = $this->view->evaluateDynamicContent($statements); + } + return strtr($content, $placeholders); + } + + /** + * Generates a unique key used for storing the content in cache. + * The key generated depends on both [[id]] and [[variations]]. + * @return string a valid cache key + */ + protected function calculateKey() + { + $factors = array(__CLASS__, $this->getId()); + if (is_array($this->variations)) { + foreach ($this->variations as $factor) { + $factors[] = $factor; + } + } + return $this->cache->buildKey($factors); + } } \ No newline at end of file diff --git a/tests/unit/MysqlTestCase.php b/tests/unit/MysqlTestCase.php index d62f95e..e1a1f7e 100644 --- a/tests/unit/MysqlTestCase.php +++ b/tests/unit/MysqlTestCase.php @@ -4,7 +4,7 @@ namespace yiiunit; class MysqlTestCase extends TestCase { - function __construct() + protected function setUp() { if (!extension_loaded('pdo') || !extension_loaded('pdo_mysql')) { $this->markTestSkipped('pdo and pdo_mysql extensions are required.'); @@ -15,7 +15,7 @@ class MysqlTestCase extends TestCase * @param bool $reset whether to clean up the test database * @return \yii\db\Connection */ - function getConnection($reset = true) + public function getConnection($reset = true) { $params = $this->getParam('mysql'); $db = new \yii\db\Connection; diff --git a/tests/unit/data/base/InvalidRulesModel.php b/tests/unit/data/base/InvalidRulesModel.php new file mode 100644 index 0000000..f5a8438 --- /dev/null +++ b/tests/unit/data/base/InvalidRulesModel.php @@ -0,0 +1,17 @@ + 'Lennon'), + array('lastName', 'required'), + array('underscore_style', 'yii\validators\CaptchaValidator'), + ); + } +} \ No newline at end of file diff --git a/tests/unit/data/base/Speaker.php b/tests/unit/data/base/Speaker.php new file mode 100644 index 0000000..93dd496 --- /dev/null +++ b/tests/unit/data/base/Speaker.php @@ -0,0 +1,39 @@ + 'This is the custom label', + ); + } + + public function rules() + { + return array( + + ); + } + + public function scenarios() + { + return array( + 'test' => array('firstName', 'lastName', '!underscore_style'), + ); + } +} diff --git a/tests/unit/framework/base/ComponentTest.php b/tests/unit/framework/base/ComponentTest.php index 2c456e2..97b0116 100644 --- a/tests/unit/framework/base/ComponentTest.php +++ b/tests/unit/framework/base/ComponentTest.php @@ -352,7 +352,7 @@ class NewComponent extends Component public function raiseEvent() { - $this->trigger('click', new Event($this)); + $this->trigger('click', new Event); } } diff --git a/tests/unit/framework/base/DictionaryTest.php b/tests/unit/framework/base/DictionaryTest.php index 0b20093..9e55547 100644 --- a/tests/unit/framework/base/DictionaryTest.php +++ b/tests/unit/framework/base/DictionaryTest.php @@ -61,7 +61,7 @@ class DictionaryTest extends \yiiunit\TestCase { $this->dictionary->add('key3',$this->item3); $this->assertEquals(3,$this->dictionary->getCount()); - $this->assertTrue($this->dictionary->contains('key3')); + $this->assertTrue($this->dictionary->has('key3')); $this->dictionary[] = 'test'; } @@ -70,28 +70,28 @@ class DictionaryTest extends \yiiunit\TestCase { $this->dictionary->remove('key1'); $this->assertEquals(1,$this->dictionary->getCount()); - $this->assertTrue(!$this->dictionary->contains('key1')); + $this->assertTrue(!$this->dictionary->has('key1')); $this->assertTrue($this->dictionary->remove('unknown key')===null); } - public function testClear() + public function testRemoveAll() { $this->dictionary->add('key3',$this->item3); - $this->dictionary->clear(); + $this->dictionary->removeAll(); $this->assertEquals(0,$this->dictionary->getCount()); - $this->assertTrue(!$this->dictionary->contains('key1') && !$this->dictionary->contains('key2')); + $this->assertTrue(!$this->dictionary->has('key1') && !$this->dictionary->has('key2')); $this->dictionary->add('key3',$this->item3); - $this->dictionary->clear(true); + $this->dictionary->removeAll(true); $this->assertEquals(0,$this->dictionary->getCount()); - $this->assertTrue(!$this->dictionary->contains('key1') && !$this->dictionary->contains('key2')); + $this->assertTrue(!$this->dictionary->has('key1') && !$this->dictionary->has('key2')); } - public function testContains() + public function testHas() { - $this->assertTrue($this->dictionary->contains('key1')); - $this->assertTrue($this->dictionary->contains('key2')); - $this->assertFalse($this->dictionary->contains('key3')); + $this->assertTrue($this->dictionary->has('key1')); + $this->assertTrue($this->dictionary->has('key2')); + $this->assertFalse($this->dictionary->has('key3')); } public function testFromArray() @@ -162,7 +162,7 @@ class DictionaryTest extends \yiiunit\TestCase unset($this->dictionary['key2']); $this->assertEquals(2,$this->dictionary->getCount()); - $this->assertTrue(!$this->dictionary->contains('key2')); + $this->assertTrue(!$this->dictionary->has('key2')); unset($this->dictionary['unknown key']); } diff --git a/tests/unit/framework/base/ModelTest.php b/tests/unit/framework/base/ModelTest.php new file mode 100644 index 0000000..aa15230 --- /dev/null +++ b/tests/unit/framework/base/ModelTest.php @@ -0,0 +1,203 @@ +assertEquals('First Name', $speaker->getAttributeLabel('firstName')); + $this->assertEquals('This is the custom label', $speaker->getAttributeLabel('customLabel')); + $this->assertEquals('Underscore Style', $speaker->getAttributeLabel('underscore_style')); + } + + public function testGetAttributes() + { + $speaker = new Speaker(); + $speaker->firstName = 'Qiang'; + $speaker->lastName = 'Xue'; + + $this->assertEquals(array( + 'firstName' => 'Qiang', + 'lastName' => 'Xue', + 'customLabel' => null, + 'underscore_style' => null, + ), $speaker->getAttributes()); + + $this->assertEquals(array( + 'firstName' => 'Qiang', + 'lastName' => 'Xue', + ), $speaker->getAttributes(array('firstName', 'lastName'))); + + $this->assertEquals(array( + 'firstName' => 'Qiang', + 'lastName' => 'Xue', + ), $speaker->getAttributes(null, array('customLabel', 'underscore_style'))); + + $this->assertEquals(array( + 'firstName' => 'Qiang', + ), $speaker->getAttributes(array('firstName', 'lastName'), array('lastName', 'customLabel', 'underscore_style'))); + } + + public function testSetAttributes() + { + // by default mass assignment doesn't work at all + $speaker = new Speaker(); + $speaker->setAttributes(array('firstName' => 'Qiang', 'underscore_style' => 'test')); + $this->assertNull($speaker->firstName); + $this->assertNull($speaker->underscore_style); + + // in the test scenario + $speaker = new Speaker(); + $speaker->setScenario('test'); + $speaker->setAttributes(array('firstName' => 'Qiang', 'underscore_style' => 'test')); + $this->assertNull($speaker->underscore_style); + $this->assertEquals('Qiang', $speaker->firstName); + + $speaker->setAttributes(array('firstName' => 'Qiang', 'underscore_style' => 'test'), false); + $this->assertEquals('test', $speaker->underscore_style); + $this->assertEquals('Qiang', $speaker->firstName); + } + + public function testActiveAttributes() + { + // by default mass assignment doesn't work at all + $speaker = new Speaker(); + $this->assertEmpty($speaker->activeAttributes()); + + $speaker = new Speaker(); + $speaker->setScenario('test'); + $this->assertEquals(array('firstName', 'lastName', 'underscore_style'), $speaker->activeAttributes()); + } + + public function testIsAttributeSafe() + { + // by default mass assignment doesn't work at all + $speaker = new Speaker(); + $this->assertFalse($speaker->isAttributeSafe('firstName')); + + $speaker = new Speaker(); + $speaker->setScenario('test'); + $this->assertTrue($speaker->isAttributeSafe('firstName')); + + } + + public function testErrors() + { + $speaker = new Speaker(); + + $this->assertEmpty($speaker->getErrors()); + $this->assertEmpty($speaker->getErrors('firstName')); + $this->assertEmpty($speaker->getFirstErrors()); + + $this->assertFalse($speaker->hasErrors()); + $this->assertFalse($speaker->hasErrors('firstName')); + + $speaker->addError('firstName', 'Something is wrong!'); + $this->assertEquals(array('firstName' => array('Something is wrong!')), $speaker->getErrors()); + $this->assertEquals(array('Something is wrong!'), $speaker->getErrors('firstName')); + + $speaker->addError('firstName', 'Totally wrong!'); + $this->assertEquals(array('firstName' => array('Something is wrong!', 'Totally wrong!')), $speaker->getErrors()); + $this->assertEquals(array('Something is wrong!', 'Totally wrong!'), $speaker->getErrors('firstName')); + + $this->assertTrue($speaker->hasErrors()); + $this->assertTrue($speaker->hasErrors('firstName')); + $this->assertFalse($speaker->hasErrors('lastName')); + + $this->assertEquals(array('Something is wrong!'), $speaker->getFirstErrors()); + $this->assertEquals('Something is wrong!', $speaker->getFirstError('firstName')); + $this->assertNull($speaker->getFirstError('lastName')); + + $speaker->addError('lastName', 'Another one!'); + $this->assertEquals(array( + 'firstName' => array( + 'Something is wrong!', + 'Totally wrong!', + ), + 'lastName' => array('Another one!'), + ), $speaker->getErrors()); + + $speaker->clearErrors('firstName'); + $this->assertEquals(array( + 'lastName' => array('Another one!'), + ), $speaker->getErrors()); + + $speaker->clearErrors(); + $this->assertEmpty($speaker->getErrors()); + $this->assertFalse($speaker->hasErrors()); + } + + public function testArraySyntax() + { + $speaker = new Speaker(); + + // get + $this->assertNull($speaker['firstName']); + + // isset + $this->assertFalse(isset($speaker['firstName'])); + + // set + $speaker['firstName'] = 'Qiang'; + + $this->assertEquals('Qiang', $speaker['firstName']); + $this->assertTrue(isset($speaker['firstName'])); + + // iteration + $attributes = array(); + foreach($speaker as $key => $attribute) { + $attributes[$key] = $attribute; + } + $this->assertEquals(array( + 'firstName' => 'Qiang', + 'lastName' => null, + 'customLabel' => null, + 'underscore_style' => null, + ), $attributes); + + // unset + unset($speaker['firstName']); + + // exception isn't expected here + $this->assertNull($speaker['firstName']); + $this->assertFalse(isset($speaker['firstName'])); + } + + public function testDefaults() + { + $singer = new Model(); + $this->assertEquals(array(), $singer->rules()); + $this->assertEquals(array(), $singer->attributeLabels()); + } + + public function testDefaultScenarios() + { + $singer = new Singer(); + $this->assertEquals(array('default' => array('lastName', 'underscore_style')), $singer->scenarios()); + } + + public function testIsAttributeRequired() + { + $singer = new Singer(); + $this->assertFalse($singer->isAttributeRequired('firstName')); + $this->assertTrue($singer->isAttributeRequired('lastName')); + } + + public function testCreateValidators() + { + $this->setExpectedException('yii\base\InvalidConfigException', 'Invalid validation rule: a rule must be an array specifying both attribute names and validator type.'); + + $invalid = new InvalidRulesModel(); + $invalid->createValidators(); + } +} diff --git a/tests/unit/framework/base/VectorTest.php b/tests/unit/framework/base/VectorTest.php index d2657bf..5c44d17 100644 --- a/tests/unit/framework/base/VectorTest.php +++ b/tests/unit/framework/base/VectorTest.php @@ -101,26 +101,26 @@ class VectorTest extends \yiiunit\TestCase $this->vector->removeAt(2); } - public function testClear() + public function testRemoveAll() { $this->vector->add($this->item3); - $this->vector->clear(); + $this->vector->removeAll(); $this->assertEquals(0,$this->vector->getCount()); $this->assertEquals(-1,$this->vector->indexOf($this->item1)); $this->assertEquals(-1,$this->vector->indexOf($this->item2)); $this->vector->add($this->item3); - $this->vector->clear(true); + $this->vector->removeAll(true); $this->assertEquals(0,$this->vector->getCount()); $this->assertEquals(-1,$this->vector->indexOf($this->item1)); $this->assertEquals(-1,$this->vector->indexOf($this->item2)); } - public function testContains() + public function testHas() { - $this->assertTrue($this->vector->contains($this->item1)); - $this->assertTrue($this->vector->contains($this->item2)); - $this->assertFalse($this->vector->contains($this->item3)); + $this->assertTrue($this->vector->has($this->item1)); + $this->assertTrue($this->vector->has($this->item2)); + $this->assertFalse($this->vector->has($this->item3)); } public function testIndexOf() diff --git a/tests/unit/framework/caching/DbCacheTest.php b/tests/unit/framework/caching/DbCacheTest.php index 3977ee8..594e946 100644 --- a/tests/unit/framework/caching/DbCacheTest.php +++ b/tests/unit/framework/caching/DbCacheTest.php @@ -11,7 +11,7 @@ class DbCacheTest extends CacheTest private $_cacheInstance; private $_connection; - function __construct() + protected function setUp() { if (!extension_loaded('pdo') || !extension_loaded('pdo_mysql')) { $this->markTestSkipped('pdo and pdo_mysql extensions are required.'); diff --git a/tests/unit/framework/db/ConnectionTest.php b/tests/unit/framework/db/ConnectionTest.php index afb4f20..256c5a9 100644 --- a/tests/unit/framework/db/ConnectionTest.php +++ b/tests/unit/framework/db/ConnectionTest.php @@ -59,7 +59,6 @@ class ConnectionTest extends \yiiunit\MysqlTestCase $this->assertEquals('`table`', $connection->quoteTableName('`table`')); $this->assertEquals('`schema`.`table`', $connection->quoteTableName('schema.table')); $this->assertEquals('`schema`.`table`', $connection->quoteTableName('schema.`table`')); - $this->assertEquals('[[table]]', $connection->quoteTableName('[[table]]')); $this->assertEquals('{{table}}', $connection->quoteTableName('{{table}}')); $this->assertEquals('(table)', $connection->quoteTableName('(table)')); } diff --git a/tests/unit/framework/util/ArrayHelperTest.php b/tests/unit/framework/helpers/ArrayHelperTest.php similarity index 97% rename from tests/unit/framework/util/ArrayHelperTest.php rename to tests/unit/framework/helpers/ArrayHelperTest.php index 117c702..187217f 100644 --- a/tests/unit/framework/util/ArrayHelperTest.php +++ b/tests/unit/framework/helpers/ArrayHelperTest.php @@ -1,6 +1,6 @@ assertEquals(4, StringHelper::strlen('this')); + $this->assertEquals(6, StringHelper::strlen('это')); + } + + public function testSubstr() + { + $this->assertEquals('th', StringHelper::substr('this', 0, 2)); + $this->assertEquals('э', StringHelper::substr('это', 0, 2)); + } + + public function testPluralize() + { + $testData = array( + 'move' => 'moves', + 'foot' => 'feet', + 'child' => 'children', + 'human' => 'humans', + 'man' => 'men', + 'staff' => 'staff', + 'tooth' => 'teeth', + 'person' => 'people', + 'mouse' => 'mice', + 'touch' => 'touches', + 'hash' => 'hashes', + 'shelf' => 'shelves', + 'potato' => 'potatoes', + 'bus' => 'buses', + 'test' => 'tests', + 'car' => 'cars', + ); + + foreach($testData as $testIn => $testOut) { + $this->assertEquals($testOut, StringHelper::pluralize($testIn)); + $this->assertEquals(ucfirst($testOut), ucfirst(StringHelper::pluralize($testIn))); + } + } + + public function testCamel2words() + { + $this->assertEquals('Camel Case', StringHelper::camel2words('camelCase')); + $this->assertEquals('Lower Case', StringHelper::camel2words('lower_case')); + $this->assertEquals('Tricky Stuff It Is Testing', StringHelper::camel2words(' tricky_stuff.it-is testing... ')); + } + + public function testCamel2id() + { + $this->assertEquals('post-tag', StringHelper::camel2id('PostTag')); + $this->assertEquals('post_tag', StringHelper::camel2id('PostTag', '_')); + + $this->assertEquals('post-tag', StringHelper::camel2id('postTag')); + $this->assertEquals('post_tag', StringHelper::camel2id('postTag', '_')); + } + + public function testId2camel() + { + $this->assertEquals('PostTag', StringHelper::id2camel('post-tag')); + $this->assertEquals('PostTag', StringHelper::id2camel('post_tag', '_')); + + $this->assertEquals('PostTag', StringHelper::id2camel('post-tag')); + $this->assertEquals('PostTag', StringHelper::id2camel('post_tag', '_')); + } +} \ No newline at end of file diff --git a/tests/unit/framework/web/UrlManagerTest.php b/tests/unit/framework/web/UrlManagerTest.php index fcdcf7d..95b3bf6 100644 --- a/tests/unit/framework/web/UrlManagerTest.php +++ b/tests/unit/framework/web/UrlManagerTest.php @@ -11,6 +11,7 @@ class UrlManagerTest extends \yiiunit\TestCase // default setting with '/' as base url $manager = new UrlManager(array( 'baseUrl' => '/', + 'cache' => null, )); $url = $manager->createUrl('post/view'); $this->assertEquals('/?r=post/view', $url); @@ -20,6 +21,7 @@ class UrlManagerTest extends \yiiunit\TestCase // default setting with '/test/' as base url $manager = new UrlManager(array( 'baseUrl' => '/test/', + 'cache' => null, )); $url = $manager->createUrl('post/view', array('id' => 1, 'title' => 'sample post')); $this->assertEquals('/test/?r=post/view&id=1&title=sample+post', $url); @@ -28,18 +30,21 @@ class UrlManagerTest extends \yiiunit\TestCase $manager = new UrlManager(array( 'enablePrettyUrl' => true, 'baseUrl' => '/', + 'cache' => null, )); $url = $manager->createUrl('post/view', array('id' => 1, 'title' => 'sample post')); $this->assertEquals('/post/view?id=1&title=sample+post', $url); $manager = new UrlManager(array( 'enablePrettyUrl' => true, 'baseUrl' => '/test/', + 'cache' => null, )); $url = $manager->createUrl('post/view', array('id' => 1, 'title' => 'sample post')); $this->assertEquals('/test/post/view?id=1&title=sample+post', $url); $manager = new UrlManager(array( 'enablePrettyUrl' => true, 'baseUrl' => '/test/index.php', + 'cache' => null, )); $url = $manager->createUrl('post/view', array('id' => 1, 'title' => 'sample post')); $this->assertEquals('/test/index.php/post/view?id=1&title=sample+post', $url); @@ -49,7 +54,7 @@ class UrlManagerTest extends \yiiunit\TestCase // pretty URL with rules $manager = new UrlManager(array( 'enablePrettyUrl' => true, - 'cacheID' => false, + 'cache' => null, 'rules' => array( array( 'pattern' => 'post//', @@ -66,7 +71,7 @@ class UrlManagerTest extends \yiiunit\TestCase // pretty URL with rules and suffix $manager = new UrlManager(array( 'enablePrettyUrl' => true, - 'cacheID' => false, + 'cache' => null, 'rules' => array( array( 'pattern' => 'post/<id>/<title>', @@ -87,6 +92,7 @@ class UrlManagerTest extends \yiiunit\TestCase $manager = new UrlManager(array( 'baseUrl' => '/', 'hostInfo' => 'http://www.example.com', + 'cache' => null, )); $url = $manager->createAbsoluteUrl('post/view', array('id' => 1, 'title' => 'sample post')); $this->assertEquals('http://www.example.com/?r=post/view&id=1&title=sample+post', $url); @@ -94,7 +100,9 @@ class UrlManagerTest extends \yiiunit\TestCase public function testParseRequest() { - $manager = new UrlManager; + $manager = new UrlManager(array( + 'cache' => null, + )); $request = new Request; // default setting without 'r' param @@ -115,6 +123,7 @@ class UrlManagerTest extends \yiiunit\TestCase // pretty URL without rules $manager = new UrlManager(array( 'enablePrettyUrl' => true, + 'cache' => null, )); // empty pathinfo $request->pathInfo = ''; @@ -136,7 +145,7 @@ class UrlManagerTest extends \yiiunit\TestCase // pretty URL rules $manager = new UrlManager(array( 'enablePrettyUrl' => true, - 'cacheID' => false, + 'cache' => null, 'rules' => array( array( 'pattern' => 'post/<id>/<title>', @@ -169,7 +178,7 @@ class UrlManagerTest extends \yiiunit\TestCase $manager = new UrlManager(array( 'enablePrettyUrl' => true, 'suffix' => '.html', - 'cacheID' => false, + 'cache' => null, 'rules' => array( array( 'pattern' => 'post/<id>/<title>', diff --git a/tests/unit/framework/web/UrlRuleTest.php b/tests/unit/framework/web/UrlRuleTest.php index 8b2b578..825199e 100644 --- a/tests/unit/framework/web/UrlRuleTest.php +++ b/tests/unit/framework/web/UrlRuleTest.php @@ -10,7 +10,7 @@ class UrlRuleTest extends \yiiunit\TestCase { public function testCreateUrl() { - $manager = new UrlManager; + $manager = new UrlManager(array('cache' => null)); $suites = $this->getTestsForCreateUrl(); foreach ($suites as $i => $suite) { list ($name, $config, $tests) = $suite; @@ -25,7 +25,7 @@ class UrlRuleTest extends \yiiunit\TestCase public function testParseRequest() { - $manager = new UrlManager; + $manager = new UrlManager(array('cache' => null)); $request = new Request; $suites = $this->getTestsForParseRequest(); foreach ($suites as $i => $suite) {