From 1184e1e148b1f4fe1af61774e58581a3ffc2cba3 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Tue, 26 Mar 2013 15:27:15 -0400 Subject: [PATCH 01/41] Finished AccessControl and HttpCache. --- framework/web/AccessControl.php | 104 ++++++++++++++++++++ framework/web/AccessRule.php | 212 ++++++++++++++++++++++++++++++++++++++++ framework/web/Application.php | 12 +++ framework/web/HttpCache.php | 125 +++++++++++++++++++++++ framework/web/Request.php | 2 +- 5 files changed, 454 insertions(+), 1 deletion(-) create mode 100644 framework/web/AccessControl.php create mode 100644 framework/web/AccessRule.php create mode 100644 framework/web/HttpCache.php 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..ac42ad1 --- /dev/null +++ b/framework/web/AccessRule.php @@ -0,0 +1,212 @@ + + * @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 user names that this rule applies to. The comparison is case-insensitive. + * If not set or empty, it means this rule applies to all users. Two special tokens are recognized: + * + * - `?`: matches a guest user (not authenticated yet) + * - `@`: matches an authenticated user + * + * @see \yii\web\Application::user + */ + public $users; + /** + * @var array list of roles that this rule applies to. For each role, the current user's + * {@link CWebUser::checkAccess} method will be invoked. If one of the invocations + * returns true, the rule will be applied. + * Note, you should mainly use roles in an "allow" rule because by definition, + * a role represents a permission collection. + * If 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->matchUser($user) + && $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 + * @return boolean whether the rule applies to the user + */ + protected function matchUser($user) + { + if (empty($this->users)) { + return true; + } + foreach ($this->users as $u) { + if ($u === '?' && $user->getIsGuest()) { + return true; + } elseif ($u === '@' && !$user->getIsGuest()) { + return true; + } elseif (!strcasecmp($u, $user->getName())) { + return true; + } + } + return false; + } + + /** + * @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 ($user->checkAccess($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..2e3a0c3 100644 --- a/framework/web/Application.php +++ b/framework/web/Application.php @@ -69,6 +69,15 @@ class Application extends \yii\base\Application } /** + * Returns the user component. + * @return User the user component + */ + public function getUser() + { + return $this->getComponent('user'); + } + + /** * 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. @@ -130,6 +139,9 @@ class Application extends \yii\base\Application 'session' => array( 'class' => 'yii\web\Session', ), + 'user' => array( + 'class' => 'yii\web\User', + ), )); } } diff --git a/framework/web/HttpCache.php b/framework/web/HttpCache.php new file mode 100644 index 0000000..e3cf17d --- /dev/null +++ b/framework/web/HttpCache.php @@ -0,0 +1,125 @@ + + * @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]]. + */ + 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. + */ + public $etagSeed; + /** + * @var mixed additional parameters that should be passed to the [[lastModified]] and [[etagSeed]] callbacks. + */ + public $params; + /** + * Http cache control headers. Set this to an empty string in order to keep this + * header from being sent entirely. + * @var string + */ + public $cacheControl = '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) + { + $requestMethod = Yii::$app->request->getRequestMethod(); + if ($requestMethod !== 'GET' && $requestMethod !== 'HEAD') { + 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); + } + + if ($etag !== null) { + header("ETag: $etag"); + } + $this->sendCacheControlHeader(); + + if ($this->hasChanged($lastModified, $etag)) { + if ($lastModified !== null) { + header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $lastModified) . ' GMT'); + } + return true; + } else { + header('HTTP/1.1 304 Not Modified'); + return false; + } + } + + protected function hasChanged($lastModified, $etag) + { + $changed = !isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) || @strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) < $lastModified; + if (!$changed && $etag !== null) { + $changed = !isset($_SERVER['HTTP_IF_NONE_MATCH']) || $_SERVER['HTTP_IF_NONE_MATCH'] !== $etag; + } + return $changed; + } + + /** + * Sends the cache control header to the client + * @see cacheControl + */ + protected function sendCacheControlHeader() + { + if (Yii::$app->session->isActive) { + session_cache_limiter('public'); + header('Pragma:', true); + } + header('Cache-Control: ' . $this->cacheControl, true); + } + + /** + * Generates an Etag from a 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/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'; } From 8724d8038438b6153975c211d60289889d0621e1 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Tue, 26 Mar 2013 19:21:45 -0400 Subject: [PATCH 02/41] HttpCache WIP. --- framework/web/HttpCache.php | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/framework/web/HttpCache.php b/framework/web/HttpCache.php index e3cf17d..d98cf92 100644 --- a/framework/web/HttpCache.php +++ b/framework/web/HttpCache.php @@ -27,7 +27,7 @@ class HttpCache extends ActionFilter * ~~~ * * where `$action` is the [[Action]] object that this filter is currently handling; - * `$params` takes the value of [[params]]. + * `$params` takes the value of [[params]]. The callback should return a UNIX timestamp. */ public $lastModified; /** @@ -39,7 +39,8 @@ class HttpCache extends ActionFilter * ~~~ * * where `$action` is the [[Action]] object that this filter is currently handling; - * `$params` takes the value of [[params]]. The callback should return a string. + * `$params` takes the value of [[params]]. The callback should return a string serving + * as the seed for generating an Etag. */ public $etagSeed; /** @@ -62,7 +63,7 @@ class HttpCache extends ActionFilter public function beforeAction($action) { $requestMethod = Yii::$app->request->getRequestMethod(); - if ($requestMethod !== 'GET' && $requestMethod !== 'HEAD') { + if ($requestMethod !== 'GET' && $requestMethod !== 'HEAD' || $this->lastModified === null && $this->etagSeed === null) { return true; } @@ -75,29 +76,36 @@ class HttpCache extends ActionFilter $etag = $this->generateEtag($seed); } + $this->sendCacheControlHeader(); if ($etag !== null) { header("ETag: $etag"); } - $this->sendCacheControlHeader(); - if ($this->hasChanged($lastModified, $etag)) { + if ($this->validateCache($lastModified, $etag)) { + header('HTTP/1.1 304 Not Modified'); + return false; + } else { if ($lastModified !== null) { header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $lastModified) . ' GMT'); } return true; - } else { - header('HTTP/1.1 304 Not Modified'); - return false; } } - protected function hasChanged($lastModified, $etag) + /** + * 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) { - $changed = !isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) || @strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) < $lastModified; - if (!$changed && $etag !== null) { - $changed = !isset($_SERVER['HTTP_IF_NONE_MATCH']) || $_SERVER['HTTP_IF_NONE_MATCH'] !== $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; } - return $changed; } /** @@ -114,7 +122,7 @@ class HttpCache extends ActionFilter } /** - * Generates an Etag from a given seed string. + * Generates an Etag from the given seed string. * @param string $seed Seed for the ETag * @return string the generated Etag */ From cd1b3d321a34c5d89bb9c75386b97218c4354318 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Tue, 26 Mar 2013 21:03:43 -0400 Subject: [PATCH 03/41] refactored url creation shortcut method. --- framework/base/Application.php | 4 +-- framework/helpers/Html.php | 15 +++++++---- framework/web/Application.php | 45 -------------------------------- framework/web/Controller.php | 25 ++++++++++++++++++ framework/web/HttpCache.php | 8 +++--- framework/web/User.php | 59 ------------------------------------------ 6 files changed, 40 insertions(+), 116 deletions(-) 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/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/web/Application.php b/framework/web/Application.php index 2e3a0c3..2533f04 100644 --- a/framework/web/Application.php +++ b/framework/web/Application.php @@ -78,51 +78,6 @@ 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 - */ - public function createUrl($route, $params = array()) - { - 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); - } - - /** * Registers the core application components. * @see setComponents */ diff --git a/framework/web/Controller.php b/framework/web/Controller.php index 2779c35..93b74aa 100644 --- a/framework/web/Controller.php +++ b/framework/web/Controller.php @@ -7,6 +7,8 @@ namespace yii\web; +use Yii; + /** * Controller is the base class of Web controllers. * @@ -16,4 +18,27 @@ 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/HttpCache.php b/framework/web/HttpCache.php index d98cf92..09df7a2 100644 --- a/framework/web/HttpCache.php +++ b/framework/web/HttpCache.php @@ -84,12 +84,10 @@ class HttpCache extends ActionFilter if ($this->validateCache($lastModified, $etag)) { header('HTTP/1.1 304 Not Modified'); return false; - } else { - if ($lastModified !== null) { - header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $lastModified) . ' GMT'); - } - return true; + } elseif ($lastModified !== null) { + header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $lastModified) . ' GMT'); } + return true; } /** diff --git a/framework/web/User.php b/framework/web/User.php index ba0d37b..4bd184f 100644 --- a/framework/web/User.php +++ b/framework/web/User.php @@ -126,65 +126,6 @@ class User extends Component private $_keyPrefix; private $_access = array(); - /** - * 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 - */ - public function __get($name) - { - if ($this->hasState($name)) { - return $this->getState($name); - } else { - return parent::__get($name); - } - } - - /** - * 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 - */ - public function __set($name, $value) - { - if ($this->hasState($name)) { - $this->setState($name, $value); - } else { - parent::__set($name, $value); - } - } - - /** - * 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); - } - } - - /** - * 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. - */ - public function __unset($name) - { - if ($this->hasState($name)) { - $this->setState($name, null); - } else { - parent::__unset($name); - } - } /** * Initializes the application component. From c03a3ff858ce90a6f5df1cdf7994185f33f661e2 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Wed, 27 Mar 2013 11:39:05 -0400 Subject: [PATCH 04/41] Finishes flash feature. --- framework/base/Dictionary.php | 8 +- framework/base/Vector.php | 6 +- framework/web/Session.php | 157 ++++++++++++++++++++---- framework/web/User.php | 174 +++++---------------------- tests/unit/framework/base/DictionaryTest.php | 24 ++-- tests/unit/framework/base/VectorTest.php | 14 +-- 6 files changed, 186 insertions(+), 197 deletions(-) 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/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/web/Session.php b/framework/web/Session.php index 5697679..eefc1a8 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,10 @@ 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'; /** * Initializes the application component. @@ -117,7 +112,9 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co if (session_id() == '') { $error = error_get_last(); $message = isset($error['message']) ? $error['message'] : 'Failed to start session.'; - Yii::warning($message, __CLASS__); + Yii::error($message, __CLASS__); + } else { + $this->updateFlashCounters(); } } @@ -462,18 +459,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 +487,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 +498,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 +512,114 @@ 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]); + } + + /** + * @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/User.php b/framework/web/User.php index 4bd184f..93eb1ce 100644 --- a/framework/web/User.php +++ b/framework/web/User.php @@ -40,7 +40,7 @@ use yii\base\Component; * 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, + * Note, when {@link enableAutoLogin 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. @@ -50,67 +50,54 @@ use yii\base\Component; * @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). * * @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'; /** * @var boolean whether to enable cookie-based login. Defaults to false. */ - public $allowAutoLogin = false; + public $enableAutoLogin = false; /** - * @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). - */ - public $guestName = 'Guest'; - /** - * @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; /** - * @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. + * Defaults to false. This property is effective only when {@link 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 + * @see enableAutoLogin * @since 1.1.0 */ public $autoRenewCookie = false; /** - * @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 - */ - public $autoUpdateFlash = true; - /** * @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 @@ -129,22 +116,16 @@ class User extends Component /** * Initializes the application component. - * This method overrides the parent implementation by starting session, - * performing cookie-based authentication if enabled, and updating the flash variables. */ public function init() { parent::init(); Yii::app()->getSession()->open(); - if ($this->getIsGuest() && $this->allowAutoLogin) { + if ($this->getIsGuest() && $this->enableAutoLogin) { $this->restoreFromCookie(); - } elseif ($this->autoRenewCookie && $this->allowAutoLogin) { + } elseif ($this->autoRenewCookie && $this->enableAutoLogin) { $this->renewCookie(); } - if ($this->autoUpdateFlash) { - $this->updateFlash(); - } - $this->updateAuthStatus(); } @@ -156,12 +137,12 @@ class User extends Component * the session storage. If the duration parameter is greater than 0, * a cookie will be sent to prepare for cookie-based login in future. * - * Note, you have to set {@link allowAutoLogin} to true + * Note, you have to set {@link enableAutoLogin} 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} + * If greater than 0, cookie-based login will be used. In this case, {@link enableAutoLogin} * must be set true, otherwise an exception will be thrown. * @return boolean whether the user is logged in */ @@ -173,10 +154,10 @@ class User extends Component $this->changeIdentity($id, $identity->getName(), $states); if ($duration > 0) { - if ($this->allowAutoLogin) { + if ($this->enableAutoLogin) { $this->saveToCookie($duration); } else { - throw new CException(Yii::t('yii', '{class}.allowAutoLogin must be set true in order to use cookie-based authentication.', + throw new CException(Yii::t('yii', '{class}.enableAutoLogin must be set true in order to use cookie-based authentication.', array('{class}' => get_class($this)))); } } @@ -196,7 +177,7 @@ class User extends Component public function logout($destroySession = true) { if ($this->beforeLogout()) { - if ($this->allowAutoLogin) { + if ($this->enableAutoLogin) { Yii::app()->getRequest()->getCookies()->remove($this->getStateKeyPrefix()); if ($this->identityCookie !== null) { $cookie = $this->createIdentityCookie($this->getStateKeyPrefix()); @@ -377,7 +358,7 @@ class User extends Component /** * Populates the current user object with the information obtained from cookie. - * This method is used when automatic login ({@link allowAutoLogin}) is enabled. + * This method is used when automatic login ({@link enableAutoLogin}) is enabled. * The user identity information is recovered from cookie. * Sufficient security measures are used to prevent cookie data from being tampered. * @see saveToCookie @@ -425,7 +406,7 @@ class User extends Component /** * Saves necessary user data into a cookie. - * This method is used when automatic login ({@link allowAutoLogin}) is enabled. + * This method is used when automatic login ({@link enableAutoLogin}) 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. @@ -555,81 +536,6 @@ class User extends Component } /** - * 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 @@ -678,28 +584,6 @@ class User extends Component } /** - * 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]++; - } - } - $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. 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/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() From 5d6c9a4c9f6af732c7e317867d9df3d459ae7c9f Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Wed, 27 Mar 2013 14:47:47 -0400 Subject: [PATCH 05/41] Fixed session bug. --- framework/web/Session.php | 44 ++++++++++++++++++++++++++------------------ 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/framework/web/Session.php b/framework/web/Session.php index eefc1a8..840a26d 100644 --- a/framework/web/Session.php +++ b/framework/web/Session.php @@ -84,6 +84,8 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co return false; } + private $_opened = false; + /** * Starts the session. */ @@ -92,29 +94,34 @@ 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') + ); + } - @session_start(); + @session_start(); - if (session_id() == '') { - $error = error_get_last(); - $message = isset($error['message']) ? $error['message'] : 'Failed to start session.'; - Yii::error($message, __CLASS__); - } else { - $this->updateFlashCounters(); + if (session_id() == '') { + $this->_opened = false; + $error = error_get_last(); + $message = isset($error['message']) ? $error['message'] : 'Failed to start session.'; + Yii::error($message, __CLASS__); + } else { + $this->_opened = true; + $this->updateFlashCounters(); + } } } @@ -123,6 +130,7 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co */ public function close() { + $this->_opened = false; if (session_id() !== '') { @session_write_close(); } @@ -149,7 +157,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() !== ''; } } From e1acc64b2b5985806c6888c335d7a49a3279682d Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Wed, 27 Mar 2013 17:09:18 -0400 Subject: [PATCH 06/41] refactoring cache and db references. --- framework/base/Module.php | 1246 +++++++++---------- framework/caching/DbCache.php | 169 ++- framework/caching/DbDependency.php | 53 +- framework/console/Controller.php | 6 +- .../console/controllers/MigrateController.php | 1281 ++++++++++---------- framework/db/Command.php | 34 +- framework/db/Connection.php | 25 +- framework/db/Schema.php | 30 +- framework/logging/DbTarget.php | 67 +- framework/web/CacheSession.php | 58 +- framework/web/DbSession.php | 170 ++- framework/web/PageCache.php | 217 ++-- framework/web/UrlManager.php | 30 +- framework/widgets/FragmentCache.php | 395 +++--- tests/unit/framework/util/HtmlTest.php | 896 +++++++------- tests/unit/framework/web/UrlManagerTest.php | 19 +- tests/unit/framework/web/UrlRuleTest.php | 4 +- 17 files changed, 2292 insertions(+), 2408 deletions(-) diff --git a/framework/base/Module.php b/framework/base/Module.php index 9988164..6b82157 100644 --- a/framework/base/Module.php +++ b/framework/base/Module.php @@ -1,623 +1,623 @@ - 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", __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', + * '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", __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', + * '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); + } + } + } + + return isset($controller) ? array($controller, $route) : false; + } +} diff --git a/framework/caching/DbCache.php b/framework/caching/DbCache.php index 44d0d03..3952852 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) + ->from($this->cacheTable) ->where('id = :id AND (expire = 0 OR expire >' . time() . ')', array(':id' => $key)); - $db = $this->getDb(); - if ($db->enableQueryCache) { + 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() . ')'); - $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..cbe0ae1 100644 --- a/framework/caching/DbDependency.php +++ b/framework/caching/DbDependency.php @@ -23,9 +23,9 @@ 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. @@ -50,24 +50,17 @@ class DbDependency extends Dependency } /** - * 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); - } - - /** * Generates the data needed to determine if dependency has been changed. * This method returns the value of the global state. * @return mixed the data needed to determine if dependency has been changed. */ 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 +71,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/console/Controller.php b/framework/console/Controller.php index b9b0523..9924822 100644 --- a/framework/console/Controller.php +++ b/framework/console/Controller.php @@ -135,9 +135,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/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/Command.php b/framework/db/Command.php index c2b2e05..ecd3674 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. @@ -132,7 +134,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", __CLASS__); $errorInfo = $e instanceof \PDOException ? $e->errorInfo : null; throw new Exception($e->getMessage(), $errorInfo, (int)$e->getCode()); } @@ -264,7 +266,7 @@ class Command extends \yii\base\Component $paramLog = "\nParameters: " . var_export($this->_params, true); } - \Yii::trace("Executing SQL: {$sql}{$paramLog}", __CLASS__); + Yii::trace("Executing SQL: {$sql}{$paramLog}", __CLASS__); if ($sql == '') { return 0; @@ -272,7 +274,7 @@ class Command extends \yii\base\Component try { if ($this->db->enableProfiling) { - \Yii::beginProfile(__METHOD__ . "($sql)", __CLASS__); + Yii::beginProfile(__METHOD__ . "($sql)", __CLASS__); } $this->prepare(); @@ -280,16 +282,16 @@ class Command extends \yii\base\Component $n = $this->pdoStatement->rowCount(); if ($this->db->enableProfiling) { - \Yii::endProfile(__METHOD__ . "($sql)", __CLASS__); + Yii::endProfile(__METHOD__ . "($sql)", __CLASS__); } return $n; } catch (\Exception $e) { if ($this->db->enableProfiling) { - \Yii::endProfile(__METHOD__ . "($sql)", __CLASS__); + Yii::endProfile(__METHOD__ . "($sql)", __CLASS__); } $message = $e->getMessage(); - \Yii::error("$message\nFailed to execute SQL: {$sql}{$paramLog}", __CLASS__); + Yii::error("$message\nFailed to execute SQL: {$sql}{$paramLog}", __CLASS__); $errorInfo = $e instanceof \PDOException ? $e->errorInfo : null; throw new Exception($message, $errorInfo, (int)$e->getCode()); @@ -381,14 +383,14 @@ class Command extends \yii\base\Component $paramLog = "\nParameters: " . var_export($this->_params, true); } - \Yii::trace("Querying SQL: {$sql}{$paramLog}", __CLASS__); + Yii::trace("Querying SQL: {$sql}{$paramLog}", __CLASS__); /** @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, @@ -397,14 +399,14 @@ class Command extends \yii\base\Component $paramLog, )); if (($result = $cache->get($cacheKey)) !== false) { - \Yii::trace('Query result found in cache', __CLASS__); + Yii::trace('Query result served from cache', __CLASS__); return $result; } } try { if ($db->enableProfiling) { - \Yii::beginProfile(__METHOD__ . "($sql)", __CLASS__); + Yii::beginProfile(__METHOD__ . "($sql)", __CLASS__); } $this->prepare(); @@ -421,21 +423,21 @@ class Command extends \yii\base\Component } if ($db->enableProfiling) { - \Yii::endProfile(__METHOD__ . "($sql)", __CLASS__); + Yii::endProfile(__METHOD__ . "($sql)", __CLASS__); } - 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', __CLASS__); } return $result; } catch (\Exception $e) { if ($db->enableProfiling) { - \Yii::endProfile(__METHOD__ . "($sql)", __CLASS__); + Yii::endProfile(__METHOD__ . "($sql)", __CLASS__); } $message = $e->getMessage(); - \Yii::error("$message\nCommand::$method() failed: {$sql}{$paramLog}", __CLASS__); + Yii::error("$message\nCommand::$method() failed: {$sql}{$paramLog}", __CLASS__); $errorInfo = $e instanceof \PDOException ? $e->errorInfo : null; throw new Exception($message, $errorInfo, (int)$e->getCode()); } diff --git a/framework/db/Connection.php b/framework/db/Connection.php index 40164a3..59e8422 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 @@ -290,7 +291,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. */ diff --git a/framework/db/Schema.php b/framework/db/Schema.php index 5fe6121..71bc9a2 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; @@ -84,21 +85,21 @@ abstract class Schema extends \yii\base\Object $db = $this->db; $realName = $this->getRealTableName($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)); } diff --git a/framework/logging/DbTarget.php b/framework/logging/DbTarget.php index 364b5a4..e4e30ce 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,9 @@ class DbTarget extends Target */ public function export($messages) { - $db = $this->getDb(); - $tableName = $db->quoteTableName($this->tableName); + $tableName = $this->db->quoteTableName($this->logTable); $sql = "INSERT INTO $tableName (level, category, log_time, message) VALUES (:level, :category, :log_time, :message)"; - $command = $db->createCommand($sql); + $command = $this->db->createCommand($sql); foreach ($messages as $message) { $command->bindValues(array( ':level' => $message[1], 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/DbSession.php b/framework/web/DbSession.php index 812185a..d3afc76 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) + ->from($this->sessionTable) ->where('expire>:expire AND id=:id', array(':expire' => time(), ':id' => $id)) - ->createCommand($this->getDb()) + ->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/PageCache.php b/framework/web/PageCache.php index 24cddea..29c8cc8 100644 --- a/framework/web/PageCache.php +++ b/framework/web/PageCache.php @@ -1,110 +1,109 @@ - - * @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 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 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/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/widgets/FragmentCache.php b/framework/widgets/FragmentCache.php index d5185f8..65bb86b 100644 --- a/framework/widgets/FragmentCache.php +++ b/framework/widgets/FragmentCache.php @@ -1,213 +1,184 @@ - - * @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 \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; + + /** + * Initializes the FragmentCache object. + */ + public function init() + { + parent::init(); + + if ($this->view === null) { + $this->view = Yii::$app->getView(); + } + + 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/framework/util/HtmlTest.php b/tests/unit/framework/util/HtmlTest.php index eba1a20..e628423 100644 --- a/tests/unit/framework/util/HtmlTest.php +++ b/tests/unit/framework/util/HtmlTest.php @@ -1,448 +1,448 @@ - array( - 'request' => array( - 'class' => 'yii\web\Request', - 'url' => '/test', - ), - ), - )); - } - - public function tearDown() - { - Yii::$app = null; - } - - public function testEncode() - { - $this->assertEquals("a<>&"'", Html::encode("a<>&\"'")); - } - - public function testDecode() - { - $this->assertEquals("a<>&\"'", Html::decode("a<>&"'")); - } - - public function testTag() - { - $this->assertEquals('
', Html::tag('br')); - $this->assertEquals('', Html::tag('span')); - $this->assertEquals('
content
', Html::tag('div', 'content')); - $this->assertEquals('', Html::tag('input', '', array('type' => 'text', 'name' => 'test', 'value' => '<>'))); - - Html::$closeVoidElements = false; - - $this->assertEquals('
', Html::tag('br')); - $this->assertEquals('', Html::tag('span')); - $this->assertEquals('
content
', Html::tag('div', 'content')); - $this->assertEquals('', Html::tag('input', '', array('type' => 'text', 'name' => 'test', 'value' => '<>'))); - - Html::$closeVoidElements = true; - - $this->assertEquals('', Html::tag('span', '', array('disabled' => true))); - Html::$showBooleanAttributeValues = false; - $this->assertEquals('', Html::tag('span', '', array('disabled' => true))); - Html::$showBooleanAttributeValues = true; - } - - public function testBeginTag() - { - $this->assertEquals('
', Html::beginTag('br')); - $this->assertEquals('', Html::beginTag('span', array('id' => 'test', 'class' => 'title'))); - } - - public function testEndTag() - { - $this->assertEquals('
', Html::endTag('br')); - $this->assertEquals('
', Html::endTag('span')); - } - - public function testCdata() - { - $data = 'test<>'; - $this->assertEquals('', Html::cdata($data)); - } - - public function testStyle() - { - $content = 'a <>'; - $this->assertEquals("", Html::style($content)); - $this->assertEquals("", Html::style($content, array('type' => 'text/less'))); - } - - public function testScript() - { - $content = 'a <>'; - $this->assertEquals("", Html::script($content)); - $this->assertEquals("", Html::script($content, array('type' => 'text/js'))); - } - - public function testCssFile() - { - $this->assertEquals('', Html::cssFile('http://example.com')); - $this->assertEquals('', Html::cssFile('')); - } - - public function testJsFile() - { - $this->assertEquals('', Html::jsFile('http://example.com')); - $this->assertEquals('', Html::jsFile('')); - } - - public function testBeginForm() - { - $this->assertEquals('
', Html::beginForm()); - $this->assertEquals('', Html::beginForm('/example', 'get')); - $hiddens = array( - '', - '', - ); - $this->assertEquals('' . "\n" . implode("\n", $hiddens), Html::beginForm('/example?id=1&title=%3C', 'get')); - } - - public function testEndForm() - { - $this->assertEquals('
', Html::endForm()); - } - - public function testA() - { - $this->assertEquals('something<>', Html::a('something<>')); - $this->assertEquals('something', Html::a('something', '/example')); - $this->assertEquals('something', Html::a('something', '')); - } - - public function testMailto() - { - $this->assertEquals('test<>', Html::mailto('test<>')); - $this->assertEquals('test<>', Html::mailto('test<>', 'test>')); - } - - public function testImg() - { - $this->assertEquals('', Html::img('/example')); - $this->assertEquals('', Html::img('')); - $this->assertEquals('something', Html::img('/example', array('alt' => 'something', 'width' => 10))); - } - - public function testLabel() - { - $this->assertEquals('', Html::label('something<>')); - $this->assertEquals('', Html::label('something<>', 'a')); - $this->assertEquals('', Html::label('something<>', 'a', array('class' => 'test'))); - } - - public function testButton() - { - $this->assertEquals('', Html::button()); - $this->assertEquals('', Html::button('test', 'value', 'content<>')); - $this->assertEquals('', Html::button('test', 'value', 'content<>', array('type' => 'submit', 'class' => "t"))); - } - - public function testSubmitButton() - { - $this->assertEquals('', Html::submitButton()); - $this->assertEquals('', Html::submitButton('test', 'value', 'content<>', array('class' => 't'))); - } - - public function testResetButton() - { - $this->assertEquals('', Html::resetButton()); - $this->assertEquals('', Html::resetButton('test', 'value', 'content<>', array('class' => 't'))); - } - - public function testInput() - { - $this->assertEquals('', Html::input('text')); - $this->assertEquals('', Html::input('text', 'test', 'value', array('class' => 't'))); - } - - public function testButtonInput() - { - $this->assertEquals('', Html::buttonInput('test')); - $this->assertEquals('', Html::buttonInput('test', 'text', array('class' => 'a'))); - } - - public function testSubmitInput() - { - $this->assertEquals('', Html::submitInput()); - $this->assertEquals('', Html::submitInput('test', 'text', array('class' => 'a'))); - } - - public function testResetInput() - { - $this->assertEquals('', Html::resetInput()); - $this->assertEquals('', Html::resetInput('test', 'text', array('class' => 'a'))); - } - - public function testTextInput() - { - $this->assertEquals('', Html::textInput('test')); - $this->assertEquals('', Html::textInput('test', 'value', array('class' => 't'))); - } - - public function testHiddenInput() - { - $this->assertEquals('', Html::hiddenInput('test')); - $this->assertEquals('', Html::hiddenInput('test', 'value', array('class' => 't'))); - } - - public function testPasswordInput() - { - $this->assertEquals('', Html::passwordInput('test')); - $this->assertEquals('', Html::passwordInput('test', 'value', array('class' => 't'))); - } - - public function testFileInput() - { - $this->assertEquals('', Html::fileInput('test')); - $this->assertEquals('', Html::fileInput('test', 'value', array('class' => 't'))); - } - - public function testTextarea() - { - $this->assertEquals('', Html::textarea('test')); - $this->assertEquals('', Html::textarea('test', 'value<>', array('class' => 't'))); - } - - public function testRadio() - { - $this->assertEquals('', Html::radio('test')); - $this->assertEquals('', Html::radio('test', true, null, array('class' => 'a'))); - $this->assertEquals('', Html::radio('test', true, 2, array('class' => 'a' , 'uncheck' => '0'))); - } - - public function testCheckbox() - { - $this->assertEquals('', Html::checkbox('test')); - $this->assertEquals('', Html::checkbox('test', true, null, array('class' => 'a'))); - $this->assertEquals('', Html::checkbox('test', true, 2, array('class' => 'a', 'uncheck' => '0'))); - } - - public function testDropDownList() - { - $expected = << - - -EOD; - $this->assertEquals($expected, Html::dropDownList('test')); - $expected = << - - - -EOD; - $this->assertEquals($expected, Html::dropDownList('test', null, $this->getDataItems())); - $expected = << - - - -EOD; - $this->assertEquals($expected, Html::dropDownList('test', 'value2', $this->getDataItems())); - } - - public function testListBox() - { - $expected = << - - -EOD; - $this->assertEquals($expected, Html::listBox('test')); - $expected = << - - - -EOD; - $this->assertEquals($expected, Html::listBox('test', null, $this->getDataItems(), array('size' => 5))); - $expected = << - - - -EOD; - $this->assertEquals($expected, Html::listBox('test', null, $this->getDataItems2())); - $expected = << - - - -EOD; - $this->assertEquals($expected, Html::listBox('test', 'value2', $this->getDataItems())); - $expected = << - - - -EOD; - $this->assertEquals($expected, Html::listBox('test', array('value1', 'value2'), $this->getDataItems())); - - $expected = << - - -EOD; - $this->assertEquals($expected, Html::listBox('test', null, array(), array('multiple' => true))); - $expected = << -EOD; - $this->assertEquals($expected, Html::listBox('test', '', array(), array('unselect' => '0'))); - } - - public function testCheckboxList() - { - $this->assertEquals('', Html::checkboxList('test')); - - $expected = << text1 - -EOD; - $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems())); - - $expected = << text1<> - -EOD; - $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems2())); - - $expected = <<
- -EOD; - $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems(), array( - 'separator' => "
\n", - 'unselect' => '0', - ))); - - $expected = <<text1 -1 -EOD; - $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems(), array( - 'item' => function ($index, $label, $name, $checked, $value) { - return $index . Html::label($label . ' ' . Html::checkbox($name, $checked, $value)); - } - ))); - } - - public function testRadioList() - { - $this->assertEquals('', Html::radioList('test')); - - $expected = << text1 - -EOD; - $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems())); - - $expected = << text1<> - -EOD; - $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems2())); - - $expected = <<
- -EOD; - $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems(), array( - 'separator' => "
\n", - 'unselect' => '0', - ))); - - $expected = <<text1 -1 -EOD; - $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems(), array( - 'item' => function ($index, $label, $name, $checked, $value) { - return $index . Html::label($label . ' ' . Html::radio($name, $checked, $value)); - } - ))); - } - - public function testRenderOptions() - { - $data = array( - 'value1' => 'label1', - 'group1' => array( - 'value11' => 'label11', - 'group11' => array( - 'value111' => 'label111', - ), - 'group12' => array(), - ), - 'value2' => 'label2', - 'group2' => array(), - ); - $expected = <<please select<> - - - - - - - - - - - - - - -EOD; - $attributes = array( - 'prompt' => 'please select<>', - 'options' => array( - 'value111' => array('class' => 'option'), - ), - 'groups' => array( - 'group12' => array('class' => 'group'), - ), - ); - $this->assertEquals($expected, Html::renderSelectOptions(array('value111', 'value1'), $data, $attributes)); - } - - public function testRenderAttributes() - { - $this->assertEquals('', Html::renderTagAttributes(array())); - $this->assertEquals(' name="test" value="1<>"', Html::renderTagAttributes(array('name' => 'test', 'empty' => null, 'value' => '1<>'))); - Html::$showBooleanAttributeValues = false; - $this->assertEquals(' checked disabled', Html::renderTagAttributes(array('checked' => 'checked', 'disabled' => true, 'hidden' => false))); - Html::$showBooleanAttributeValues = true; - } - - protected function getDataItems() - { - return array( - 'value1' => 'text1', - 'value2' => 'text2', - ); - } - - protected function getDataItems2() - { - return array( - 'value1<>' => 'text1<>', - 'value 2' => 'text 2', - ); - } -} + array( + 'request' => array( + 'class' => 'yii\web\Request', + 'url' => '/test', + ), + ), + )); + } + + public function tearDown() + { + Yii::$app = null; + } + + public function testEncode() + { + $this->assertEquals("a<>&"'", Html::encode("a<>&\"'")); + } + + public function testDecode() + { + $this->assertEquals("a<>&\"'", Html::decode("a<>&"'")); + } + + public function testTag() + { + $this->assertEquals('
', Html::tag('br')); + $this->assertEquals('', Html::tag('span')); + $this->assertEquals('
content
', Html::tag('div', 'content')); + $this->assertEquals('', Html::tag('input', '', array('type' => 'text', 'name' => 'test', 'value' => '<>'))); + + Html::$closeVoidElements = false; + + $this->assertEquals('
', Html::tag('br')); + $this->assertEquals('', Html::tag('span')); + $this->assertEquals('
content
', Html::tag('div', 'content')); + $this->assertEquals('', Html::tag('input', '', array('type' => 'text', 'name' => 'test', 'value' => '<>'))); + + Html::$closeVoidElements = true; + + $this->assertEquals('', Html::tag('span', '', array('disabled' => true))); + Html::$showBooleanAttributeValues = false; + $this->assertEquals('', Html::tag('span', '', array('disabled' => true))); + Html::$showBooleanAttributeValues = true; + } + + public function testBeginTag() + { + $this->assertEquals('
', Html::beginTag('br')); + $this->assertEquals('', Html::beginTag('span', array('id' => 'test', 'class' => 'title'))); + } + + public function testEndTag() + { + $this->assertEquals('
', Html::endTag('br')); + $this->assertEquals('
', Html::endTag('span')); + } + + public function testCdata() + { + $data = 'test<>'; + $this->assertEquals('', Html::cdata($data)); + } + + public function testStyle() + { + $content = 'a <>'; + $this->assertEquals("", Html::style($content)); + $this->assertEquals("", Html::style($content, array('type' => 'text/less'))); + } + + public function testScript() + { + $content = 'a <>'; + $this->assertEquals("", Html::script($content)); + $this->assertEquals("", Html::script($content, array('type' => 'text/js'))); + } + + public function testCssFile() + { + $this->assertEquals('', Html::cssFile('http://example.com')); + $this->assertEquals('', Html::cssFile('')); + } + + public function testJsFile() + { + $this->assertEquals('', Html::jsFile('http://example.com')); + $this->assertEquals('', Html::jsFile('')); + } + + public function testBeginForm() + { + $this->assertEquals('
', Html::beginForm()); + $this->assertEquals('', Html::beginForm('/example', 'get')); + $hiddens = array( + '', + '', + ); + $this->assertEquals('' . "\n" . implode("\n", $hiddens), Html::beginForm('/example?id=1&title=%3C', 'get')); + } + + public function testEndForm() + { + $this->assertEquals('
', Html::endForm()); + } + + public function testA() + { + $this->assertEquals('something<>', Html::a('something<>')); + $this->assertEquals('something', Html::a('something', '/example')); + $this->assertEquals('something', Html::a('something', '')); + } + + public function testMailto() + { + $this->assertEquals('test<>', Html::mailto('test<>')); + $this->assertEquals('test<>', Html::mailto('test<>', 'test>')); + } + + public function testImg() + { + $this->assertEquals('', Html::img('/example')); + $this->assertEquals('', Html::img('')); + $this->assertEquals('something', Html::img('/example', array('alt' => 'something', 'width' => 10))); + } + + public function testLabel() + { + $this->assertEquals('', Html::label('something<>')); + $this->assertEquals('', Html::label('something<>', 'a')); + $this->assertEquals('', Html::label('something<>', 'a', array('class' => 'test'))); + } + + public function testButton() + { + $this->assertEquals('', Html::button()); + $this->assertEquals('', Html::button('test', 'value', 'content<>')); + $this->assertEquals('', Html::button('test', 'value', 'content<>', array('type' => 'submit', 'class' => "t"))); + } + + public function testSubmitButton() + { + $this->assertEquals('', Html::submitButton()); + $this->assertEquals('', Html::submitButton('test', 'value', 'content<>', array('class' => 't'))); + } + + public function testResetButton() + { + $this->assertEquals('', Html::resetButton()); + $this->assertEquals('', Html::resetButton('test', 'value', 'content<>', array('class' => 't'))); + } + + public function testInput() + { + $this->assertEquals('', Html::input('text')); + $this->assertEquals('', Html::input('text', 'test', 'value', array('class' => 't'))); + } + + public function testButtonInput() + { + $this->assertEquals('', Html::buttonInput('test')); + $this->assertEquals('', Html::buttonInput('test', 'text', array('class' => 'a'))); + } + + public function testSubmitInput() + { + $this->assertEquals('', Html::submitInput()); + $this->assertEquals('', Html::submitInput('test', 'text', array('class' => 'a'))); + } + + public function testResetInput() + { + $this->assertEquals('', Html::resetInput()); + $this->assertEquals('', Html::resetInput('test', 'text', array('class' => 'a'))); + } + + public function testTextInput() + { + $this->assertEquals('', Html::textInput('test')); + $this->assertEquals('', Html::textInput('test', 'value', array('class' => 't'))); + } + + public function testHiddenInput() + { + $this->assertEquals('', Html::hiddenInput('test')); + $this->assertEquals('', Html::hiddenInput('test', 'value', array('class' => 't'))); + } + + public function testPasswordInput() + { + $this->assertEquals('', Html::passwordInput('test')); + $this->assertEquals('', Html::passwordInput('test', 'value', array('class' => 't'))); + } + + public function testFileInput() + { + $this->assertEquals('', Html::fileInput('test')); + $this->assertEquals('', Html::fileInput('test', 'value', array('class' => 't'))); + } + + public function testTextarea() + { + $this->assertEquals('', Html::textarea('test')); + $this->assertEquals('', Html::textarea('test', 'value<>', array('class' => 't'))); + } + + public function testRadio() + { + $this->assertEquals('', Html::radio('test')); + $this->assertEquals('', Html::radio('test', true, null, array('class' => 'a'))); + $this->assertEquals('', Html::radio('test', true, 2, array('class' => 'a' , 'uncheck' => '0'))); + } + + public function testCheckbox() + { + $this->assertEquals('', Html::checkbox('test')); + $this->assertEquals('', Html::checkbox('test', true, null, array('class' => 'a'))); + $this->assertEquals('', Html::checkbox('test', true, 2, array('class' => 'a', 'uncheck' => '0'))); + } + + public function testDropDownList() + { + $expected = << + + +EOD; + $this->assertEquals($expected, Html::dropDownList('test')); + $expected = << + + + +EOD; + $this->assertEquals($expected, Html::dropDownList('test', null, $this->getDataItems())); + $expected = << + + + +EOD; + $this->assertEquals($expected, Html::dropDownList('test', 'value2', $this->getDataItems())); + } + + public function testListBox() + { + $expected = << + + +EOD; + $this->assertEquals($expected, Html::listBox('test')); + $expected = << + + + +EOD; + $this->assertEquals($expected, Html::listBox('test', null, $this->getDataItems(), array('size' => 5))); + $expected = << + + + +EOD; + $this->assertEquals($expected, Html::listBox('test', null, $this->getDataItems2())); + $expected = << + + + +EOD; + $this->assertEquals($expected, Html::listBox('test', 'value2', $this->getDataItems())); + $expected = << + + + +EOD; + $this->assertEquals($expected, Html::listBox('test', array('value1', 'value2'), $this->getDataItems())); + + $expected = << + + +EOD; + $this->assertEquals($expected, Html::listBox('test', null, array(), array('multiple' => true))); + $expected = << +EOD; + $this->assertEquals($expected, Html::listBox('test', '', array(), array('unselect' => '0'))); + } + + public function testCheckboxList() + { + $this->assertEquals('', Html::checkboxList('test')); + + $expected = << text1 + +EOD; + $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems())); + + $expected = << text1<> + +EOD; + $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems2())); + + $expected = <<
+ +EOD; + $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems(), array( + 'separator' => "
\n", + 'unselect' => '0', + ))); + + $expected = <<text1 +1 +EOD; + $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems(), array( + 'item' => function ($index, $label, $name, $checked, $value) { + return $index . Html::label($label . ' ' . Html::checkbox($name, $checked, $value)); + } + ))); + } + + public function testRadioList() + { + $this->assertEquals('', Html::radioList('test')); + + $expected = << text1 + +EOD; + $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems())); + + $expected = << text1<> + +EOD; + $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems2())); + + $expected = <<
+ +EOD; + $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems(), array( + 'separator' => "
\n", + 'unselect' => '0', + ))); + + $expected = <<text1 +1 +EOD; + $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems(), array( + 'item' => function ($index, $label, $name, $checked, $value) { + return $index . Html::label($label . ' ' . Html::radio($name, $checked, $value)); + } + ))); + } + + public function testRenderOptions() + { + $data = array( + 'value1' => 'label1', + 'group1' => array( + 'value11' => 'label11', + 'group11' => array( + 'value111' => 'label111', + ), + 'group12' => array(), + ), + 'value2' => 'label2', + 'group2' => array(), + ); + $expected = <<please select<> + + + + + + + + + + + + + + +EOD; + $attributes = array( + 'prompt' => 'please select<>', + 'options' => array( + 'value111' => array('class' => 'option'), + ), + 'groups' => array( + 'group12' => array('class' => 'group'), + ), + ); + $this->assertEquals($expected, Html::renderSelectOptions(array('value111', 'value1'), $data, $attributes)); + } + + public function testRenderAttributes() + { + $this->assertEquals('', Html::renderTagAttributes(array())); + $this->assertEquals(' name="test" value="1<>"', Html::renderTagAttributes(array('name' => 'test', 'empty' => null, 'value' => '1<>'))); + Html::$showBooleanAttributeValues = false; + $this->assertEquals(' checked disabled', Html::renderTagAttributes(array('checked' => 'checked', 'disabled' => true, 'hidden' => false))); + Html::$showBooleanAttributeValues = true; + } + + protected function getDataItems() + { + return array( + 'value1' => 'text1', + 'value2' => 'text2', + ); + } + + protected function getDataItems2() + { + return array( + 'value1<>' => 'text1<>', + 'value 2' => 'text 2', + ); + } +} 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) { From 74d4e04d95f2ab4226804e086d4d3cd4d3442ce9 Mon Sep 17 00:00:00 2001 From: Qiang Xue <qiang.xue@gmail.com> Date: Wed, 27 Mar 2013 17:37:45 -0400 Subject: [PATCH 07/41] line ending fix. --- tests/unit/framework/util/HtmlTest.php | 896 ++++++++++++++++----------------- 1 file changed, 448 insertions(+), 448 deletions(-) diff --git a/tests/unit/framework/util/HtmlTest.php b/tests/unit/framework/util/HtmlTest.php index e628423..eba1a20 100644 --- a/tests/unit/framework/util/HtmlTest.php +++ b/tests/unit/framework/util/HtmlTest.php @@ -1,448 +1,448 @@ -<?php - -namespace yiiunit\framework\util; - -use Yii; -use yii\helpers\Html; -use yii\web\Application; - -class HtmlTest extends \yii\test\TestCase -{ - public function setUp() - { - new Application('test', '@yiiunit/runtime', array( - 'components' => array( - 'request' => array( - 'class' => 'yii\web\Request', - 'url' => '/test', - ), - ), - )); - } - - public function tearDown() - { - Yii::$app = null; - } - - public function testEncode() - { - $this->assertEquals("a<>&"'", Html::encode("a<>&\"'")); - } - - public function testDecode() - { - $this->assertEquals("a<>&\"'", Html::decode("a<>&"'")); - } - - public function testTag() - { - $this->assertEquals('<br />', Html::tag('br')); - $this->assertEquals('<span></span>', Html::tag('span')); - $this->assertEquals('<div>content</div>', Html::tag('div', 'content')); - $this->assertEquals('<input type="text" name="test" value="<>" />', Html::tag('input', '', array('type' => 'text', 'name' => 'test', 'value' => '<>'))); - - Html::$closeVoidElements = false; - - $this->assertEquals('<br>', Html::tag('br')); - $this->assertEquals('<span></span>', Html::tag('span')); - $this->assertEquals('<div>content</div>', Html::tag('div', 'content')); - $this->assertEquals('<input type="text" name="test" value="<>">', Html::tag('input', '', array('type' => 'text', 'name' => 'test', 'value' => '<>'))); - - Html::$closeVoidElements = true; - - $this->assertEquals('<span disabled="disabled"></span>', Html::tag('span', '', array('disabled' => true))); - Html::$showBooleanAttributeValues = false; - $this->assertEquals('<span disabled></span>', Html::tag('span', '', array('disabled' => true))); - Html::$showBooleanAttributeValues = true; - } - - public function testBeginTag() - { - $this->assertEquals('<br>', Html::beginTag('br')); - $this->assertEquals('<span id="test" class="title">', Html::beginTag('span', array('id' => 'test', 'class' => 'title'))); - } - - public function testEndTag() - { - $this->assertEquals('</br>', Html::endTag('br')); - $this->assertEquals('</span>', Html::endTag('span')); - } - - public function testCdata() - { - $data = 'test<>'; - $this->assertEquals('<![CDATA[' . $data . ']]>', Html::cdata($data)); - } - - public function testStyle() - { - $content = 'a <>'; - $this->assertEquals("<style type=\"text/css\">/*<![CDATA[*/\n{$content}\n/*]]>*/</style>", Html::style($content)); - $this->assertEquals("<style type=\"text/less\">/*<![CDATA[*/\n{$content}\n/*]]>*/</style>", Html::style($content, array('type' => 'text/less'))); - } - - public function testScript() - { - $content = 'a <>'; - $this->assertEquals("<script type=\"text/javascript\">/*<![CDATA[*/\n{$content}\n/*]]>*/</script>", Html::script($content)); - $this->assertEquals("<script type=\"text/js\">/*<![CDATA[*/\n{$content}\n/*]]>*/</script>", Html::script($content, array('type' => 'text/js'))); - } - - public function testCssFile() - { - $this->assertEquals('<link type="text/css" href="http://example.com" rel="stylesheet" />', Html::cssFile('http://example.com')); - $this->assertEquals('<link type="text/css" href="/test" rel="stylesheet" />', Html::cssFile('')); - } - - public function testJsFile() - { - $this->assertEquals('<script type="text/javascript" src="http://example.com"></script>', Html::jsFile('http://example.com')); - $this->assertEquals('<script type="text/javascript" src="/test"></script>', Html::jsFile('')); - } - - public function testBeginForm() - { - $this->assertEquals('<form action="/test" method="post">', Html::beginForm()); - $this->assertEquals('<form action="/example" method="get">', Html::beginForm('/example', 'get')); - $hiddens = array( - '<input type="hidden" name="id" value="1" />', - '<input type="hidden" name="title" value="<" />', - ); - $this->assertEquals('<form action="/example" method="get">' . "\n" . implode("\n", $hiddens), Html::beginForm('/example?id=1&title=%3C', 'get')); - } - - public function testEndForm() - { - $this->assertEquals('</form>', Html::endForm()); - } - - public function testA() - { - $this->assertEquals('<a>something<></a>', Html::a('something<>')); - $this->assertEquals('<a href="/example">something</a>', Html::a('something', '/example')); - $this->assertEquals('<a href="/test">something</a>', Html::a('something', '')); - } - - public function testMailto() - { - $this->assertEquals('<a href="mailto:test<>">test<></a>', Html::mailto('test<>')); - $this->assertEquals('<a href="mailto:test>">test<></a>', Html::mailto('test<>', 'test>')); - } - - public function testImg() - { - $this->assertEquals('<img src="/example" alt="" />', Html::img('/example')); - $this->assertEquals('<img src="/test" alt="" />', Html::img('')); - $this->assertEquals('<img src="/example" width="10" alt="something" />', Html::img('/example', array('alt' => 'something', 'width' => 10))); - } - - public function testLabel() - { - $this->assertEquals('<label>something<></label>', Html::label('something<>')); - $this->assertEquals('<label for="a">something<></label>', Html::label('something<>', 'a')); - $this->assertEquals('<label class="test" for="a">something<></label>', Html::label('something<>', 'a', array('class' => 'test'))); - } - - public function testButton() - { - $this->assertEquals('<button type="button">Button</button>', Html::button()); - $this->assertEquals('<button type="button" name="test" value="value">content<></button>', Html::button('test', 'value', 'content<>')); - $this->assertEquals('<button type="submit" class="t" name="test" value="value">content<></button>', Html::button('test', 'value', 'content<>', array('type' => 'submit', 'class' => "t"))); - } - - public function testSubmitButton() - { - $this->assertEquals('<button type="submit">Submit</button>', Html::submitButton()); - $this->assertEquals('<button type="submit" class="t" name="test" value="value">content<></button>', Html::submitButton('test', 'value', 'content<>', array('class' => 't'))); - } - - public function testResetButton() - { - $this->assertEquals('<button type="reset">Reset</button>', Html::resetButton()); - $this->assertEquals('<button type="reset" class="t" name="test" value="value">content<></button>', Html::resetButton('test', 'value', 'content<>', array('class' => 't'))); - } - - public function testInput() - { - $this->assertEquals('<input type="text" />', Html::input('text')); - $this->assertEquals('<input type="text" class="t" name="test" value="value" />', Html::input('text', 'test', 'value', array('class' => 't'))); - } - - public function testButtonInput() - { - $this->assertEquals('<input type="button" name="test" value="Button" />', Html::buttonInput('test')); - $this->assertEquals('<input type="button" class="a" name="test" value="text" />', Html::buttonInput('test', 'text', array('class' => 'a'))); - } - - public function testSubmitInput() - { - $this->assertEquals('<input type="submit" value="Submit" />', Html::submitInput()); - $this->assertEquals('<input type="submit" class="a" name="test" value="text" />', Html::submitInput('test', 'text', array('class' => 'a'))); - } - - public function testResetInput() - { - $this->assertEquals('<input type="reset" value="Reset" />', Html::resetInput()); - $this->assertEquals('<input type="reset" class="a" name="test" value="text" />', Html::resetInput('test', 'text', array('class' => 'a'))); - } - - public function testTextInput() - { - $this->assertEquals('<input type="text" name="test" />', Html::textInput('test')); - $this->assertEquals('<input type="text" class="t" name="test" value="value" />', Html::textInput('test', 'value', array('class' => 't'))); - } - - public function testHiddenInput() - { - $this->assertEquals('<input type="hidden" name="test" />', Html::hiddenInput('test')); - $this->assertEquals('<input type="hidden" class="t" name="test" value="value" />', Html::hiddenInput('test', 'value', array('class' => 't'))); - } - - public function testPasswordInput() - { - $this->assertEquals('<input type="password" name="test" />', Html::passwordInput('test')); - $this->assertEquals('<input type="password" class="t" name="test" value="value" />', Html::passwordInput('test', 'value', array('class' => 't'))); - } - - public function testFileInput() - { - $this->assertEquals('<input type="file" name="test" />', Html::fileInput('test')); - $this->assertEquals('<input type="file" class="t" name="test" value="value" />', Html::fileInput('test', 'value', array('class' => 't'))); - } - - public function testTextarea() - { - $this->assertEquals('<textarea name="test"></textarea>', Html::textarea('test')); - $this->assertEquals('<textarea class="t" name="test">value<></textarea>', Html::textarea('test', 'value<>', array('class' => 't'))); - } - - public function testRadio() - { - $this->assertEquals('<input type="radio" name="test" value="1" />', Html::radio('test')); - $this->assertEquals('<input type="radio" class="a" name="test" checked="checked" />', Html::radio('test', true, null, array('class' => 'a'))); - $this->assertEquals('<input type="hidden" name="test" value="0" /><input type="radio" class="a" name="test" value="2" checked="checked" />', Html::radio('test', true, 2, array('class' => 'a' , 'uncheck' => '0'))); - } - - public function testCheckbox() - { - $this->assertEquals('<input type="checkbox" name="test" value="1" />', Html::checkbox('test')); - $this->assertEquals('<input type="checkbox" class="a" name="test" checked="checked" />', Html::checkbox('test', true, null, array('class' => 'a'))); - $this->assertEquals('<input type="hidden" name="test" value="0" /><input type="checkbox" class="a" name="test" value="2" checked="checked" />', Html::checkbox('test', true, 2, array('class' => 'a', 'uncheck' => '0'))); - } - - public function testDropDownList() - { - $expected = <<<EOD -<select name="test"> - -</select> -EOD; - $this->assertEquals($expected, Html::dropDownList('test')); - $expected = <<<EOD -<select name="test"> -<option value="value1">text1</option> -<option value="value2">text2</option> -</select> -EOD; - $this->assertEquals($expected, Html::dropDownList('test', null, $this->getDataItems())); - $expected = <<<EOD -<select name="test"> -<option value="value1">text1</option> -<option value="value2" selected="selected">text2</option> -</select> -EOD; - $this->assertEquals($expected, Html::dropDownList('test', 'value2', $this->getDataItems())); - } - - public function testListBox() - { - $expected = <<<EOD -<select name="test" size="4"> - -</select> -EOD; - $this->assertEquals($expected, Html::listBox('test')); - $expected = <<<EOD -<select name="test" size="5"> -<option value="value1">text1</option> -<option value="value2">text2</option> -</select> -EOD; - $this->assertEquals($expected, Html::listBox('test', null, $this->getDataItems(), array('size' => 5))); - $expected = <<<EOD -<select name="test" size="4"> -<option value="value1<>">text1<></option> -<option value="value 2">text  2</option> -</select> -EOD; - $this->assertEquals($expected, Html::listBox('test', null, $this->getDataItems2())); - $expected = <<<EOD -<select name="test" size="4"> -<option value="value1">text1</option> -<option value="value2" selected="selected">text2</option> -</select> -EOD; - $this->assertEquals($expected, Html::listBox('test', 'value2', $this->getDataItems())); - $expected = <<<EOD -<select name="test" size="4"> -<option value="value1" selected="selected">text1</option> -<option value="value2" selected="selected">text2</option> -</select> -EOD; - $this->assertEquals($expected, Html::listBox('test', array('value1', 'value2'), $this->getDataItems())); - - $expected = <<<EOD -<select name="test[]" multiple="multiple" size="4"> - -</select> -EOD; - $this->assertEquals($expected, Html::listBox('test', null, array(), array('multiple' => true))); - $expected = <<<EOD -<input type="hidden" name="test" value="0" /><select name="test" size="4"> - -</select> -EOD; - $this->assertEquals($expected, Html::listBox('test', '', array(), array('unselect' => '0'))); - } - - public function testCheckboxList() - { - $this->assertEquals('', Html::checkboxList('test')); - - $expected = <<<EOD -<label><input type="checkbox" name="test[]" value="value1" /> text1</label> -<label><input type="checkbox" name="test[]" value="value2" checked="checked" /> text2</label> -EOD; - $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems())); - - $expected = <<<EOD -<label><input type="checkbox" name="test[]" value="value1<>" /> text1<></label> -<label><input type="checkbox" name="test[]" value="value 2" /> text 2</label> -EOD; - $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems2())); - - $expected = <<<EOD -<input type="hidden" name="test" value="0" /><label><input type="checkbox" name="test[]" value="value1" /> text1</label><br /> -<label><input type="checkbox" name="test[]" value="value2" checked="checked" /> text2</label> -EOD; - $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems(), array( - 'separator' => "<br />\n", - 'unselect' => '0', - ))); - - $expected = <<<EOD -0<label>text1 <input type="checkbox" name="test[]" value="value1" /></label> -1<label>text2 <input type="checkbox" name="test[]" value="value2" checked="checked" /></label> -EOD; - $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems(), array( - 'item' => function ($index, $label, $name, $checked, $value) { - return $index . Html::label($label . ' ' . Html::checkbox($name, $checked, $value)); - } - ))); - } - - public function testRadioList() - { - $this->assertEquals('', Html::radioList('test')); - - $expected = <<<EOD -<label><input type="radio" name="test" value="value1" /> text1</label> -<label><input type="radio" name="test" value="value2" checked="checked" /> text2</label> -EOD; - $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems())); - - $expected = <<<EOD -<label><input type="radio" name="test" value="value1<>" /> text1<></label> -<label><input type="radio" name="test" value="value 2" /> text 2</label> -EOD; - $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems2())); - - $expected = <<<EOD -<input type="hidden" name="test" value="0" /><label><input type="radio" name="test" value="value1" /> text1</label><br /> -<label><input type="radio" name="test" value="value2" checked="checked" /> text2</label> -EOD; - $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems(), array( - 'separator' => "<br />\n", - 'unselect' => '0', - ))); - - $expected = <<<EOD -0<label>text1 <input type="radio" name="test" value="value1" /></label> -1<label>text2 <input type="radio" name="test" value="value2" checked="checked" /></label> -EOD; - $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems(), array( - 'item' => function ($index, $label, $name, $checked, $value) { - return $index . Html::label($label . ' ' . Html::radio($name, $checked, $value)); - } - ))); - } - - public function testRenderOptions() - { - $data = array( - 'value1' => 'label1', - 'group1' => array( - 'value11' => 'label11', - 'group11' => array( - 'value111' => 'label111', - ), - 'group12' => array(), - ), - 'value2' => 'label2', - 'group2' => array(), - ); - $expected = <<<EOD -<option value="">please select<></option> -<option value="value1" selected="selected">label1</option> -<optgroup label="group1"> -<option value="value11">label11</option> -<optgroup label="group11"> -<option class="option" value="value111" selected="selected">label111</option> -</optgroup> -<optgroup class="group" label="group12"> - -</optgroup> -</optgroup> -<option value="value2">label2</option> -<optgroup label="group2"> - -</optgroup> -EOD; - $attributes = array( - 'prompt' => 'please select<>', - 'options' => array( - 'value111' => array('class' => 'option'), - ), - 'groups' => array( - 'group12' => array('class' => 'group'), - ), - ); - $this->assertEquals($expected, Html::renderSelectOptions(array('value111', 'value1'), $data, $attributes)); - } - - public function testRenderAttributes() - { - $this->assertEquals('', Html::renderTagAttributes(array())); - $this->assertEquals(' name="test" value="1<>"', Html::renderTagAttributes(array('name' => 'test', 'empty' => null, 'value' => '1<>'))); - Html::$showBooleanAttributeValues = false; - $this->assertEquals(' checked disabled', Html::renderTagAttributes(array('checked' => 'checked', 'disabled' => true, 'hidden' => false))); - Html::$showBooleanAttributeValues = true; - } - - protected function getDataItems() - { - return array( - 'value1' => 'text1', - 'value2' => 'text2', - ); - } - - protected function getDataItems2() - { - return array( - 'value1<>' => 'text1<>', - 'value 2' => 'text 2', - ); - } -} +<?php + +namespace yiiunit\framework\util; + +use Yii; +use yii\helpers\Html; +use yii\web\Application; + +class HtmlTest extends \yii\test\TestCase +{ + public function setUp() + { + new Application('test', '@yiiunit/runtime', array( + 'components' => array( + 'request' => array( + 'class' => 'yii\web\Request', + 'url' => '/test', + ), + ), + )); + } + + public function tearDown() + { + Yii::$app = null; + } + + public function testEncode() + { + $this->assertEquals("a<>&"'", Html::encode("a<>&\"'")); + } + + public function testDecode() + { + $this->assertEquals("a<>&\"'", Html::decode("a<>&"'")); + } + + public function testTag() + { + $this->assertEquals('<br />', Html::tag('br')); + $this->assertEquals('<span></span>', Html::tag('span')); + $this->assertEquals('<div>content</div>', Html::tag('div', 'content')); + $this->assertEquals('<input type="text" name="test" value="<>" />', Html::tag('input', '', array('type' => 'text', 'name' => 'test', 'value' => '<>'))); + + Html::$closeVoidElements = false; + + $this->assertEquals('<br>', Html::tag('br')); + $this->assertEquals('<span></span>', Html::tag('span')); + $this->assertEquals('<div>content</div>', Html::tag('div', 'content')); + $this->assertEquals('<input type="text" name="test" value="<>">', Html::tag('input', '', array('type' => 'text', 'name' => 'test', 'value' => '<>'))); + + Html::$closeVoidElements = true; + + $this->assertEquals('<span disabled="disabled"></span>', Html::tag('span', '', array('disabled' => true))); + Html::$showBooleanAttributeValues = false; + $this->assertEquals('<span disabled></span>', Html::tag('span', '', array('disabled' => true))); + Html::$showBooleanAttributeValues = true; + } + + public function testBeginTag() + { + $this->assertEquals('<br>', Html::beginTag('br')); + $this->assertEquals('<span id="test" class="title">', Html::beginTag('span', array('id' => 'test', 'class' => 'title'))); + } + + public function testEndTag() + { + $this->assertEquals('</br>', Html::endTag('br')); + $this->assertEquals('</span>', Html::endTag('span')); + } + + public function testCdata() + { + $data = 'test<>'; + $this->assertEquals('<![CDATA[' . $data . ']]>', Html::cdata($data)); + } + + public function testStyle() + { + $content = 'a <>'; + $this->assertEquals("<style type=\"text/css\">/*<![CDATA[*/\n{$content}\n/*]]>*/</style>", Html::style($content)); + $this->assertEquals("<style type=\"text/less\">/*<![CDATA[*/\n{$content}\n/*]]>*/</style>", Html::style($content, array('type' => 'text/less'))); + } + + public function testScript() + { + $content = 'a <>'; + $this->assertEquals("<script type=\"text/javascript\">/*<![CDATA[*/\n{$content}\n/*]]>*/</script>", Html::script($content)); + $this->assertEquals("<script type=\"text/js\">/*<![CDATA[*/\n{$content}\n/*]]>*/</script>", Html::script($content, array('type' => 'text/js'))); + } + + public function testCssFile() + { + $this->assertEquals('<link type="text/css" href="http://example.com" rel="stylesheet" />', Html::cssFile('http://example.com')); + $this->assertEquals('<link type="text/css" href="/test" rel="stylesheet" />', Html::cssFile('')); + } + + public function testJsFile() + { + $this->assertEquals('<script type="text/javascript" src="http://example.com"></script>', Html::jsFile('http://example.com')); + $this->assertEquals('<script type="text/javascript" src="/test"></script>', Html::jsFile('')); + } + + public function testBeginForm() + { + $this->assertEquals('<form action="/test" method="post">', Html::beginForm()); + $this->assertEquals('<form action="/example" method="get">', Html::beginForm('/example', 'get')); + $hiddens = array( + '<input type="hidden" name="id" value="1" />', + '<input type="hidden" name="title" value="<" />', + ); + $this->assertEquals('<form action="/example" method="get">' . "\n" . implode("\n", $hiddens), Html::beginForm('/example?id=1&title=%3C', 'get')); + } + + public function testEndForm() + { + $this->assertEquals('</form>', Html::endForm()); + } + + public function testA() + { + $this->assertEquals('<a>something<></a>', Html::a('something<>')); + $this->assertEquals('<a href="/example">something</a>', Html::a('something', '/example')); + $this->assertEquals('<a href="/test">something</a>', Html::a('something', '')); + } + + public function testMailto() + { + $this->assertEquals('<a href="mailto:test<>">test<></a>', Html::mailto('test<>')); + $this->assertEquals('<a href="mailto:test>">test<></a>', Html::mailto('test<>', 'test>')); + } + + public function testImg() + { + $this->assertEquals('<img src="/example" alt="" />', Html::img('/example')); + $this->assertEquals('<img src="/test" alt="" />', Html::img('')); + $this->assertEquals('<img src="/example" width="10" alt="something" />', Html::img('/example', array('alt' => 'something', 'width' => 10))); + } + + public function testLabel() + { + $this->assertEquals('<label>something<></label>', Html::label('something<>')); + $this->assertEquals('<label for="a">something<></label>', Html::label('something<>', 'a')); + $this->assertEquals('<label class="test" for="a">something<></label>', Html::label('something<>', 'a', array('class' => 'test'))); + } + + public function testButton() + { + $this->assertEquals('<button type="button">Button</button>', Html::button()); + $this->assertEquals('<button type="button" name="test" value="value">content<></button>', Html::button('test', 'value', 'content<>')); + $this->assertEquals('<button type="submit" class="t" name="test" value="value">content<></button>', Html::button('test', 'value', 'content<>', array('type' => 'submit', 'class' => "t"))); + } + + public function testSubmitButton() + { + $this->assertEquals('<button type="submit">Submit</button>', Html::submitButton()); + $this->assertEquals('<button type="submit" class="t" name="test" value="value">content<></button>', Html::submitButton('test', 'value', 'content<>', array('class' => 't'))); + } + + public function testResetButton() + { + $this->assertEquals('<button type="reset">Reset</button>', Html::resetButton()); + $this->assertEquals('<button type="reset" class="t" name="test" value="value">content<></button>', Html::resetButton('test', 'value', 'content<>', array('class' => 't'))); + } + + public function testInput() + { + $this->assertEquals('<input type="text" />', Html::input('text')); + $this->assertEquals('<input type="text" class="t" name="test" value="value" />', Html::input('text', 'test', 'value', array('class' => 't'))); + } + + public function testButtonInput() + { + $this->assertEquals('<input type="button" name="test" value="Button" />', Html::buttonInput('test')); + $this->assertEquals('<input type="button" class="a" name="test" value="text" />', Html::buttonInput('test', 'text', array('class' => 'a'))); + } + + public function testSubmitInput() + { + $this->assertEquals('<input type="submit" value="Submit" />', Html::submitInput()); + $this->assertEquals('<input type="submit" class="a" name="test" value="text" />', Html::submitInput('test', 'text', array('class' => 'a'))); + } + + public function testResetInput() + { + $this->assertEquals('<input type="reset" value="Reset" />', Html::resetInput()); + $this->assertEquals('<input type="reset" class="a" name="test" value="text" />', Html::resetInput('test', 'text', array('class' => 'a'))); + } + + public function testTextInput() + { + $this->assertEquals('<input type="text" name="test" />', Html::textInput('test')); + $this->assertEquals('<input type="text" class="t" name="test" value="value" />', Html::textInput('test', 'value', array('class' => 't'))); + } + + public function testHiddenInput() + { + $this->assertEquals('<input type="hidden" name="test" />', Html::hiddenInput('test')); + $this->assertEquals('<input type="hidden" class="t" name="test" value="value" />', Html::hiddenInput('test', 'value', array('class' => 't'))); + } + + public function testPasswordInput() + { + $this->assertEquals('<input type="password" name="test" />', Html::passwordInput('test')); + $this->assertEquals('<input type="password" class="t" name="test" value="value" />', Html::passwordInput('test', 'value', array('class' => 't'))); + } + + public function testFileInput() + { + $this->assertEquals('<input type="file" name="test" />', Html::fileInput('test')); + $this->assertEquals('<input type="file" class="t" name="test" value="value" />', Html::fileInput('test', 'value', array('class' => 't'))); + } + + public function testTextarea() + { + $this->assertEquals('<textarea name="test"></textarea>', Html::textarea('test')); + $this->assertEquals('<textarea class="t" name="test">value<></textarea>', Html::textarea('test', 'value<>', array('class' => 't'))); + } + + public function testRadio() + { + $this->assertEquals('<input type="radio" name="test" value="1" />', Html::radio('test')); + $this->assertEquals('<input type="radio" class="a" name="test" checked="checked" />', Html::radio('test', true, null, array('class' => 'a'))); + $this->assertEquals('<input type="hidden" name="test" value="0" /><input type="radio" class="a" name="test" value="2" checked="checked" />', Html::radio('test', true, 2, array('class' => 'a' , 'uncheck' => '0'))); + } + + public function testCheckbox() + { + $this->assertEquals('<input type="checkbox" name="test" value="1" />', Html::checkbox('test')); + $this->assertEquals('<input type="checkbox" class="a" name="test" checked="checked" />', Html::checkbox('test', true, null, array('class' => 'a'))); + $this->assertEquals('<input type="hidden" name="test" value="0" /><input type="checkbox" class="a" name="test" value="2" checked="checked" />', Html::checkbox('test', true, 2, array('class' => 'a', 'uncheck' => '0'))); + } + + public function testDropDownList() + { + $expected = <<<EOD +<select name="test"> + +</select> +EOD; + $this->assertEquals($expected, Html::dropDownList('test')); + $expected = <<<EOD +<select name="test"> +<option value="value1">text1</option> +<option value="value2">text2</option> +</select> +EOD; + $this->assertEquals($expected, Html::dropDownList('test', null, $this->getDataItems())); + $expected = <<<EOD +<select name="test"> +<option value="value1">text1</option> +<option value="value2" selected="selected">text2</option> +</select> +EOD; + $this->assertEquals($expected, Html::dropDownList('test', 'value2', $this->getDataItems())); + } + + public function testListBox() + { + $expected = <<<EOD +<select name="test" size="4"> + +</select> +EOD; + $this->assertEquals($expected, Html::listBox('test')); + $expected = <<<EOD +<select name="test" size="5"> +<option value="value1">text1</option> +<option value="value2">text2</option> +</select> +EOD; + $this->assertEquals($expected, Html::listBox('test', null, $this->getDataItems(), array('size' => 5))); + $expected = <<<EOD +<select name="test" size="4"> +<option value="value1<>">text1<></option> +<option value="value 2">text  2</option> +</select> +EOD; + $this->assertEquals($expected, Html::listBox('test', null, $this->getDataItems2())); + $expected = <<<EOD +<select name="test" size="4"> +<option value="value1">text1</option> +<option value="value2" selected="selected">text2</option> +</select> +EOD; + $this->assertEquals($expected, Html::listBox('test', 'value2', $this->getDataItems())); + $expected = <<<EOD +<select name="test" size="4"> +<option value="value1" selected="selected">text1</option> +<option value="value2" selected="selected">text2</option> +</select> +EOD; + $this->assertEquals($expected, Html::listBox('test', array('value1', 'value2'), $this->getDataItems())); + + $expected = <<<EOD +<select name="test[]" multiple="multiple" size="4"> + +</select> +EOD; + $this->assertEquals($expected, Html::listBox('test', null, array(), array('multiple' => true))); + $expected = <<<EOD +<input type="hidden" name="test" value="0" /><select name="test" size="4"> + +</select> +EOD; + $this->assertEquals($expected, Html::listBox('test', '', array(), array('unselect' => '0'))); + } + + public function testCheckboxList() + { + $this->assertEquals('', Html::checkboxList('test')); + + $expected = <<<EOD +<label><input type="checkbox" name="test[]" value="value1" /> text1</label> +<label><input type="checkbox" name="test[]" value="value2" checked="checked" /> text2</label> +EOD; + $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems())); + + $expected = <<<EOD +<label><input type="checkbox" name="test[]" value="value1<>" /> text1<></label> +<label><input type="checkbox" name="test[]" value="value 2" /> text 2</label> +EOD; + $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems2())); + + $expected = <<<EOD +<input type="hidden" name="test" value="0" /><label><input type="checkbox" name="test[]" value="value1" /> text1</label><br /> +<label><input type="checkbox" name="test[]" value="value2" checked="checked" /> text2</label> +EOD; + $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems(), array( + 'separator' => "<br />\n", + 'unselect' => '0', + ))); + + $expected = <<<EOD +0<label>text1 <input type="checkbox" name="test[]" value="value1" /></label> +1<label>text2 <input type="checkbox" name="test[]" value="value2" checked="checked" /></label> +EOD; + $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems(), array( + 'item' => function ($index, $label, $name, $checked, $value) { + return $index . Html::label($label . ' ' . Html::checkbox($name, $checked, $value)); + } + ))); + } + + public function testRadioList() + { + $this->assertEquals('', Html::radioList('test')); + + $expected = <<<EOD +<label><input type="radio" name="test" value="value1" /> text1</label> +<label><input type="radio" name="test" value="value2" checked="checked" /> text2</label> +EOD; + $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems())); + + $expected = <<<EOD +<label><input type="radio" name="test" value="value1<>" /> text1<></label> +<label><input type="radio" name="test" value="value 2" /> text 2</label> +EOD; + $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems2())); + + $expected = <<<EOD +<input type="hidden" name="test" value="0" /><label><input type="radio" name="test" value="value1" /> text1</label><br /> +<label><input type="radio" name="test" value="value2" checked="checked" /> text2</label> +EOD; + $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems(), array( + 'separator' => "<br />\n", + 'unselect' => '0', + ))); + + $expected = <<<EOD +0<label>text1 <input type="radio" name="test" value="value1" /></label> +1<label>text2 <input type="radio" name="test" value="value2" checked="checked" /></label> +EOD; + $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems(), array( + 'item' => function ($index, $label, $name, $checked, $value) { + return $index . Html::label($label . ' ' . Html::radio($name, $checked, $value)); + } + ))); + } + + public function testRenderOptions() + { + $data = array( + 'value1' => 'label1', + 'group1' => array( + 'value11' => 'label11', + 'group11' => array( + 'value111' => 'label111', + ), + 'group12' => array(), + ), + 'value2' => 'label2', + 'group2' => array(), + ); + $expected = <<<EOD +<option value="">please select<></option> +<option value="value1" selected="selected">label1</option> +<optgroup label="group1"> +<option value="value11">label11</option> +<optgroup label="group11"> +<option class="option" value="value111" selected="selected">label111</option> +</optgroup> +<optgroup class="group" label="group12"> + +</optgroup> +</optgroup> +<option value="value2">label2</option> +<optgroup label="group2"> + +</optgroup> +EOD; + $attributes = array( + 'prompt' => 'please select<>', + 'options' => array( + 'value111' => array('class' => 'option'), + ), + 'groups' => array( + 'group12' => array('class' => 'group'), + ), + ); + $this->assertEquals($expected, Html::renderSelectOptions(array('value111', 'value1'), $data, $attributes)); + } + + public function testRenderAttributes() + { + $this->assertEquals('', Html::renderTagAttributes(array())); + $this->assertEquals(' name="test" value="1<>"', Html::renderTagAttributes(array('name' => 'test', 'empty' => null, 'value' => '1<>'))); + Html::$showBooleanAttributeValues = false; + $this->assertEquals(' checked disabled', Html::renderTagAttributes(array('checked' => 'checked', 'disabled' => true, 'hidden' => false))); + Html::$showBooleanAttributeValues = true; + } + + protected function getDataItems() + { + return array( + 'value1' => 'text1', + 'value2' => 'text2', + ); + } + + protected function getDataItems2() + { + return array( + 'value1<>' => 'text1<>', + 'value 2' => 'text 2', + ); + } +} From 2fd87fb6d002c0d698fe05cda43af30790ed05be Mon Sep 17 00:00:00 2001 From: Qiang Xue <qiang.xue@gmail.com> Date: Thu, 28 Mar 2013 08:28:03 -0400 Subject: [PATCH 08/41] User WIP --- framework/base/Controller.php | 3 --- framework/web/HttpCache.php | 6 ++--- framework/web/User.php | 55 ------------------------------------------- 3 files changed, 2 insertions(+), 62 deletions(-) diff --git a/framework/base/Controller.php b/framework/base/Controller.php index 17fb4da..ff6d8f7 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 <qiang.xue@gmail.com> * @since 2.0 */ diff --git a/framework/web/HttpCache.php b/framework/web/HttpCache.php index 09df7a2..b715c32 100644 --- a/framework/web/HttpCache.php +++ b/framework/web/HttpCache.php @@ -112,10 +112,8 @@ class HttpCache extends ActionFilter */ protected function sendCacheControlHeader() { - if (Yii::$app->session->isActive) { - session_cache_limiter('public'); - header('Pragma:', true); - } + session_cache_limiter('public'); + header('Pragma:', true); header('Cache-Control: ' . $this->cacheControl, true); } diff --git a/framework/web/User.php b/framework/web/User.php index 93eb1ce..2ecbcda 100644 --- a/framework/web/User.php +++ b/framework/web/User.php @@ -223,30 +223,6 @@ class User extends Component } /** - * 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); - } - - /** * 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. @@ -587,7 +563,6 @@ class User extends Component * 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 */ protected function updateAuthStatus() { @@ -600,34 +575,4 @@ class User extends Component } } } - - /** - * 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 <code>$params=array()</code>. - * @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; - } } From b505a9d9e17d87c6a8ed357f41eb5afbdd89f0c3 Mon Sep 17 00:00:00 2001 From: Qiang Xue <qiang.xue@gmail.com> Date: Thu, 28 Mar 2013 11:06:17 -0400 Subject: [PATCH 09/41] Finished HttpCache. --- framework/web/HttpCache.php | 18 ++++++++++-------- framework/web/User.php | 43 ++----------------------------------------- 2 files changed, 12 insertions(+), 49 deletions(-) diff --git a/framework/web/HttpCache.php b/framework/web/HttpCache.php index b715c32..f64b37f 100644 --- a/framework/web/HttpCache.php +++ b/framework/web/HttpCache.php @@ -48,11 +48,9 @@ class HttpCache extends ActionFilter */ public $params; /** - * Http cache control headers. Set this to an empty string in order to keep this - * header from being sent entirely. - * @var string + * @var string HTTP cache control header. If null, the header will not be sent. */ - public $cacheControl = 'max-age=3600, public'; + public $cacheControlHeader = 'Cache-Control: max-age=3600, public'; /** * This method is invoked right before an action is to be executed (after all possible filters.) @@ -62,8 +60,8 @@ class HttpCache extends ActionFilter */ public function beforeAction($action) { - $requestMethod = Yii::$app->request->getRequestMethod(); - if ($requestMethod !== 'GET' && $requestMethod !== 'HEAD' || $this->lastModified === null && $this->etagSeed === null) { + $verb = Yii::$app->request->getRequestMethod(); + if ($verb !== 'GET' && $verb !== 'HEAD' || $this->lastModified === null && $this->etagSeed === null) { return true; } @@ -84,7 +82,9 @@ class HttpCache extends ActionFilter if ($this->validateCache($lastModified, $etag)) { header('HTTP/1.1 304 Not Modified'); return false; - } elseif ($lastModified !== null) { + } + + if ($lastModified !== null) { header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $lastModified) . ' GMT'); } return true; @@ -114,7 +114,9 @@ class HttpCache extends ActionFilter { session_cache_limiter('public'); header('Pragma:', true); - header('Cache-Control: ' . $this->cacheControl, true); + if ($this->cacheControlHeader !== null) { + header($this->cacheControlHeader, true); + } } /** diff --git a/framework/web/User.php b/framework/web/User.php index 2ecbcda..fdde60b 100644 --- a/framework/web/User.php +++ b/framework/web/User.php @@ -7,49 +7,10 @@ namespace yii\web; +use Yii; use yii\base\Component; /** - * CWebUser represents the persistent state for a Web application user. - * - * CWebUser is used as an application component whose ID is 'user'. - * Therefore, at any place one can access the user state via - * <code>Yii::app()->user</code>. - * - * 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: - * <ol> - * <li>The user provides information needed for authentication.</li> - * <li>An {@link IUserIdentity identity instance} is created with the user-provided information.</li> - * <li>Call {@link IUserIdentity::authenticate} to check if the identity is valid.</li> - * <li>If valid, call {@link CWebUser::login} to login the user, and - * Redirect the user browser to {@link returnUrl}.</li> - * <li>If not valid, retrieve the error code or message from the identity - * instance and display it.</li> - * </ol> - * - * 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 enableAutoLogin 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. * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 @@ -120,7 +81,7 @@ class User extends Component public function init() { parent::init(); - Yii::app()->getSession()->open(); + Yii::$app->getSession()->open(); if ($this->getIsGuest() && $this->enableAutoLogin) { $this->restoreFromCookie(); } elseif ($this->autoRenewCookie && $this->enableAutoLogin) { From 5f0f721c4aff14a75495cbca2390fae8a39c41b1 Mon Sep 17 00:00:00 2001 From: Qiang Xue <qiang.xue@gmail.com> Date: Thu, 28 Mar 2013 11:17:05 -0400 Subject: [PATCH 10/41] Finished AccessControl. --- framework/web/AccessRule.php | 46 +++++++++++--------------------------------- 1 file changed, 11 insertions(+), 35 deletions(-) diff --git a/framework/web/AccessRule.php b/framework/web/AccessRule.php index ac42ad1..3f8c057 100644 --- a/framework/web/AccessRule.php +++ b/framework/web/AccessRule.php @@ -35,22 +35,16 @@ class AccessRule extends Component */ public $controllers; /** - * @var array list of user names that this rule applies to. The comparison is case-insensitive. - * If not set or empty, it means this rule applies to all users. Two special tokens are recognized: + * @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 * - * @see \yii\web\Application::user - */ - public $users; - /** - * @var array list of roles that this rule applies to. For each role, the current user's - * {@link CWebUser::checkAccess} method will be invoked. If one of the invocations - * returns true, the rule will be applied. - * Note, you should mainly use roles in an "allow" rule because by definition, - * a role represents a permission collection. - * If not set or empty, it means this rule applies to all roles. + * 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; /** @@ -106,7 +100,6 @@ class AccessRule extends Component public function allows($action, $user, $request) { if ($this->matchAction($action) - && $this->matchUser($user) && $this->matchRole($user) && $this->matchIP($request->getUserIP()) && $this->matchVerb($request->getRequestMethod()) @@ -138,27 +131,6 @@ class AccessRule extends Component } /** - * @param User $user the user - * @return boolean whether the rule applies to the user - */ - protected function matchUser($user) - { - if (empty($this->users)) { - return true; - } - foreach ($this->users as $u) { - if ($u === '?' && $user->getIsGuest()) { - return true; - } elseif ($u === '@' && !$user->getIsGuest()) { - return true; - } elseif (!strcasecmp($u, $user->getName())) { - return true; - } - } - return false; - } - - /** * @param User $user the user object * @return boolean whether the rule applies to the role */ @@ -168,7 +140,11 @@ class AccessRule extends Component return true; } foreach ($this->roles as $role) { - if ($user->checkAccess($role)) { + if ($role === '?' && $user->getIsGuest()) { + return true; + } elseif ($role === '@' && !$user->getIsGuest()) { + return true; + } elseif ($user->hasAccess($role)) { return true; } } From 66fcf622aaace5dc2a5a20d1b098ea0c86b7ae3a Mon Sep 17 00:00:00 2001 From: Alexander Makarov <sam@rmcreative.ru> Date: Fri, 29 Mar 2013 01:36:13 +0400 Subject: [PATCH 11/41] Fixed Model::getFirstErrors() --- framework/base/Model.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/framework/base/Model.php b/framework/base/Model.php index 402a558..5e55f8d 100644 --- a/framework/base/Model.php +++ b/framework/base/Model.php @@ -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]; } } } From 1fbf81be57fb2b3ad07a401f0f12e18cd0f2e7a3 Mon Sep 17 00:00:00 2001 From: Qiang Xue <qiang.xue@gmail.com> Date: Thu, 28 Mar 2013 17:43:27 -0400 Subject: [PATCH 12/41] User WIP. --- docs/code_style.md | 2 +- framework/base/Component.php | 5 +- framework/base/Event.php | 19 +- framework/base/Model.php | 2 +- framework/db/ActiveRecord.php | 4 +- framework/web/Identity.php | 45 +++ framework/web/Response.php | 3 +- framework/web/Session.php | 1 + framework/web/User.php | 462 ++++++++++++++-------------- framework/web/UserEvent.php | 34 ++ tests/unit/framework/base/ComponentTest.php | 2 +- 11 files changed, 329 insertions(+), 250 deletions(-) create mode 100644 framework/web/Identity.php create mode 100644 framework/web/UserEvent.php diff --git a/docs/code_style.md b/docs/code_style.md index fcf643d..92a934b 100644 --- a/docs/code_style.md +++ b/docs/code_style.md @@ -204,7 +204,7 @@ doIt('a', array( ~~~ if ($event === null) { - return new Event($this); + return new Event(); } elseif ($event instanceof CoolEvent) { return $event->instance(); } else { 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/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/Model.php b/framework/base/Model.php index 402a558..9056a71 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; } diff --git a/framework/db/ActiveRecord.php b/framework/db/ActiveRecord.php index 0c15121..d8f2f65 100644 --- a/framework/db/ActiveRecord.php +++ b/framework/db/ActiveRecord.php @@ -847,7 +847,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 +887,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/web/Identity.php b/framework/web/Identity.php new file mode 100644 index 0000000..4668337 --- /dev/null +++ b/framework/web/Identity.php @@ -0,0 +1,45 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\web; + +/** + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @since 2.0 + */ +interface Identity +{ + /** + * Returns an ID that can uniquely identify a user identity. + * The returned ID can be a string, an integer, or any serializable data. + * @return mixed 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 space of such keys should be big and random enough to defeat potential identity attacks. + * The returned key can be a string, an integer, or any serializable data. + * @return mixed a key that is used to check the validity of a given identity ID. + * @see validateAuthKey() + */ + public function getAuthKey(); + /** + * Validates the given auth key. + * @param string $authKey the given auth key + * @return boolean whether the given auth key is valid. + * @see getAuthKey() + */ + public function validateAuthKey($authKey); + /** + * Finds an identity by the given ID. + * @param mixed $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. + */ + public static function findIdentity($id); +} \ No newline at end of file diff --git a/framework/web/Response.php b/framework/web/Response.php index d6659cf..d23c5b9 100644 --- a/framework/web/Response.php +++ b/framework/web/Response.php @@ -7,6 +7,7 @@ namespace yii\web; +use Yii; use yii\helpers\FileHelper; /** @@ -178,6 +179,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 840a26d..c289db2 100644 --- a/framework/web/Session.php +++ b/framework/web/Session.php @@ -619,6 +619,7 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co } /** + * 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 */ diff --git a/framework/web/User.php b/framework/web/User.php index fdde60b..2326a10 100644 --- a/framework/web/User.php +++ b/framework/web/User.php @@ -9,17 +9,26 @@ namespace yii\web; use Yii; use yii\base\Component; +use yii\base\InvalidConfigException; /** - * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ class User extends Component { - const STATES_VAR = '__states'; - const AUTH_TIMEOUT_VAR = '__timeout'; + const ID_VAR = '__id'; + const AUTH_EXPIRE_VAR = '__expire'; + const EVENT_BEFORE_LOGIN = 'beforeLogin'; + const EVENT_AFTER_LOGIN = 'afterLogin'; + const EVENT_BEFORE_LOGOUT = 'beforeLogout'; + const EVENT_AFTER_LOGOUT = 'afterLogout'; + + /** + * @var string the class name of the [[identity]] object. + */ + public $identityClass; /** * @var boolean whether to enable cookie-based login. Defaults to false. */ @@ -41,7 +50,7 @@ class User extends Component * @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'); /** * @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 @@ -70,10 +79,9 @@ class User extends Component * @see loginRequired */ public $loginRequiredAjaxResponse; + - private $_keyPrefix; - private $_access = array(); - + public $stateVar = '__states'; /** * Initializes the application component. @@ -81,13 +89,47 @@ class User extends Component public function init() { parent::init(); + + if ($this->enableAutoLogin && !isset($this->identityCookie['name'])) { + throw new InvalidConfigException('User::identityCookie must contain the "name" element.'); + } + Yii::$app->getSession()->open(); - if ($this->getIsGuest() && $this->enableAutoLogin) { - $this->restoreFromCookie(); - } elseif ($this->autoRenewCookie && $this->enableAutoLogin) { - $this->renewCookie(); + + $this->renewAuthStatus(); + + if ($this->enableAutoLogin) { + if ($this->getIsGuest()) { + $this->loginByCookie(); + } elseif ($this->autoRenewCookie) { + $this->renewIdentityCookie(); + } + } + } + + /** + * @var Identity the identity object associated with the currently logged user. + */ + private $_identity = false; + + public function getIdentity() + { + if ($this->_identity === false) { + $id = $this->getId(); + if ($id === null) { + $this->_identity = null; + } else { + /** @var $class Identity */ + $class = $this->identityClass; + $this->_identity = $class::findIdentity($this->getId()); + } } - $this->updateAuthStatus(); + return $this->_identity; + } + + public function setIdentity($identity) + { + $this->switchIdentity($identity); } /** @@ -101,7 +143,7 @@ class User extends Component * Note, you have to set {@link enableAutoLogin} 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 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. * If greater than 0, cookie-based login will be used. In this case, {@link enableAutoLogin} * must be set true, otherwise an exception will be thrown. @@ -109,26 +151,46 @@ class User extends Component */ 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 ($duration > 0) { - if ($this->enableAutoLogin) { - $this->saveToCookie($duration); - } else { - throw new CException(Yii::t('yii', '{class}.enableAutoLogin must be set true in order to use cookie-based authentication.', - array('{class}' => get_class($this)))); - } + if ($this->beforeLogin($identity, false)) { + $this->switchIdentity($identity); + if ($duration > 0 && $this->enableAutoLogin) { + $this->saveIdentityCookie($identity, $duration); } - - $this->afterLogin(false); + $this->afterLogin($identity, false); } return !$this->getIsGuest(); } /** + * Populates the current user object with the information obtained from cookie. + * This method is used when automatic login ({@link enableAutoLogin}) is enabled. + * The user identity information is recovered from cookie. + * Sufficient security measures are used to prevent cookie data from being tampered. + * @see saveIdentityCookie + */ + 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) && $this->beforeLogin($identity, true)) { + $this->switchIdentity($identity); + if ($this->autoRenewCookie) { + $this->saveIdentityCookie($identity, $duration); + } + $this->afterLogin($identity, true); + } + } + } + } + + /** * 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. @@ -137,33 +199,26 @@ class User extends Component */ public function logout($destroySession = true) { - if ($this->beforeLogout()) { + $identity = $this->getIdentity(); + if ($identity !== null && $this->beforeLogout($identity)) { + $this->switchIdentity(null); if ($this->enableAutoLogin) { - 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); - } + Yii::$app->getResponse()->getCookies()->remove(new Cookie($this->identityCookie)); } 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; } /** @@ -172,7 +227,7 @@ class User extends Component */ public function getId() { - return $this->getState('__id'); + return $this->getState(static::ID_VAR); } /** @@ -180,7 +235,7 @@ class User extends Component */ public function setId($value) { - $this->setState('__id', $value); + $this->setState(static::ID_VAR, $value); } /** @@ -253,11 +308,15 @@ class User extends Component * @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 */ - protected function beforeLogin($id, $states, $fromCookie) + protected function beforeLogin($identity, $fromCookie) { - return true; + $event = new UserEvent(array( + 'identity' => $identity, + 'fromCookie' => $fromCookie, + )); + $this->trigger(self::EVENT_BEFORE_LOGIN, $event); + return $event->isValid; } /** @@ -265,10 +324,13 @@ class User extends Component * 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 */ - protected function afterLogin($fromCookie) + protected function afterLogin($identity, $fromCookie) { + $this->trigger(self::EVENT_AFTER_LOGIN, new UserEvent(array( + 'identity' => $identity, + 'fromCookie' => $fromCookie, + ))); } /** @@ -277,66 +339,44 @@ class User extends Component * 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 */ - 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 */ - protected function afterLogout() + protected function afterLogout($identity) { + $this->trigger(self::EVENT_AFTER_LOGOUT, new UserEvent(array( + 'identity' => $identity, + ))); } - /** - * Populates the current user object with the information obtained from cookie. - * This method is used when automatic login ({@link enableAutoLogin}) 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); - } - } - } - } /** * 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() + protected function renewIdentityCookie() { - $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); + $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); } } } @@ -346,194 +386,162 @@ class User extends Component * This method is used when automatic login ({@link enableAutoLogin}) 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 Identity $identity * @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 + * @see loginByCookie */ - protected function saveToCookie($duration) + protected function saveIdentityCookie($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); + )); + $cookie->expire = time() + $duration; + Yii::$app->getResponse()->getCookies()->add($cookie); } /** - * Creates a cookie to store identity information. - * @param string $name the cookie name - * @return CHttpCookie the cookie used to store identity information + * 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 Identity $identity a unique identifier for the user */ - protected function createIdentityCookie($name) + protected function switchIdentity($identity) { - $cookie = new CHttpCookie($name, ''); - if (is_array($this->identityCookie)) { - foreach ($this->identityCookie as $name => $value) { - $cookie->$name = $value; + Yii::$app->getSession()->regenerateID(true); + $this->setIdentity($identity); + if ($identity instanceof Identity) { + $this->setId($identity->getId()); + if ($this->authTimeout !== null) { + $this->setState(self::AUTH_EXPIRE_VAR, time() + $this->authTimeout); } - } - 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()); + $this->removeAllStates(); } } /** - * @param string $value a prefix for the name of the session variables storing user session data. + * Updates the authentication status according to {@link authTimeout}. + * If the user has been inactive for {@link authTimeout} seconds, + * he will be automatically logged out. */ - public function setStateKeyPrefix($value) + protected function renewAuthStatus() { - $this->_keyPrefix = $value; + if ($this->authTimeout !== null && !$this->getIsGuest()) { + $expire = $this->getState(self::AUTH_EXPIRE_VAR); + if ($expire !== null && $expire < time()) { + $this->logout(false); + } else { + $this->setState(self::AUTH_EXPIRE_VAR, time() + $this->authTimeout); + } + } } /** - * Returns the value of a variable that is stored in user session. - * - * 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. - * - * @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 + * Returns a user state. + * A user state is a session data item associated with the current user. + * If the user logs out, all his/her user states will be removed. + * @param string $key the key identifying the state + * @param mixed $defaultValue value to be returned if the state does not exist. + * @return mixed the state */ 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. - * - * @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]); + $manifest = isset($_SESSION[$this->stateVar]) ? $_SESSION[$this->stateVar] : null; + if (is_array($manifest) && isset($manifest[$key], $_SESSION[$key])) { + return $_SESSION[$key]; } else { - $_SESSION[$key] = $value; + return $defaultValue; } } /** - * 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. + * Returns all user states. + * @return array states (key => state). */ - public function hasState($key) + public function getAllStates() { - $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]); + $manifest = isset($_SESSION[$this->stateVar]) ? $_SESSION[$this->stateVar] : null; + $states = array(); + if (is_array($manifest)) { + foreach (array_keys($manifest) as $key) { + if (isset($_SESSION[$key])) { + $states[$key] = $_SESSION[$key]; + } } } + return $states; } /** - * 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 + * Stores a user state. + * A user state is a session data item associated with the current user. + * If the user logs out, all his/her user states will be removed. + * @param string $key the key identifying the state. Note that states + * 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 state */ - protected function changeIdentity($id, $name, $states) + public function setState($key, $value) { - Yii::app()->getSession()->regenerateID(true); - $this->setId($id); - $this->setName($name); - $this->loadIdentityStates($states); + $manifest = isset($_SESSION[$this->stateVar]) ? $_SESSION[$this->stateVar] : array(); + $manifest[$value] = true; + $_SESSION[$key] = $value; + $_SESSION[$this->stateVar] = $manifest; } /** - * Retrieves identity states from persistent storage and saves them as an array. - * @return array the identity states + * Removes a user state. + * If the user logs out, all his/her user states will be removed automatically. + * @param string $key the key identifying the state. Note that states + * 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 state. Null if the state does not exist. */ - protected function saveIdentityStates() + public function removeState($key) { - $states = array(); - foreach ($this->getState(self::STATES_VAR, array()) as $name => $dummy) { - $states[$name] = $this->getState($name); + $manifest = isset($_SESSION[$this->stateVar]) ? $_SESSION[$this->stateVar] : null; + if (is_array($manifest) && isset($manifest[$key], $_SESSION[$key])) { + $value = $_SESSION[$key]; + } else { + $value = null; } - return $states; + unset($_SESSION[$this->stateVar][$key], $_SESSION[$key]); + return $value; } /** - * Loads identity states from an array and saves them to persistent storage. - * @param array $states the identity states + * Removes all states. + * If the user logs out, all his/her user states will be removed automatically + * without the need to call this method manually. + * + * Note that states 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. */ - protected function loadIdentityStates($states) + public function removeAllStates() { - $names = array(); - if (is_array($states)) { - foreach ($states as $name => $value) { - $this->setState($name, $value); - $names[$name] = true; - } - } - $this->setState(self::STATES_VAR, $names); + $manifest = isset($_SESSION[$this->stateVar]) ? $_SESSION[$this->stateVar] : null; + if (is_array($manifest)) { + foreach (array_keys($manifest) as $key) { + unset($_SESSION[$key]); + } + } + unset($_SESSION[$this->stateVar]); } /** - * Updates the authentication status according to {@link authTimeout}. - * If the user has been inactive for {@link authTimeout} seconds, - * he will be automatically logged out. + * Returns a value indicating whether there is a state associated with the specified key. + * @param string $key key identifying the state + * @return boolean whether the specified state exists */ - protected function updateAuthStatus() + public function hasState($key) { - if ($this->authTimeout !== null && !$this->getIsGuest()) { - $expires = $this->getState(self::AUTH_TIMEOUT_VAR); - if ($expires !== null && $expires < time()) { - $this->logout(false); - } else { - $this->setState(self::AUTH_TIMEOUT_VAR, time() + $this->authTimeout); - } - } + return $this->getState($key) !== null; } } diff --git a/framework/web/UserEvent.php b/framework/web/UserEvent.php new file mode 100644 index 0000000..3a8723a --- /dev/null +++ b/framework/web/UserEvent.php @@ -0,0 +1,34 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\web; + +use yii\base\Event; + +/** + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @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 $fromCookie; + /** + * @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; +} \ No newline at end of file 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); } } From 0b265f0e36d6bcd017342adab53551e544d86072 Mon Sep 17 00:00:00 2001 From: Alexander Makarov <sam@rmcreative.ru> Date: Fri, 29 Mar 2013 02:30:42 +0400 Subject: [PATCH 13/41] A bit more friendly behavior for unsetting a model attribute --- framework/base/Model.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/framework/base/Model.php b/framework/base/Model.php index 5e55f8d..5da9e56 100644 --- a/framework/base/Model.php +++ b/framework/base/Model.php @@ -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; } } From a72d2f536a0ff3c6405253107b1ddf0a6792710f Mon Sep 17 00:00:00 2001 From: Alexander Makarov <sam@rmcreative.ru> Date: Fri, 29 Mar 2013 02:50:36 +0400 Subject: [PATCH 14/41] Some tests for Model --- tests/unit/data/base/Speaker.php | 40 ++++++++ tests/unit/framework/base/ModelTest.php | 172 ++++++++++++++++++++++++++++++++ 2 files changed, 212 insertions(+) create mode 100644 tests/unit/data/base/Speaker.php create mode 100644 tests/unit/framework/base/ModelTest.php diff --git a/tests/unit/data/base/Speaker.php b/tests/unit/data/base/Speaker.php new file mode 100644 index 0000000..240b691 --- /dev/null +++ b/tests/unit/data/base/Speaker.php @@ -0,0 +1,40 @@ +<?php +namespace yiiunit\data\base; + +/** + * Speaker + */ +use yii\base\Model; + +class Speaker extends Model +{ + public $firstName; + public $lastName; + + public $customLabel; + public $underscore_style; + + protected $protectedProperty; + private $_privateProperty; + + public function attributeLabels() + { + return array( + 'customLabel' => '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/ModelTest.php b/tests/unit/framework/base/ModelTest.php new file mode 100644 index 0000000..bd10f45 --- /dev/null +++ b/tests/unit/framework/base/ModelTest.php @@ -0,0 +1,172 @@ +<?php + +namespace yiiunit\framework\base; +use yiiunit\TestCase; +use yiiunit\data\base\Speaker; + +/** + * ModelTest + */ +class ModelTest extends TestCase +{ + public function testGetAttributeLalel() + { + $speaker = new Speaker(); + $this->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'])); + } +} From 7b73fdff5c1e473ebda39ea6a12526e12b9033b5 Mon Sep 17 00:00:00 2001 From: Alexander Makarov <sam@rmcreative.ru> Date: Fri, 29 Mar 2013 03:22:19 +0400 Subject: [PATCH 15/41] Better validation rules validity check --- framework/base/Model.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/base/Model.php b/framework/base/Model.php index 7818293..13e567d 100644 --- a/framework/base/Model.php +++ b/framework/base/Model.php @@ -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 { From 99238886378845741f22d1dfc15f2970d72202ef Mon Sep 17 00:00:00 2001 From: Alexander Makarov <sam@rmcreative.ru> Date: Fri, 29 Mar 2013 03:22:50 +0400 Subject: [PATCH 16/41] More Model tests --- tests/unit/data/base/InvalidRulesModel.php | 17 ++++++++++++++++ tests/unit/data/base/Singer.php | 21 ++++++++++++++++++++ tests/unit/data/base/Speaker.php | 3 +-- tests/unit/framework/base/ModelTest.php | 31 ++++++++++++++++++++++++++++++ 4 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 tests/unit/data/base/InvalidRulesModel.php create mode 100644 tests/unit/data/base/Singer.php 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 @@ +<?php +namespace yiiunit\data\base; +use yii\base\Model; + +/** + * InvalidRulesModel + */ +class InvalidRulesModel extends Model +{ + public function rules() + { + return array( + array('test'), + ); + } + +} diff --git a/tests/unit/data/base/Singer.php b/tests/unit/data/base/Singer.php new file mode 100644 index 0000000..3305b98 --- /dev/null +++ b/tests/unit/data/base/Singer.php @@ -0,0 +1,21 @@ +<?php +namespace yiiunit\data\base; +use yii\base\Model; + +/** + * Singer + */ +class Singer extends Model +{ + public $fistName; + public $lastName; + + public function rules() + { + return array( + array('lastName', 'default', 'value' => '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 index 240b691..93dd496 100644 --- a/tests/unit/data/base/Speaker.php +++ b/tests/unit/data/base/Speaker.php @@ -1,11 +1,10 @@ <?php namespace yiiunit\data\base; +use yii\base\Model; /** * Speaker */ -use yii\base\Model; - class Speaker extends Model { public $firstName; diff --git a/tests/unit/framework/base/ModelTest.php b/tests/unit/framework/base/ModelTest.php index bd10f45..aa15230 100644 --- a/tests/unit/framework/base/ModelTest.php +++ b/tests/unit/framework/base/ModelTest.php @@ -1,8 +1,11 @@ <?php namespace yiiunit\framework\base; +use yii\base\Model; use yiiunit\TestCase; use yiiunit\data\base\Speaker; +use yiiunit\data\base\Singer; +use yiiunit\data\base\InvalidRulesModel; /** * ModelTest @@ -169,4 +172,32 @@ class ModelTest extends TestCase $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(); + } } From e7295ad564a327397e0807f04109d12643ceaca2 Mon Sep 17 00:00:00 2001 From: Qiang Xue <qiang.xue@gmail.com> Date: Thu, 28 Mar 2013 20:07:49 -0400 Subject: [PATCH 17/41] Use __METHOD__ as log category. --- framework/base/Model.php | 2 +- framework/base/Module.php | 4 ++-- framework/db/Command.php | 28 +++++++++++++++------------- framework/db/Connection.php | 6 +++--- framework/db/Transaction.php | 6 +++--- framework/i18n/PhpMessageSource.php | 2 +- framework/web/Identity.php | 7 +++---- framework/web/Session.php | 2 +- framework/web/User.php | 11 ++++++++++- 9 files changed, 39 insertions(+), 29 deletions(-) diff --git a/framework/base/Model.php b/framework/base/Model.php index 7818293..b9b5846 100644 --- a/framework/base/Model.php +++ b/framework/base/Model.php @@ -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__); } } diff --git a/framework/base/Module.php b/framework/base/Module.php index 6b82157..cf751c0 100644 --- a/framework/base/Module.php +++ b/framework/base/Module.php @@ -346,7 +346,7 @@ abstract class Module extends Component if ($this->_modules[$id] instanceof Module) { return $this->_modules[$id]; } elseif ($load) { - Yii::trace("Loading module: $id", __CLASS__); + Yii::trace("Loading module: $id", __METHOD__); return $this->_modules[$id] = Yii::createObject($this->_modules[$id], $id, $this); } } @@ -452,7 +452,7 @@ abstract class Module extends Component if ($this->_components[$id] instanceof Component) { return $this->_components[$id]; } elseif ($load) { - Yii::trace("Loading component: $id", __CLASS__); + Yii::trace("Loading component: $id", __METHOD__); return $this->_components[$id] = Yii::createObject($this->_components[$id]); } } diff --git a/framework/db/Command.php b/framework/db/Command.php index ecd3674..a30aa14 100644 --- a/framework/db/Command.php +++ b/framework/db/Command.php @@ -134,7 +134,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()); } @@ -266,15 +266,16 @@ class Command extends \yii\base\Component $paramLog = "\nParameters: " . var_export($this->_params, true); } - Yii::trace("Executing SQL: {$sql}{$paramLog}", __CLASS__); + Yii::trace("Executing SQL: {$sql}{$paramLog}", __METHOD__); if ($sql == '') { return 0; } try { + $token = "SQL: $sql"; if ($this->db->enableProfiling) { - Yii::beginProfile(__METHOD__ . "($sql)", __CLASS__); + Yii::beginProfile($token, __METHOD__); } $this->prepare(); @@ -282,16 +283,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: {$sql}{$paramLog}", __METHOD__); $errorInfo = $e instanceof \PDOException ? $e->errorInfo : null; throw new Exception($message, $errorInfo, (int)$e->getCode()); @@ -383,7 +384,7 @@ class Command extends \yii\base\Component $paramLog = "\nParameters: " . var_export($this->_params, true); } - Yii::trace("Querying SQL: {$sql}{$paramLog}", __CLASS__); + Yii::trace("Querying SQL: {$sql}{$paramLog}", __METHOD__); /** @var $cache \yii\caching\Cache */ if ($db->enableQueryCache && $method !== '') { @@ -399,14 +400,15 @@ class Command extends \yii\base\Component $paramLog, )); if (($result = $cache->get($cacheKey)) !== false) { - Yii::trace('Query result served from 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(); @@ -423,21 +425,21 @@ class Command extends \yii\base\Component } if ($db->enableProfiling) { - Yii::endProfile(__METHOD__ . "($sql)", __CLASS__); + Yii::endProfile($token, __METHOD__); } 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: {$sql}{$paramLog}", __METHOD__); $errorInfo = $e instanceof \PDOException ? $e->errorInfo : null; throw new Exception($message, $errorInfo, (int)$e->getCode()); } diff --git a/framework/db/Connection.php b/framework/db/Connection.php index 59e8422..e84970b 100644 --- a/framework/db/Connection.php +++ b/framework/db/Connection.php @@ -324,12 +324,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()); } @@ -343,7 +343,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; 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/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/web/Identity.php b/framework/web/Identity.php index 4668337..805d3d4 100644 --- a/framework/web/Identity.php +++ b/framework/web/Identity.php @@ -16,15 +16,14 @@ interface Identity { /** * Returns an ID that can uniquely identify a user identity. - * The returned ID can be a string, an integer, or any serializable data. - * @return mixed an ID that uniquely identifies 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 space of such keys should be big and random enough to defeat potential identity attacks. * The returned key can be a string, an integer, or any serializable data. - * @return mixed a key that is used to check the validity of a given identity ID. + * @return string a key that is used to check the validity of a given identity ID. * @see validateAuthKey() */ public function getAuthKey(); @@ -37,7 +36,7 @@ interface Identity public function validateAuthKey($authKey); /** * Finds an identity by the given ID. - * @param mixed $id the ID to be looked for + * @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. */ diff --git a/framework/web/Session.php b/framework/web/Session.php index c289db2..3e0f599 100644 --- a/framework/web/Session.php +++ b/framework/web/Session.php @@ -117,7 +117,7 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co $this->_opened = false; $error = error_get_last(); $message = isset($error['message']) ? $error['message'] : 'Failed to start session.'; - Yii::error($message, __CLASS__); + Yii::error($message, __METHOD__); } else { $this->_opened = true; $this->updateFlashCounters(); diff --git a/framework/web/User.php b/framework/web/User.php index 2326a10..74b5f18 100644 --- a/framework/web/User.php +++ b/framework/web/User.php @@ -90,6 +90,9 @@ class User extends Component { 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.'); } @@ -179,7 +182,13 @@ class User extends Component /** @var $class Identity */ $class = $this->identityClass; $identity = $class::findIdentity($id); - if ($identity !== null && $identity->validateAuthKey($authKey) && $this->beforeLogin($identity, true)) { + if ($identity === null || !$identity->validateAuthKey($authKey)) { + if ($identity !== null) { + Yii::warning("Invalid auth key attempted for user '$id': $authKey", __METHOD__); + } + return; + } + if ($this->beforeLogin($identity, true)) { $this->switchIdentity($identity); if ($this->autoRenewCookie) { $this->saveIdentityCookie($identity, $duration); From 4d59587320160248838036163ef205fc5730efc4 Mon Sep 17 00:00:00 2001 From: Carsten Brandt <mail@cebe.cc> Date: Fri, 29 Mar 2013 01:10:03 +0100 Subject: [PATCH 18/41] fixed constructor overriding in test classes now use setUp() instead --- tests/unit/MysqlTestCase.php | 4 ++-- tests/unit/framework/caching/DbCacheTest.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) 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/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.'); From 6aa86712e2d5fa37bbd632dbab553bb9b4623a50 Mon Sep 17 00:00:00 2001 From: Qiang Xue <qiang.xue@gmail.com> Date: Thu, 28 Mar 2013 21:51:31 -0400 Subject: [PATCH 19/41] User WIP --- framework/web/Application.php | 30 ++++- framework/web/Controller.php | 15 +++ framework/web/Identity.php | 3 +- framework/web/Response.php | 65 ++++++----- framework/web/User.php | 252 +++++++++++------------------------------- 5 files changed, 147 insertions(+), 218 deletions(-) diff --git a/framework/web/Application.php b/framework/web/Application.php index 2533f04..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 diff --git a/framework/web/Controller.php b/framework/web/Controller.php index 93b74aa..126fbbc 100644 --- a/framework/web/Controller.php +++ b/framework/web/Controller.php @@ -8,6 +8,7 @@ namespace yii\web; use Yii; +use yii\helpers\Html; /** * Controller is the base class of Web controllers. @@ -41,4 +42,18 @@ class Controller extends \yii\base\Controller return Yii::$app->getUrlManager()->createUrl($route, $params); } + /** + * Redirects the browser to the specified URL or route (controller/action). + * @param mixed $url the URL to be redirected to. If the parameter is an array, + * the first element must be a route to a controller action and the rest + * are GET parameters in name-value pairs. + * @param boolean $terminate whether to terminate the current application after calling this method. Defaults to true. + * @param integer $statusCode the HTTP status code. Defaults to 302. See {@link http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html} + * for details about HTTP status code. + */ + public function redirect($url, $terminate = true, $statusCode = 302) + { + $url = Html::url($url); + Yii::$app->getResponse()->redirect($url, $terminate, $statusCode); + } } \ No newline at end of file diff --git a/framework/web/Identity.php b/framework/web/Identity.php index 805d3d4..5f07d58 100644 --- a/framework/web/Identity.php +++ b/framework/web/Identity.php @@ -38,7 +38,8 @@ 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. + * 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); } \ No newline at end of file diff --git a/framework/web/Response.php b/framework/web/Response.php index d23c5b9..8133132 100644 --- a/framework/web/Response.php +++ b/framework/web/Response.php @@ -107,37 +107,42 @@ class Response extends \yii\base\Response * <li>addHeaders: an array of additional http headers in header-value pairs (available since version 1.1.10)</li> * </ul> */ - 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('Content-Disposition: ' . $disposition . '; filename="' . $options['saveName'] . '"'); + if (isset($options['addHeaders'])) { + foreach ($options['addHeaders'] as $header => $value) { + header($header . ': ' . $value); + } } - header(trim($options['xHeader']).': '.$filePath); + header(trim($options['xHeader']) . ': ' . $filePath); - if(!isset($options['terminate']) || $options['terminate']) - Yii::app()->end(); + if (!isset($options['terminate']) || $options['terminate']) { + Yii::$app->end(); + } } /** @@ -148,13 +153,15 @@ class Response extends \yii\base\Response * @param integer $statusCode the HTTP status code. Defaults to 302. See {@link http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html} * for details about HTTP status code. */ - 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(); + if (strpos($url, '/') === 0 && strpos($url, '//') !== 0) { + $url = Yii::$app->getRequest()->getHostInfo() . $url; + } + header('Location: ' . $url, true, $statusCode); + if ($terminate) { + Yii::$app->end(); + } } diff --git a/framework/web/User.php b/framework/web/User.php index 74b5f18..aa9e421 100644 --- a/framework/web/User.php +++ b/framework/web/User.php @@ -9,7 +9,9 @@ namespace yii\web; use Yii; use yii\base\Component; +use yii\base\HttpException; use yii\base\InvalidConfigException; +use yii\helpers\Html; /** * @author Qiang Xue <qiang.xue@gmail.com> @@ -17,15 +19,21 @@ use yii\base\InvalidConfigException; */ class User extends Component { - const ID_VAR = '__id'; - const AUTH_EXPIRE_VAR = '__expire'; - const EVENT_BEFORE_LOGIN = 'beforeLogin'; const EVENT_AFTER_LOGIN = 'afterLogin'; const EVENT_BEFORE_LOGOUT = 'beforeLogout'; const EVENT_AFTER_LOGOUT = 'afterLogout'; /** + * @var Identity the identity object associated with the currently logged user. + * This property is set automatically be the User component. Do not modify it directly + * unless you understand the consequence. You should normally use [[login()]], [[logout()]], + * or [[switchIdentity()]] to update the identity associated with the current user. + * + * If this property is null, it means the current user is a guest (not authenticated). + */ + public $identity; + /** * @var string the class name of the [[identity]] object. */ public $identityClass; @@ -64,24 +72,12 @@ class User extends Component * 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 enableAutoLogin - * @since 1.1.0 - */ - public $autoRenewCookie = false; - /** - * @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 */ - public $loginRequiredAjaxResponse; - + public $autoRenewCookie = true; - public $stateVar = '__states'; + public $idSessionVar = '__id'; + public $authTimeoutSessionVar = '__expire'; + public $returnUrlSessionVar = '__returnUrl'; /** * Initializes the application component. @@ -99,6 +95,8 @@ class User extends Component Yii::$app->getSession()->open(); + $this->loadIdentity(); + $this->renewAuthStatus(); if ($this->enableAutoLogin) { @@ -110,29 +108,16 @@ class User extends Component } } - /** - * @var Identity the identity object associated with the currently logged user. - */ - private $_identity = false; - - public function getIdentity() + public function loadIdentity() { - if ($this->_identity === false) { - $id = $this->getId(); - if ($id === null) { - $this->_identity = null; - } else { - /** @var $class Identity */ - $class = $this->identityClass; - $this->_identity = $class::findIdentity($this->getId()); - } + $id = $this->getId(); + if ($id === null) { + $this->identity = null; + } else { + /** @var $class Identity */ + $class = $this->identityClass; + $this->identity = $class::findIdentity($this->getId()); } - return $this->_identity; - } - - public function setIdentity($identity) - { - $this->switchIdentity($identity); } /** @@ -157,7 +142,7 @@ class User extends Component if ($this->beforeLogin($identity, false)) { $this->switchIdentity($identity); if ($duration > 0 && $this->enableAutoLogin) { - $this->saveIdentityCookie($identity, $duration); + $this->sendIdentityCookie($identity, $duration); } $this->afterLogin($identity, false); } @@ -169,7 +154,7 @@ class User extends Component * This method is used when automatic login ({@link enableAutoLogin}) is enabled. * The user identity information is recovered from cookie. * Sufficient security measures are used to prevent cookie data from being tampered. - * @see saveIdentityCookie + * @see sendIdentityCookie */ protected function loginByCookie() { @@ -182,18 +167,16 @@ class User extends Component /** @var $class Identity */ $class = $this->identityClass; $identity = $class::findIdentity($id); - if ($identity === null || !$identity->validateAuthKey($authKey)) { - if ($identity !== null) { - Yii::warning("Invalid auth key attempted for user '$id': $authKey", __METHOD__); + if ($identity !== null && $identity->validateAuthKey($authKey)) { + if ($this->beforeLogin($identity, true)) { + $this->switchIdentity($identity); + if ($this->autoRenewCookie) { + $this->sendIdentityCookie($identity, $duration); + } + $this->afterLogin($identity, true); } - return; - } - if ($this->beforeLogin($identity, true)) { - $this->switchIdentity($identity); - if ($this->autoRenewCookie) { - $this->saveIdentityCookie($identity, $duration); - } - $this->afterLogin($identity, true); + } elseif ($identity !== null) { + Yii::warning("Invalid auth key attempted for user '$id': $authKey", __METHOD__); } } } @@ -208,7 +191,7 @@ class User extends Component */ public function logout($destroySession = true) { - $identity = $this->getIdentity(); + $identity = $this->identity; if ($identity !== null && $this->beforeLogout($identity)) { $this->switchIdentity(null); if ($this->enableAutoLogin) { @@ -227,7 +210,7 @@ class User extends Component */ public function getIsGuest() { - return $this->getIdentity() === null; + return $this->identity === null; } /** @@ -236,7 +219,7 @@ class User extends Component */ public function getId() { - return $this->getState(static::ID_VAR); + return Yii::$app->getSession()->get($this->idSessionVar); } /** @@ -244,7 +227,7 @@ class User extends Component */ public function setId($value) { - $this->setState(static::ID_VAR, $value); + Yii::$app->getSession()->set($this->idSessionVar, $value); } /** @@ -258,12 +241,12 @@ class User extends Component */ public function getReturnUrl($defaultUrl = null) { - if ($defaultUrl === null) { - $defaultReturnUrl = Yii::app()->getUrlManager()->showScriptName ? Yii::app()->getRequest()->getScriptUrl() : Yii::app()->getRequest()->getBaseUrl() . '/'; + $url = Yii::$app->getSession()->get($this->returnUrlSessionVar, $defaultUrl); + if ($url === null) { + return Yii::$app->getHomeUrl(); } else { - $defaultReturnUrl = CHtml::normalizeUrl($defaultUrl); + return Html::url($url); } - return $this->getState('__returnUrl', $defaultReturnUrl); } /** @@ -271,7 +254,7 @@ class User extends Component */ public function setReturnUrl($value) { - $this->setState('__returnUrl', $value); + Yii::$app->getSession()->set($this->returnUrlSessionVar, $value); } /** @@ -285,24 +268,22 @@ class User extends Component */ public function loginRequired() { - $app = Yii::app(); - $request = $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)); + $url = Html::url($url); + $request = Yii::$app->getRequest(); + if (strpos($url, '/') === 0 && strpos($url, '//') !== 0) { + $url = $request->getHostInfo() . $url; + } + if ($request->getIsAjaxRequest()) { + echo json_encode(array( + 'redirect' => $url, + )); + Yii::$app->end(); + } else { + Yii::$app->getResponse()->redirect($url); } - $request->redirect($url); } else { - throw new CHttpException(403, Yii::t('yii', 'Login Required')); + throw new HttpException(403, Yii::t('yii|Login Required')); } } @@ -399,7 +380,7 @@ class User extends Component * @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 loginByCookie */ - protected function saveIdentityCookie($identity, $duration) + protected function sendIdentityCookie($identity, $duration) { $cookie = new Cookie($this->identityCookie); $cookie->value = json_encode(array( @@ -423,14 +404,16 @@ class User extends Component protected function switchIdentity($identity) { Yii::$app->getSession()->regenerateID(true); - $this->setIdentity($identity); + $this->identity = $identity; if ($identity instanceof Identity) { $this->setId($identity->getId()); if ($this->authTimeout !== null) { - $this->setState(self::AUTH_EXPIRE_VAR, time() + $this->authTimeout); + Yii::$app->getSession()->set($this->authTimeoutSessionVar, time() + $this->authTimeout); } } else { - $this->removeAllStates(); + $session = Yii::$app->getSession(); + $session->remove($this->idSessionVar); + $session->remove($this->authTimeoutSessionVar); } } @@ -442,115 +425,12 @@ class User extends Component protected function renewAuthStatus() { if ($this->authTimeout !== null && !$this->getIsGuest()) { - $expire = $this->getState(self::AUTH_EXPIRE_VAR); + $expire = Yii::$app->getSession()->get($this->authTimeoutSessionVar); if ($expire !== null && $expire < time()) { $this->logout(false); } else { - $this->setState(self::AUTH_EXPIRE_VAR, time() + $this->authTimeout); + Yii::$app->getSession()->set($this->authTimeoutSessionVar, time() + $this->authTimeout); } } } - - /** - * Returns a user state. - * A user state is a session data item associated with the current user. - * If the user logs out, all his/her user states will be removed. - * @param string $key the key identifying the state - * @param mixed $defaultValue value to be returned if the state does not exist. - * @return mixed the state - */ - public function getState($key, $defaultValue = null) - { - $manifest = isset($_SESSION[$this->stateVar]) ? $_SESSION[$this->stateVar] : null; - if (is_array($manifest) && isset($manifest[$key], $_SESSION[$key])) { - return $_SESSION[$key]; - } else { - return $defaultValue; - } - } - - /** - * Returns all user states. - * @return array states (key => state). - */ - public function getAllStates() - { - $manifest = isset($_SESSION[$this->stateVar]) ? $_SESSION[$this->stateVar] : null; - $states = array(); - if (is_array($manifest)) { - foreach (array_keys($manifest) as $key) { - if (isset($_SESSION[$key])) { - $states[$key] = $_SESSION[$key]; - } - } - } - return $states; - } - - /** - * Stores a user state. - * A user state is a session data item associated with the current user. - * If the user logs out, all his/her user states will be removed. - * @param string $key the key identifying the state. Note that states - * 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 state - */ - public function setState($key, $value) - { - $manifest = isset($_SESSION[$this->stateVar]) ? $_SESSION[$this->stateVar] : array(); - $manifest[$value] = true; - $_SESSION[$key] = $value; - $_SESSION[$this->stateVar] = $manifest; - } - - /** - * Removes a user state. - * If the user logs out, all his/her user states will be removed automatically. - * @param string $key the key identifying the state. Note that states - * 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 state. Null if the state does not exist. - */ - public function removeState($key) - { - $manifest = isset($_SESSION[$this->stateVar]) ? $_SESSION[$this->stateVar] : null; - if (is_array($manifest) && isset($manifest[$key], $_SESSION[$key])) { - $value = $_SESSION[$key]; - } else { - $value = null; - } - unset($_SESSION[$this->stateVar][$key], $_SESSION[$key]); - return $value; - } - - /** - * Removes all states. - * If the user logs out, all his/her user states will be removed automatically - * without the need to call this method manually. - * - * Note that states 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 removeAllStates() - { - $manifest = isset($_SESSION[$this->stateVar]) ? $_SESSION[$this->stateVar] : null; - if (is_array($manifest)) { - foreach (array_keys($manifest) as $key) { - unset($_SESSION[$key]); - } - } - unset($_SESSION[$this->stateVar]); - } - - /** - * Returns a value indicating whether there is a state associated with the specified key. - * @param string $key key identifying the state - * @return boolean whether the specified state exists - */ - public function hasState($key) - { - return $this->getState($key) !== null; - } } From c0d0d1dc357b7b79307a751972f9e9be10606a0c Mon Sep 17 00:00:00 2001 From: Qiang Xue <qiang.xue@gmail.com> Date: Thu, 28 Mar 2013 23:53:36 -0400 Subject: [PATCH 20/41] Finished User. --- framework/web/Controller.php | 15 ---- framework/web/Identity.php | 20 +++-- framework/web/Response.php | 38 +++++++- framework/web/User.php | 201 +++++++++++++++++++++---------------------- framework/web/UserEvent.php | 2 +- 5 files changed, 146 insertions(+), 130 deletions(-) diff --git a/framework/web/Controller.php b/framework/web/Controller.php index 126fbbc..8049299 100644 --- a/framework/web/Controller.php +++ b/framework/web/Controller.php @@ -41,19 +41,4 @@ class Controller extends \yii\base\Controller } return Yii::$app->getUrlManager()->createUrl($route, $params); } - - /** - * Redirects the browser to the specified URL or route (controller/action). - * @param mixed $url the URL to be redirected to. If the parameter is an array, - * the first element must be a route to a controller action and the rest - * are GET parameters in name-value pairs. - * @param boolean $terminate whether to terminate the current application after calling this method. Defaults to true. - * @param integer $statusCode the HTTP status code. Defaults to 302. See {@link http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html} - * for details about HTTP status code. - */ - public function redirect($url, $terminate = true, $statusCode = 302) - { - $url = Html::url($url); - Yii::$app->getResponse()->redirect($url, $terminate, $statusCode); - } } \ No newline at end of file diff --git a/framework/web/Identity.php b/framework/web/Identity.php index 5f07d58..89d4282 100644 --- a/framework/web/Identity.php +++ b/framework/web/Identity.php @@ -15,6 +15,14 @@ namespace yii\web; 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. */ @@ -23,23 +31,19 @@ interface Identity * Returns a key that can be used to check the validity of a given identity ID. * The space of such keys should be big and random enough to defeat potential identity attacks. * The returned key can be a string, an integer, or any serializable data. + * + * 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); - /** - * 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); } \ No newline at end of file diff --git a/framework/web/Response.php b/framework/web/Response.php index 8133132..da2482f 100644 --- a/framework/web/Response.php +++ b/framework/web/Response.php @@ -9,6 +9,7 @@ namespace yii\web; use Yii; use yii\helpers\FileHelper; +use yii\helpers\Html; /** * @author Qiang Xue <qiang.xue@gmail.com> @@ -17,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. @@ -147,24 +156,45 @@ class Response extends \yii\base\Response /** * 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) { + $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, diff --git a/framework/web/User.php b/framework/web/User.php index aa9e421..ebfcd03 100644 --- a/framework/web/User.php +++ b/framework/web/User.php @@ -11,7 +11,6 @@ use Yii; use yii\base\Component; use yii\base\HttpException; use yii\base\InvalidConfigException; -use yii\helpers\Html; /** * @author Qiang Xue <qiang.xue@gmail.com> @@ -67,17 +66,27 @@ class User extends Component 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 enableAutoLogin} 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 enableAutoLogin */ public $autoRenewCookie = true; + /** + * @var string the session variable name used to store the value of [[id]]. + */ + public $idVar = '__id'; + /** + * @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 $authTimeoutVar = '__expire'; + /** + * @var string the session variable name used to store the value of [[returnUrl]]. + */ + public $returnUrlVar = '__returnUrl'; - public $idSessionVar = '__id'; - public $authTimeoutSessionVar = '__expire'; - public $returnUrlSessionVar = '__returnUrl'; /** * Initializes the application component. @@ -108,7 +117,10 @@ class User extends Component } } - public function loadIdentity() + /** + * Loads the [[identity]] object according to [[id]]. + */ + protected function loadIdentity() { $id = $this->getId(); if ($id === null) { @@ -116,25 +128,22 @@ class User extends Component } else { /** @var $class Identity */ $class = $this->identityClass; - $this->identity = $class::findIdentity($this->getId()); + $this->identity = $class::findIdentity($id); } } /** * 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. - * - * Note, you have to set {@link enableAutoLogin} to true - * if you want to allow user to be authenticated based on the cookie information. + * 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. * * @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. - * If greater than 0, cookie-based login will be used. In this case, {@link enableAutoLogin} - * must be set true, otherwise an exception will be thrown. + * @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) @@ -150,11 +159,10 @@ class User extends Component } /** - * Populates the current user object with the information obtained from cookie. - * This method is used when automatic login ({@link enableAutoLogin}) is enabled. - * The user identity information is recovered from cookie. - * Sufficient security measures are used to prevent cookie data from being tampered. - * @see sendIdentityCookie + * 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() { @@ -185,9 +193,8 @@ class User extends Component /** * 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) { @@ -215,73 +222,63 @@ class User extends Component /** * 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 Yii::$app->getSession()->get($this->idSessionVar); + return Yii::$app->getSession()->get($this->idVar); } /** - * @param mixed $value the unique identifier for the user. If null, it means the user is a guest. + * @param string|integer $value the unique identifier for the user. If null, it means the user is a guest. */ public function setId($value) { - Yii::$app->getSession()->set($this->idSessionVar, $value); + Yii::$app->getSession()->set($this->idVar, $value); } /** * 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) { - $url = Yii::$app->getSession()->get($this->returnUrlSessionVar, $defaultUrl); - if ($url === null) { - return Yii::$app->getHomeUrl(); - } else { - return Html::url($url); - } + $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) { - Yii::$app->getSession()->set($this->returnUrlSessionVar, $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() { - if (($url = $this->loginUrl) !== null) { - $url = Html::url($url); - $request = Yii::$app->getRequest(); - if (strpos($url, '/') === 0 && strpos($url, '//') !== 0) { - $url = $request->getHostInfo() . $url; - } - if ($request->getIsAjaxRequest()) { - echo json_encode(array( - 'redirect' => $url, - )); - Yii::$app->end(); - } else { - Yii::$app->getResponse()->redirect($url); - } + $request = Yii::$app->getRequest(); + if (!$request->getIsAjaxRequest()) { + $this->setReturnUrl($request->getUrl()); + } + if ($this->loginUrl !== null) { + Yii::$app->getResponse()->redirect($this->loginUrl); } else { throw new HttpException(403, Yii::t('yii|Login Required')); } @@ -289,21 +286,18 @@ class User extends Component /** * 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 + * 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($identity, $fromCookie) + protected function beforeLogin($identity, $cookieBased) { $event = new UserEvent(array( 'identity' => $identity, - 'fromCookie' => $fromCookie, + 'cookieBased' => $cookieBased, )); $this->trigger(self::EVENT_BEFORE_LOGIN, $event); return $event->isValid; @@ -311,24 +305,27 @@ class User extends Component /** * 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. + * 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($identity, $fromCookie) + protected function afterLogin($identity, $cookieBased) { $this->trigger(self::EVENT_AFTER_LOGIN, new UserEvent(array( 'identity' => $identity, - 'fromCookie' => $fromCookie, + '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 + * 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($identity) { @@ -340,8 +337,11 @@ class User extends Component } /** - * 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. + * 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($identity) { @@ -350,7 +350,6 @@ class User extends Component ))); } - /** * Renews the identity cookie. * This method will set the expiration time of the identity cookie to be the current time @@ -372,12 +371,12 @@ class User extends Component } /** - * Saves necessary user data into a cookie. - * This method is used when automatic login ({@link enableAutoLogin}) 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. + * 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. Defaults to 0, meaning login till the user closes the browser. + * @param integer $duration number of seconds that the user can remain in logged-in status. * @see loginByCookie */ protected function sendIdentityCookie($identity, $duration) @@ -394,42 +393,40 @@ class User extends Component /** * 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 Identity $identity a unique identifier for the user + * This method is called by [[login()]] and [[loginByCookie()]] + * when the current user needs to be associated with the corresponding + * identity information. + * @param Identity $identity the identity information to be associated with the current user. */ protected function switchIdentity($identity) { Yii::$app->getSession()->regenerateID(true); $this->identity = $identity; + $session = Yii::$app->getSession(); + $session->remove($this->idVar); + $session->remove($this->authTimeoutVar); if ($identity instanceof Identity) { $this->setId($identity->getId()); if ($this->authTimeout !== null) { - Yii::$app->getSession()->set($this->authTimeoutSessionVar, time() + $this->authTimeout); + Yii::$app->getSession()->set($this->authTimeoutVar, time() + $this->authTimeout); } - } else { - $session = Yii::$app->getSession(); - $session->remove($this->idSessionVar); - $session->remove($this->authTimeoutSessionVar); } } /** - * Updates the authentication status according to {@link authTimeout}. - * If the user has been inactive for {@link authTimeout} seconds, - * he will be automatically logged out. + * 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 renewAuthStatus() { - if ($this->authTimeout !== null && !$this->getIsGuest()) { - $expire = Yii::$app->getSession()->get($this->authTimeoutSessionVar); + if ($this->authTimeout !== null && $this->identity !== null) { + $expire = Yii::$app->getSession()->get($this->authTimeoutVar); if ($expire !== null && $expire < time()) { $this->logout(false); } else { - Yii::$app->getSession()->set($this->authTimeoutSessionVar, time() + $this->authTimeout); + Yii::$app->getSession()->set($this->authTimeoutVar, time() + $this->authTimeout); } } } diff --git a/framework/web/UserEvent.php b/framework/web/UserEvent.php index 3a8723a..6955ae5 100644 --- a/framework/web/UserEvent.php +++ b/framework/web/UserEvent.php @@ -24,7 +24,7 @@ class UserEvent extends Event * @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 $fromCookie; + 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. From 4d93e468d6e6d6f51b301a61fb722dd4e55510d7 Mon Sep 17 00:00:00 2001 From: Qiang Xue <qiang.xue@gmail.com> Date: Fri, 29 Mar 2013 08:17:23 -0400 Subject: [PATCH 21/41] Fixed UserEvent bug. Updated docs. --- framework/web/User.php | 8 ++++++++ framework/web/UserEvent.php | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/framework/web/User.php b/framework/web/User.php index ebfcd03..5b5f977 100644 --- a/framework/web/User.php +++ b/framework/web/User.php @@ -13,6 +13,14 @@ use yii\base\HttpException; use yii\base\InvalidConfigException; /** + * User is an application component that manages the user authentication status. + * + * 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. + * + * 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 <qiang.xue@gmail.com> * @since 2.0 */ diff --git a/framework/web/UserEvent.php b/framework/web/UserEvent.php index 6955ae5..7a5d23d 100644 --- a/framework/web/UserEvent.php +++ b/framework/web/UserEvent.php @@ -30,5 +30,5 @@ class UserEvent extends Event * 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; + public $isValid = true; } \ No newline at end of file From 4f2a69e9e1612e3fea4ae10eeda82733bfdc95b8 Mon Sep 17 00:00:00 2001 From: Qiang Xue <qiang.xue@gmail.com> Date: Fri, 29 Mar 2013 08:26:04 -0400 Subject: [PATCH 22/41] Added IP to log result. --- framework/logging/Target.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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"; } } From 00319fb898ea095c9ccb79901d661cd1678c04e1 Mon Sep 17 00:00:00 2001 From: Qiang Xue <qiang.xue@gmail.com> Date: Fri, 29 Mar 2013 08:32:58 -0400 Subject: [PATCH 23/41] Fixed about previous exception. --- framework/base/HttpException.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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); } /** From 9da81894be1b0268eab2216b44c4bc48e5e24fd5 Mon Sep 17 00:00:00 2001 From: Alexander Makarov <sam@rmcreative.ru> Date: Fri, 29 Mar 2013 18:23:50 +0400 Subject: [PATCH 24/41] moved helper test from util namespace to helpers namespace, added tests for StringHelper --- tests/unit/framework/helpers/ArrayHelperTest.php | 50 +++ tests/unit/framework/helpers/HtmlTest.php | 448 ++++++++++++++++++++++ tests/unit/framework/helpers/StringHelperTest.php | 73 ++++ tests/unit/framework/util/ArrayHelperTest.php | 50 --- tests/unit/framework/util/HtmlTest.php | 448 ---------------------- 5 files changed, 571 insertions(+), 498 deletions(-) create mode 100644 tests/unit/framework/helpers/ArrayHelperTest.php create mode 100644 tests/unit/framework/helpers/HtmlTest.php create mode 100644 tests/unit/framework/helpers/StringHelperTest.php delete mode 100644 tests/unit/framework/util/ArrayHelperTest.php delete mode 100644 tests/unit/framework/util/HtmlTest.php diff --git a/tests/unit/framework/helpers/ArrayHelperTest.php b/tests/unit/framework/helpers/ArrayHelperTest.php new file mode 100644 index 0000000..187217f --- /dev/null +++ b/tests/unit/framework/helpers/ArrayHelperTest.php @@ -0,0 +1,50 @@ +<?php + +namespace yiiunit\framework\helpers; + +use yii\helpers\ArrayHelper; + +class ArrayHelperTest extends \yii\test\TestCase +{ + public function testMerge() + { + + + } + + public function testMultisort() + { + // single key + $array = array( + array('name' => 'b', 'age' => 3), + array('name' => 'a', 'age' => 1), + array('name' => 'c', 'age' => 2), + ); + ArrayHelper::multisort($array, 'name'); + $this->assertEquals(array('name' => 'a', 'age' => 1), $array[0]); + $this->assertEquals(array('name' => 'b', 'age' => 3), $array[1]); + $this->assertEquals(array('name' => 'c', 'age' => 2), $array[2]); + + // multiple keys + $array = array( + array('name' => 'b', 'age' => 3), + array('name' => 'a', 'age' => 2), + array('name' => 'a', 'age' => 1), + ); + ArrayHelper::multisort($array, array('name', 'age')); + $this->assertEquals(array('name' => 'a', 'age' => 1), $array[0]); + $this->assertEquals(array('name' => 'a', 'age' => 2), $array[1]); + $this->assertEquals(array('name' => 'b', 'age' => 3), $array[2]); + + // case-insensitive + $array = array( + array('name' => 'a', 'age' => 3), + array('name' => 'b', 'age' => 2), + array('name' => 'A', 'age' => 1), + ); + ArrayHelper::multisort($array, array('name', 'age'), SORT_ASC, array(SORT_STRING|SORT_FLAG_CASE, SORT_REGULAR)); + $this->assertEquals(array('name' => 'A', 'age' => 1), $array[0]); + $this->assertEquals(array('name' => 'a', 'age' => 3), $array[1]); + $this->assertEquals(array('name' => 'b', 'age' => 2), $array[2]); + } +} diff --git a/tests/unit/framework/helpers/HtmlTest.php b/tests/unit/framework/helpers/HtmlTest.php new file mode 100644 index 0000000..2c3de72 --- /dev/null +++ b/tests/unit/framework/helpers/HtmlTest.php @@ -0,0 +1,448 @@ +<?php + +namespace yiiunit\framework\helpers; + +use Yii; +use yii\helpers\Html; +use yii\web\Application; + +class HtmlTest extends \yii\test\TestCase +{ + public function setUp() + { + new Application('test', '@yiiunit/runtime', array( + 'components' => array( + 'request' => array( + 'class' => 'yii\web\Request', + 'url' => '/test', + ), + ), + )); + } + + public function tearDown() + { + Yii::$app = null; + } + + public function testEncode() + { + $this->assertEquals("a<>&"'", Html::encode("a<>&\"'")); + } + + public function testDecode() + { + $this->assertEquals("a<>&\"'", Html::decode("a<>&"'")); + } + + public function testTag() + { + $this->assertEquals('<br />', Html::tag('br')); + $this->assertEquals('<span></span>', Html::tag('span')); + $this->assertEquals('<div>content</div>', Html::tag('div', 'content')); + $this->assertEquals('<input type="text" name="test" value="<>" />', Html::tag('input', '', array('type' => 'text', 'name' => 'test', 'value' => '<>'))); + + Html::$closeVoidElements = false; + + $this->assertEquals('<br>', Html::tag('br')); + $this->assertEquals('<span></span>', Html::tag('span')); + $this->assertEquals('<div>content</div>', Html::tag('div', 'content')); + $this->assertEquals('<input type="text" name="test" value="<>">', Html::tag('input', '', array('type' => 'text', 'name' => 'test', 'value' => '<>'))); + + Html::$closeVoidElements = true; + + $this->assertEquals('<span disabled="disabled"></span>', Html::tag('span', '', array('disabled' => true))); + Html::$showBooleanAttributeValues = false; + $this->assertEquals('<span disabled></span>', Html::tag('span', '', array('disabled' => true))); + Html::$showBooleanAttributeValues = true; + } + + public function testBeginTag() + { + $this->assertEquals('<br>', Html::beginTag('br')); + $this->assertEquals('<span id="test" class="title">', Html::beginTag('span', array('id' => 'test', 'class' => 'title'))); + } + + public function testEndTag() + { + $this->assertEquals('</br>', Html::endTag('br')); + $this->assertEquals('</span>', Html::endTag('span')); + } + + public function testCdata() + { + $data = 'test<>'; + $this->assertEquals('<![CDATA[' . $data . ']]>', Html::cdata($data)); + } + + public function testStyle() + { + $content = 'a <>'; + $this->assertEquals("<style type=\"text/css\">/*<![CDATA[*/\n{$content}\n/*]]>*/</style>", Html::style($content)); + $this->assertEquals("<style type=\"text/less\">/*<![CDATA[*/\n{$content}\n/*]]>*/</style>", Html::style($content, array('type' => 'text/less'))); + } + + public function testScript() + { + $content = 'a <>'; + $this->assertEquals("<script type=\"text/javascript\">/*<![CDATA[*/\n{$content}\n/*]]>*/</script>", Html::script($content)); + $this->assertEquals("<script type=\"text/js\">/*<![CDATA[*/\n{$content}\n/*]]>*/</script>", Html::script($content, array('type' => 'text/js'))); + } + + public function testCssFile() + { + $this->assertEquals('<link type="text/css" href="http://example.com" rel="stylesheet" />', Html::cssFile('http://example.com')); + $this->assertEquals('<link type="text/css" href="/test" rel="stylesheet" />', Html::cssFile('')); + } + + public function testJsFile() + { + $this->assertEquals('<script type="text/javascript" src="http://example.com"></script>', Html::jsFile('http://example.com')); + $this->assertEquals('<script type="text/javascript" src="/test"></script>', Html::jsFile('')); + } + + public function testBeginForm() + { + $this->assertEquals('<form action="/test" method="post">', Html::beginForm()); + $this->assertEquals('<form action="/example" method="get">', Html::beginForm('/example', 'get')); + $hiddens = array( + '<input type="hidden" name="id" value="1" />', + '<input type="hidden" name="title" value="<" />', + ); + $this->assertEquals('<form action="/example" method="get">' . "\n" . implode("\n", $hiddens), Html::beginForm('/example?id=1&title=%3C', 'get')); + } + + public function testEndForm() + { + $this->assertEquals('</form>', Html::endForm()); + } + + public function testA() + { + $this->assertEquals('<a>something<></a>', Html::a('something<>')); + $this->assertEquals('<a href="/example">something</a>', Html::a('something', '/example')); + $this->assertEquals('<a href="/test">something</a>', Html::a('something', '')); + } + + public function testMailto() + { + $this->assertEquals('<a href="mailto:test<>">test<></a>', Html::mailto('test<>')); + $this->assertEquals('<a href="mailto:test>">test<></a>', Html::mailto('test<>', 'test>')); + } + + public function testImg() + { + $this->assertEquals('<img src="/example" alt="" />', Html::img('/example')); + $this->assertEquals('<img src="/test" alt="" />', Html::img('')); + $this->assertEquals('<img src="/example" width="10" alt="something" />', Html::img('/example', array('alt' => 'something', 'width' => 10))); + } + + public function testLabel() + { + $this->assertEquals('<label>something<></label>', Html::label('something<>')); + $this->assertEquals('<label for="a">something<></label>', Html::label('something<>', 'a')); + $this->assertEquals('<label class="test" for="a">something<></label>', Html::label('something<>', 'a', array('class' => 'test'))); + } + + public function testButton() + { + $this->assertEquals('<button type="button">Button</button>', Html::button()); + $this->assertEquals('<button type="button" name="test" value="value">content<></button>', Html::button('test', 'value', 'content<>')); + $this->assertEquals('<button type="submit" class="t" name="test" value="value">content<></button>', Html::button('test', 'value', 'content<>', array('type' => 'submit', 'class' => "t"))); + } + + public function testSubmitButton() + { + $this->assertEquals('<button type="submit">Submit</button>', Html::submitButton()); + $this->assertEquals('<button type="submit" class="t" name="test" value="value">content<></button>', Html::submitButton('test', 'value', 'content<>', array('class' => 't'))); + } + + public function testResetButton() + { + $this->assertEquals('<button type="reset">Reset</button>', Html::resetButton()); + $this->assertEquals('<button type="reset" class="t" name="test" value="value">content<></button>', Html::resetButton('test', 'value', 'content<>', array('class' => 't'))); + } + + public function testInput() + { + $this->assertEquals('<input type="text" />', Html::input('text')); + $this->assertEquals('<input type="text" class="t" name="test" value="value" />', Html::input('text', 'test', 'value', array('class' => 't'))); + } + + public function testButtonInput() + { + $this->assertEquals('<input type="button" name="test" value="Button" />', Html::buttonInput('test')); + $this->assertEquals('<input type="button" class="a" name="test" value="text" />', Html::buttonInput('test', 'text', array('class' => 'a'))); + } + + public function testSubmitInput() + { + $this->assertEquals('<input type="submit" value="Submit" />', Html::submitInput()); + $this->assertEquals('<input type="submit" class="a" name="test" value="text" />', Html::submitInput('test', 'text', array('class' => 'a'))); + } + + public function testResetInput() + { + $this->assertEquals('<input type="reset" value="Reset" />', Html::resetInput()); + $this->assertEquals('<input type="reset" class="a" name="test" value="text" />', Html::resetInput('test', 'text', array('class' => 'a'))); + } + + public function testTextInput() + { + $this->assertEquals('<input type="text" name="test" />', Html::textInput('test')); + $this->assertEquals('<input type="text" class="t" name="test" value="value" />', Html::textInput('test', 'value', array('class' => 't'))); + } + + public function testHiddenInput() + { + $this->assertEquals('<input type="hidden" name="test" />', Html::hiddenInput('test')); + $this->assertEquals('<input type="hidden" class="t" name="test" value="value" />', Html::hiddenInput('test', 'value', array('class' => 't'))); + } + + public function testPasswordInput() + { + $this->assertEquals('<input type="password" name="test" />', Html::passwordInput('test')); + $this->assertEquals('<input type="password" class="t" name="test" value="value" />', Html::passwordInput('test', 'value', array('class' => 't'))); + } + + public function testFileInput() + { + $this->assertEquals('<input type="file" name="test" />', Html::fileInput('test')); + $this->assertEquals('<input type="file" class="t" name="test" value="value" />', Html::fileInput('test', 'value', array('class' => 't'))); + } + + public function testTextarea() + { + $this->assertEquals('<textarea name="test"></textarea>', Html::textarea('test')); + $this->assertEquals('<textarea class="t" name="test">value<></textarea>', Html::textarea('test', 'value<>', array('class' => 't'))); + } + + public function testRadio() + { + $this->assertEquals('<input type="radio" name="test" value="1" />', Html::radio('test')); + $this->assertEquals('<input type="radio" class="a" name="test" checked="checked" />', Html::radio('test', true, null, array('class' => 'a'))); + $this->assertEquals('<input type="hidden" name="test" value="0" /><input type="radio" class="a" name="test" value="2" checked="checked" />', Html::radio('test', true, 2, array('class' => 'a' , 'uncheck' => '0'))); + } + + public function testCheckbox() + { + $this->assertEquals('<input type="checkbox" name="test" value="1" />', Html::checkbox('test')); + $this->assertEquals('<input type="checkbox" class="a" name="test" checked="checked" />', Html::checkbox('test', true, null, array('class' => 'a'))); + $this->assertEquals('<input type="hidden" name="test" value="0" /><input type="checkbox" class="a" name="test" value="2" checked="checked" />', Html::checkbox('test', true, 2, array('class' => 'a', 'uncheck' => '0'))); + } + + public function testDropDownList() + { + $expected = <<<EOD +<select name="test"> + +</select> +EOD; + $this->assertEquals($expected, Html::dropDownList('test')); + $expected = <<<EOD +<select name="test"> +<option value="value1">text1</option> +<option value="value2">text2</option> +</select> +EOD; + $this->assertEquals($expected, Html::dropDownList('test', null, $this->getDataItems())); + $expected = <<<EOD +<select name="test"> +<option value="value1">text1</option> +<option value="value2" selected="selected">text2</option> +</select> +EOD; + $this->assertEquals($expected, Html::dropDownList('test', 'value2', $this->getDataItems())); + } + + public function testListBox() + { + $expected = <<<EOD +<select name="test" size="4"> + +</select> +EOD; + $this->assertEquals($expected, Html::listBox('test')); + $expected = <<<EOD +<select name="test" size="5"> +<option value="value1">text1</option> +<option value="value2">text2</option> +</select> +EOD; + $this->assertEquals($expected, Html::listBox('test', null, $this->getDataItems(), array('size' => 5))); + $expected = <<<EOD +<select name="test" size="4"> +<option value="value1<>">text1<></option> +<option value="value 2">text  2</option> +</select> +EOD; + $this->assertEquals($expected, Html::listBox('test', null, $this->getDataItems2())); + $expected = <<<EOD +<select name="test" size="4"> +<option value="value1">text1</option> +<option value="value2" selected="selected">text2</option> +</select> +EOD; + $this->assertEquals($expected, Html::listBox('test', 'value2', $this->getDataItems())); + $expected = <<<EOD +<select name="test" size="4"> +<option value="value1" selected="selected">text1</option> +<option value="value2" selected="selected">text2</option> +</select> +EOD; + $this->assertEquals($expected, Html::listBox('test', array('value1', 'value2'), $this->getDataItems())); + + $expected = <<<EOD +<select name="test[]" multiple="multiple" size="4"> + +</select> +EOD; + $this->assertEquals($expected, Html::listBox('test', null, array(), array('multiple' => true))); + $expected = <<<EOD +<input type="hidden" name="test" value="0" /><select name="test" size="4"> + +</select> +EOD; + $this->assertEquals($expected, Html::listBox('test', '', array(), array('unselect' => '0'))); + } + + public function testCheckboxList() + { + $this->assertEquals('', Html::checkboxList('test')); + + $expected = <<<EOD +<label><input type="checkbox" name="test[]" value="value1" /> text1</label> +<label><input type="checkbox" name="test[]" value="value2" checked="checked" /> text2</label> +EOD; + $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems())); + + $expected = <<<EOD +<label><input type="checkbox" name="test[]" value="value1<>" /> text1<></label> +<label><input type="checkbox" name="test[]" value="value 2" /> text 2</label> +EOD; + $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems2())); + + $expected = <<<EOD +<input type="hidden" name="test" value="0" /><label><input type="checkbox" name="test[]" value="value1" /> text1</label><br /> +<label><input type="checkbox" name="test[]" value="value2" checked="checked" /> text2</label> +EOD; + $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems(), array( + 'separator' => "<br />\n", + 'unselect' => '0', + ))); + + $expected = <<<EOD +0<label>text1 <input type="checkbox" name="test[]" value="value1" /></label> +1<label>text2 <input type="checkbox" name="test[]" value="value2" checked="checked" /></label> +EOD; + $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems(), array( + 'item' => function ($index, $label, $name, $checked, $value) { + return $index . Html::label($label . ' ' . Html::checkbox($name, $checked, $value)); + } + ))); + } + + public function testRadioList() + { + $this->assertEquals('', Html::radioList('test')); + + $expected = <<<EOD +<label><input type="radio" name="test" value="value1" /> text1</label> +<label><input type="radio" name="test" value="value2" checked="checked" /> text2</label> +EOD; + $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems())); + + $expected = <<<EOD +<label><input type="radio" name="test" value="value1<>" /> text1<></label> +<label><input type="radio" name="test" value="value 2" /> text 2</label> +EOD; + $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems2())); + + $expected = <<<EOD +<input type="hidden" name="test" value="0" /><label><input type="radio" name="test" value="value1" /> text1</label><br /> +<label><input type="radio" name="test" value="value2" checked="checked" /> text2</label> +EOD; + $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems(), array( + 'separator' => "<br />\n", + 'unselect' => '0', + ))); + + $expected = <<<EOD +0<label>text1 <input type="radio" name="test" value="value1" /></label> +1<label>text2 <input type="radio" name="test" value="value2" checked="checked" /></label> +EOD; + $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems(), array( + 'item' => function ($index, $label, $name, $checked, $value) { + return $index . Html::label($label . ' ' . Html::radio($name, $checked, $value)); + } + ))); + } + + public function testRenderOptions() + { + $data = array( + 'value1' => 'label1', + 'group1' => array( + 'value11' => 'label11', + 'group11' => array( + 'value111' => 'label111', + ), + 'group12' => array(), + ), + 'value2' => 'label2', + 'group2' => array(), + ); + $expected = <<<EOD +<option value="">please select<></option> +<option value="value1" selected="selected">label1</option> +<optgroup label="group1"> +<option value="value11">label11</option> +<optgroup label="group11"> +<option class="option" value="value111" selected="selected">label111</option> +</optgroup> +<optgroup class="group" label="group12"> + +</optgroup> +</optgroup> +<option value="value2">label2</option> +<optgroup label="group2"> + +</optgroup> +EOD; + $attributes = array( + 'prompt' => 'please select<>', + 'options' => array( + 'value111' => array('class' => 'option'), + ), + 'groups' => array( + 'group12' => array('class' => 'group'), + ), + ); + $this->assertEquals($expected, Html::renderSelectOptions(array('value111', 'value1'), $data, $attributes)); + } + + public function testRenderAttributes() + { + $this->assertEquals('', Html::renderTagAttributes(array())); + $this->assertEquals(' name="test" value="1<>"', Html::renderTagAttributes(array('name' => 'test', 'empty' => null, 'value' => '1<>'))); + Html::$showBooleanAttributeValues = false; + $this->assertEquals(' checked disabled', Html::renderTagAttributes(array('checked' => 'checked', 'disabled' => true, 'hidden' => false))); + Html::$showBooleanAttributeValues = true; + } + + protected function getDataItems() + { + return array( + 'value1' => 'text1', + 'value2' => 'text2', + ); + } + + protected function getDataItems2() + { + return array( + 'value1<>' => 'text1<>', + 'value 2' => 'text 2', + ); + } +} diff --git a/tests/unit/framework/helpers/StringHelperTest.php b/tests/unit/framework/helpers/StringHelperTest.php new file mode 100644 index 0000000..4e1266f --- /dev/null +++ b/tests/unit/framework/helpers/StringHelperTest.php @@ -0,0 +1,73 @@ +<?php +namespace yiiunit\framework\helpers; +use \yii\helpers\StringHelper as StringHelper; + +/** + * StringHelperTest + */ +class StringHelperTest extends \yii\test\TestCase +{ + public function testStrlen() + { + $this->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/util/ArrayHelperTest.php b/tests/unit/framework/util/ArrayHelperTest.php deleted file mode 100644 index 117c702..0000000 --- a/tests/unit/framework/util/ArrayHelperTest.php +++ /dev/null @@ -1,50 +0,0 @@ -<?php - -namespace yiiunit\framework\util; - -use yii\helpers\ArrayHelper; - -class ArrayHelperTest extends \yii\test\TestCase -{ - public function testMerge() - { - - - } - - public function testMultisort() - { - // single key - $array = array( - array('name' => 'b', 'age' => 3), - array('name' => 'a', 'age' => 1), - array('name' => 'c', 'age' => 2), - ); - ArrayHelper::multisort($array, 'name'); - $this->assertEquals(array('name' => 'a', 'age' => 1), $array[0]); - $this->assertEquals(array('name' => 'b', 'age' => 3), $array[1]); - $this->assertEquals(array('name' => 'c', 'age' => 2), $array[2]); - - // multiple keys - $array = array( - array('name' => 'b', 'age' => 3), - array('name' => 'a', 'age' => 2), - array('name' => 'a', 'age' => 1), - ); - ArrayHelper::multisort($array, array('name', 'age')); - $this->assertEquals(array('name' => 'a', 'age' => 1), $array[0]); - $this->assertEquals(array('name' => 'a', 'age' => 2), $array[1]); - $this->assertEquals(array('name' => 'b', 'age' => 3), $array[2]); - - // case-insensitive - $array = array( - array('name' => 'a', 'age' => 3), - array('name' => 'b', 'age' => 2), - array('name' => 'A', 'age' => 1), - ); - ArrayHelper::multisort($array, array('name', 'age'), SORT_ASC, array(SORT_STRING|SORT_FLAG_CASE, SORT_REGULAR)); - $this->assertEquals(array('name' => 'A', 'age' => 1), $array[0]); - $this->assertEquals(array('name' => 'a', 'age' => 3), $array[1]); - $this->assertEquals(array('name' => 'b', 'age' => 2), $array[2]); - } -} diff --git a/tests/unit/framework/util/HtmlTest.php b/tests/unit/framework/util/HtmlTest.php deleted file mode 100644 index eba1a20..0000000 --- a/tests/unit/framework/util/HtmlTest.php +++ /dev/null @@ -1,448 +0,0 @@ -<?php - -namespace yiiunit\framework\util; - -use Yii; -use yii\helpers\Html; -use yii\web\Application; - -class HtmlTest extends \yii\test\TestCase -{ - public function setUp() - { - new Application('test', '@yiiunit/runtime', array( - 'components' => array( - 'request' => array( - 'class' => 'yii\web\Request', - 'url' => '/test', - ), - ), - )); - } - - public function tearDown() - { - Yii::$app = null; - } - - public function testEncode() - { - $this->assertEquals("a<>&"'", Html::encode("a<>&\"'")); - } - - public function testDecode() - { - $this->assertEquals("a<>&\"'", Html::decode("a<>&"'")); - } - - public function testTag() - { - $this->assertEquals('<br />', Html::tag('br')); - $this->assertEquals('<span></span>', Html::tag('span')); - $this->assertEquals('<div>content</div>', Html::tag('div', 'content')); - $this->assertEquals('<input type="text" name="test" value="<>" />', Html::tag('input', '', array('type' => 'text', 'name' => 'test', 'value' => '<>'))); - - Html::$closeVoidElements = false; - - $this->assertEquals('<br>', Html::tag('br')); - $this->assertEquals('<span></span>', Html::tag('span')); - $this->assertEquals('<div>content</div>', Html::tag('div', 'content')); - $this->assertEquals('<input type="text" name="test" value="<>">', Html::tag('input', '', array('type' => 'text', 'name' => 'test', 'value' => '<>'))); - - Html::$closeVoidElements = true; - - $this->assertEquals('<span disabled="disabled"></span>', Html::tag('span', '', array('disabled' => true))); - Html::$showBooleanAttributeValues = false; - $this->assertEquals('<span disabled></span>', Html::tag('span', '', array('disabled' => true))); - Html::$showBooleanAttributeValues = true; - } - - public function testBeginTag() - { - $this->assertEquals('<br>', Html::beginTag('br')); - $this->assertEquals('<span id="test" class="title">', Html::beginTag('span', array('id' => 'test', 'class' => 'title'))); - } - - public function testEndTag() - { - $this->assertEquals('</br>', Html::endTag('br')); - $this->assertEquals('</span>', Html::endTag('span')); - } - - public function testCdata() - { - $data = 'test<>'; - $this->assertEquals('<![CDATA[' . $data . ']]>', Html::cdata($data)); - } - - public function testStyle() - { - $content = 'a <>'; - $this->assertEquals("<style type=\"text/css\">/*<![CDATA[*/\n{$content}\n/*]]>*/</style>", Html::style($content)); - $this->assertEquals("<style type=\"text/less\">/*<![CDATA[*/\n{$content}\n/*]]>*/</style>", Html::style($content, array('type' => 'text/less'))); - } - - public function testScript() - { - $content = 'a <>'; - $this->assertEquals("<script type=\"text/javascript\">/*<![CDATA[*/\n{$content}\n/*]]>*/</script>", Html::script($content)); - $this->assertEquals("<script type=\"text/js\">/*<![CDATA[*/\n{$content}\n/*]]>*/</script>", Html::script($content, array('type' => 'text/js'))); - } - - public function testCssFile() - { - $this->assertEquals('<link type="text/css" href="http://example.com" rel="stylesheet" />', Html::cssFile('http://example.com')); - $this->assertEquals('<link type="text/css" href="/test" rel="stylesheet" />', Html::cssFile('')); - } - - public function testJsFile() - { - $this->assertEquals('<script type="text/javascript" src="http://example.com"></script>', Html::jsFile('http://example.com')); - $this->assertEquals('<script type="text/javascript" src="/test"></script>', Html::jsFile('')); - } - - public function testBeginForm() - { - $this->assertEquals('<form action="/test" method="post">', Html::beginForm()); - $this->assertEquals('<form action="/example" method="get">', Html::beginForm('/example', 'get')); - $hiddens = array( - '<input type="hidden" name="id" value="1" />', - '<input type="hidden" name="title" value="<" />', - ); - $this->assertEquals('<form action="/example" method="get">' . "\n" . implode("\n", $hiddens), Html::beginForm('/example?id=1&title=%3C', 'get')); - } - - public function testEndForm() - { - $this->assertEquals('</form>', Html::endForm()); - } - - public function testA() - { - $this->assertEquals('<a>something<></a>', Html::a('something<>')); - $this->assertEquals('<a href="/example">something</a>', Html::a('something', '/example')); - $this->assertEquals('<a href="/test">something</a>', Html::a('something', '')); - } - - public function testMailto() - { - $this->assertEquals('<a href="mailto:test<>">test<></a>', Html::mailto('test<>')); - $this->assertEquals('<a href="mailto:test>">test<></a>', Html::mailto('test<>', 'test>')); - } - - public function testImg() - { - $this->assertEquals('<img src="/example" alt="" />', Html::img('/example')); - $this->assertEquals('<img src="/test" alt="" />', Html::img('')); - $this->assertEquals('<img src="/example" width="10" alt="something" />', Html::img('/example', array('alt' => 'something', 'width' => 10))); - } - - public function testLabel() - { - $this->assertEquals('<label>something<></label>', Html::label('something<>')); - $this->assertEquals('<label for="a">something<></label>', Html::label('something<>', 'a')); - $this->assertEquals('<label class="test" for="a">something<></label>', Html::label('something<>', 'a', array('class' => 'test'))); - } - - public function testButton() - { - $this->assertEquals('<button type="button">Button</button>', Html::button()); - $this->assertEquals('<button type="button" name="test" value="value">content<></button>', Html::button('test', 'value', 'content<>')); - $this->assertEquals('<button type="submit" class="t" name="test" value="value">content<></button>', Html::button('test', 'value', 'content<>', array('type' => 'submit', 'class' => "t"))); - } - - public function testSubmitButton() - { - $this->assertEquals('<button type="submit">Submit</button>', Html::submitButton()); - $this->assertEquals('<button type="submit" class="t" name="test" value="value">content<></button>', Html::submitButton('test', 'value', 'content<>', array('class' => 't'))); - } - - public function testResetButton() - { - $this->assertEquals('<button type="reset">Reset</button>', Html::resetButton()); - $this->assertEquals('<button type="reset" class="t" name="test" value="value">content<></button>', Html::resetButton('test', 'value', 'content<>', array('class' => 't'))); - } - - public function testInput() - { - $this->assertEquals('<input type="text" />', Html::input('text')); - $this->assertEquals('<input type="text" class="t" name="test" value="value" />', Html::input('text', 'test', 'value', array('class' => 't'))); - } - - public function testButtonInput() - { - $this->assertEquals('<input type="button" name="test" value="Button" />', Html::buttonInput('test')); - $this->assertEquals('<input type="button" class="a" name="test" value="text" />', Html::buttonInput('test', 'text', array('class' => 'a'))); - } - - public function testSubmitInput() - { - $this->assertEquals('<input type="submit" value="Submit" />', Html::submitInput()); - $this->assertEquals('<input type="submit" class="a" name="test" value="text" />', Html::submitInput('test', 'text', array('class' => 'a'))); - } - - public function testResetInput() - { - $this->assertEquals('<input type="reset" value="Reset" />', Html::resetInput()); - $this->assertEquals('<input type="reset" class="a" name="test" value="text" />', Html::resetInput('test', 'text', array('class' => 'a'))); - } - - public function testTextInput() - { - $this->assertEquals('<input type="text" name="test" />', Html::textInput('test')); - $this->assertEquals('<input type="text" class="t" name="test" value="value" />', Html::textInput('test', 'value', array('class' => 't'))); - } - - public function testHiddenInput() - { - $this->assertEquals('<input type="hidden" name="test" />', Html::hiddenInput('test')); - $this->assertEquals('<input type="hidden" class="t" name="test" value="value" />', Html::hiddenInput('test', 'value', array('class' => 't'))); - } - - public function testPasswordInput() - { - $this->assertEquals('<input type="password" name="test" />', Html::passwordInput('test')); - $this->assertEquals('<input type="password" class="t" name="test" value="value" />', Html::passwordInput('test', 'value', array('class' => 't'))); - } - - public function testFileInput() - { - $this->assertEquals('<input type="file" name="test" />', Html::fileInput('test')); - $this->assertEquals('<input type="file" class="t" name="test" value="value" />', Html::fileInput('test', 'value', array('class' => 't'))); - } - - public function testTextarea() - { - $this->assertEquals('<textarea name="test"></textarea>', Html::textarea('test')); - $this->assertEquals('<textarea class="t" name="test">value<></textarea>', Html::textarea('test', 'value<>', array('class' => 't'))); - } - - public function testRadio() - { - $this->assertEquals('<input type="radio" name="test" value="1" />', Html::radio('test')); - $this->assertEquals('<input type="radio" class="a" name="test" checked="checked" />', Html::radio('test', true, null, array('class' => 'a'))); - $this->assertEquals('<input type="hidden" name="test" value="0" /><input type="radio" class="a" name="test" value="2" checked="checked" />', Html::radio('test', true, 2, array('class' => 'a' , 'uncheck' => '0'))); - } - - public function testCheckbox() - { - $this->assertEquals('<input type="checkbox" name="test" value="1" />', Html::checkbox('test')); - $this->assertEquals('<input type="checkbox" class="a" name="test" checked="checked" />', Html::checkbox('test', true, null, array('class' => 'a'))); - $this->assertEquals('<input type="hidden" name="test" value="0" /><input type="checkbox" class="a" name="test" value="2" checked="checked" />', Html::checkbox('test', true, 2, array('class' => 'a', 'uncheck' => '0'))); - } - - public function testDropDownList() - { - $expected = <<<EOD -<select name="test"> - -</select> -EOD; - $this->assertEquals($expected, Html::dropDownList('test')); - $expected = <<<EOD -<select name="test"> -<option value="value1">text1</option> -<option value="value2">text2</option> -</select> -EOD; - $this->assertEquals($expected, Html::dropDownList('test', null, $this->getDataItems())); - $expected = <<<EOD -<select name="test"> -<option value="value1">text1</option> -<option value="value2" selected="selected">text2</option> -</select> -EOD; - $this->assertEquals($expected, Html::dropDownList('test', 'value2', $this->getDataItems())); - } - - public function testListBox() - { - $expected = <<<EOD -<select name="test" size="4"> - -</select> -EOD; - $this->assertEquals($expected, Html::listBox('test')); - $expected = <<<EOD -<select name="test" size="5"> -<option value="value1">text1</option> -<option value="value2">text2</option> -</select> -EOD; - $this->assertEquals($expected, Html::listBox('test', null, $this->getDataItems(), array('size' => 5))); - $expected = <<<EOD -<select name="test" size="4"> -<option value="value1<>">text1<></option> -<option value="value 2">text  2</option> -</select> -EOD; - $this->assertEquals($expected, Html::listBox('test', null, $this->getDataItems2())); - $expected = <<<EOD -<select name="test" size="4"> -<option value="value1">text1</option> -<option value="value2" selected="selected">text2</option> -</select> -EOD; - $this->assertEquals($expected, Html::listBox('test', 'value2', $this->getDataItems())); - $expected = <<<EOD -<select name="test" size="4"> -<option value="value1" selected="selected">text1</option> -<option value="value2" selected="selected">text2</option> -</select> -EOD; - $this->assertEquals($expected, Html::listBox('test', array('value1', 'value2'), $this->getDataItems())); - - $expected = <<<EOD -<select name="test[]" multiple="multiple" size="4"> - -</select> -EOD; - $this->assertEquals($expected, Html::listBox('test', null, array(), array('multiple' => true))); - $expected = <<<EOD -<input type="hidden" name="test" value="0" /><select name="test" size="4"> - -</select> -EOD; - $this->assertEquals($expected, Html::listBox('test', '', array(), array('unselect' => '0'))); - } - - public function testCheckboxList() - { - $this->assertEquals('', Html::checkboxList('test')); - - $expected = <<<EOD -<label><input type="checkbox" name="test[]" value="value1" /> text1</label> -<label><input type="checkbox" name="test[]" value="value2" checked="checked" /> text2</label> -EOD; - $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems())); - - $expected = <<<EOD -<label><input type="checkbox" name="test[]" value="value1<>" /> text1<></label> -<label><input type="checkbox" name="test[]" value="value 2" /> text 2</label> -EOD; - $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems2())); - - $expected = <<<EOD -<input type="hidden" name="test" value="0" /><label><input type="checkbox" name="test[]" value="value1" /> text1</label><br /> -<label><input type="checkbox" name="test[]" value="value2" checked="checked" /> text2</label> -EOD; - $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems(), array( - 'separator' => "<br />\n", - 'unselect' => '0', - ))); - - $expected = <<<EOD -0<label>text1 <input type="checkbox" name="test[]" value="value1" /></label> -1<label>text2 <input type="checkbox" name="test[]" value="value2" checked="checked" /></label> -EOD; - $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems(), array( - 'item' => function ($index, $label, $name, $checked, $value) { - return $index . Html::label($label . ' ' . Html::checkbox($name, $checked, $value)); - } - ))); - } - - public function testRadioList() - { - $this->assertEquals('', Html::radioList('test')); - - $expected = <<<EOD -<label><input type="radio" name="test" value="value1" /> text1</label> -<label><input type="radio" name="test" value="value2" checked="checked" /> text2</label> -EOD; - $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems())); - - $expected = <<<EOD -<label><input type="radio" name="test" value="value1<>" /> text1<></label> -<label><input type="radio" name="test" value="value 2" /> text 2</label> -EOD; - $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems2())); - - $expected = <<<EOD -<input type="hidden" name="test" value="0" /><label><input type="radio" name="test" value="value1" /> text1</label><br /> -<label><input type="radio" name="test" value="value2" checked="checked" /> text2</label> -EOD; - $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems(), array( - 'separator' => "<br />\n", - 'unselect' => '0', - ))); - - $expected = <<<EOD -0<label>text1 <input type="radio" name="test" value="value1" /></label> -1<label>text2 <input type="radio" name="test" value="value2" checked="checked" /></label> -EOD; - $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems(), array( - 'item' => function ($index, $label, $name, $checked, $value) { - return $index . Html::label($label . ' ' . Html::radio($name, $checked, $value)); - } - ))); - } - - public function testRenderOptions() - { - $data = array( - 'value1' => 'label1', - 'group1' => array( - 'value11' => 'label11', - 'group11' => array( - 'value111' => 'label111', - ), - 'group12' => array(), - ), - 'value2' => 'label2', - 'group2' => array(), - ); - $expected = <<<EOD -<option value="">please select<></option> -<option value="value1" selected="selected">label1</option> -<optgroup label="group1"> -<option value="value11">label11</option> -<optgroup label="group11"> -<option class="option" value="value111" selected="selected">label111</option> -</optgroup> -<optgroup class="group" label="group12"> - -</optgroup> -</optgroup> -<option value="value2">label2</option> -<optgroup label="group2"> - -</optgroup> -EOD; - $attributes = array( - 'prompt' => 'please select<>', - 'options' => array( - 'value111' => array('class' => 'option'), - ), - 'groups' => array( - 'group12' => array('class' => 'group'), - ), - ); - $this->assertEquals($expected, Html::renderSelectOptions(array('value111', 'value1'), $data, $attributes)); - } - - public function testRenderAttributes() - { - $this->assertEquals('', Html::renderTagAttributes(array())); - $this->assertEquals(' name="test" value="1<>"', Html::renderTagAttributes(array('name' => 'test', 'empty' => null, 'value' => '1<>'))); - Html::$showBooleanAttributeValues = false; - $this->assertEquals(' checked disabled', Html::renderTagAttributes(array('checked' => 'checked', 'disabled' => true, 'hidden' => false))); - Html::$showBooleanAttributeValues = true; - } - - protected function getDataItems() - { - return array( - 'value1' => 'text1', - 'value2' => 'text2', - ); - } - - protected function getDataItems2() - { - return array( - 'value1<>' => 'text1<>', - 'value 2' => 'text 2', - ); - } -} From f69a73baf2bf377fbba09268335aec8228c35413 Mon Sep 17 00:00:00 2001 From: Qiang Xue <qiang.xue@gmail.com> Date: Fri, 29 Mar 2013 13:31:08 -0400 Subject: [PATCH 25/41] refactored User and Identity classes. --- framework/web/Identity.php | 36 +++++++++++++- framework/web/User.php | 115 ++++++++++++++++++++++++--------------------- 2 files changed, 96 insertions(+), 55 deletions(-) diff --git a/framework/web/Identity.php b/framework/web/Identity.php index 89d4282..6d67bc0 100644 --- a/framework/web/Identity.php +++ b/framework/web/Identity.php @@ -8,6 +8,35 @@ namespace yii\web; /** + * Identity is the interface that should be implemented by a class providing identity information. + * + * This interface can typically be implemented by a user model class. For example, the following + * code shows how to implement this interface by a User ActiveRecord class: + * + * ~~~ + * class User extends ActiveRecord implements Identity + * { + * public static function findIdentity($id) + * { + * return static::find($id); + * } + * + * public function getId() + * { + * return $this->id; + * } + * + * public function getAuthKey() + * { + * return $this->authKey; + * } + * + * public function validateAuthKey($authKey) + * { + * return $this->authKey === $authKey; + * } + * } + * ~~~ * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 @@ -29,8 +58,11 @@ interface Identity public function getId(); /** * Returns a key that can be used to check the validity of a given identity ID. - * The space of such keys should be big and random enough to defeat potential identity attacks. - * The returned key can be a string, an integer, or any serializable data. + * + * 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. diff --git a/framework/web/User.php b/framework/web/User.php index 5b5f977..4dc2607 100644 --- a/framework/web/User.php +++ b/framework/web/User.php @@ -13,7 +13,7 @@ use yii\base\HttpException; use yii\base\InvalidConfigException; /** - * User is an application component that manages the user authentication status. + * User is the class for the "user" application component that manages the user authentication status. * * 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. @@ -32,15 +32,6 @@ class User extends Component const EVENT_AFTER_LOGOUT = 'afterLogout'; /** - * @var Identity the identity object associated with the currently logged user. - * This property is set automatically be the User component. Do not modify it directly - * unless you understand the consequence. You should normally use [[login()]], [[logout()]], - * or [[switchIdentity()]] to update the identity associated with the current user. - * - * If this property is null, it means the current user is a guest (not authenticated). - */ - public $identity; - /** * @var string the class name of the [[identity]] object. */ public $identityClass; @@ -65,7 +56,7 @@ class User extends Component * @var array the configuration of the identity cookie. This property is used only when [[enableAutoLogin]] is true. * @see Cookie */ - public $identityCookie = array('name' => '__identity'); + public $identityCookie = array('name' => '__identity', 'httponly' => true); /** * @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 @@ -112,8 +103,6 @@ class User extends Component Yii::$app->getSession()->open(); - $this->loadIdentity(); - $this->renewAuthStatus(); if ($this->enableAutoLogin) { @@ -125,19 +114,43 @@ class User extends Component } } + private $_identity = false; + /** - * Loads the [[identity]] object according to [[id]]. + * 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 */ - protected function loadIdentity() + public function getIdentity() { - $id = $this->getId(); - if ($id === null) { - $this->identity = null; - } else { - /** @var $class Identity */ - $class = $this->identityClass; - $this->identity = $class::findIdentity($id); + 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; + } + + /** + * 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 setIdentity($identity) + { + $this->_identity = $identity; } /** @@ -157,10 +170,7 @@ class User extends Component public function login($identity, $duration = 0) { if ($this->beforeLogin($identity, false)) { - $this->switchIdentity($identity); - if ($duration > 0 && $this->enableAutoLogin) { - $this->sendIdentityCookie($identity, $duration); - } + $this->switchIdentity($identity, $duration); $this->afterLogin($identity, false); } return !$this->getIsGuest(); @@ -185,10 +195,7 @@ class User extends Component $identity = $class::findIdentity($id); if ($identity !== null && $identity->validateAuthKey($authKey)) { if ($this->beforeLogin($identity, true)) { - $this->switchIdentity($identity); - if ($this->autoRenewCookie) { - $this->sendIdentityCookie($identity, $duration); - } + $this->switchIdentity($identity, $this->autoRenewCookie ? $duration : 0); $this->afterLogin($identity, true); } } elseif ($identity !== null) { @@ -206,12 +213,9 @@ class User extends Component */ public function logout($destroySession = true) { - $identity = $this->identity; + $identity = $this->getIdentity(); if ($identity !== null && $this->beforeLogout($identity)) { $this->switchIdentity(null); - if ($this->enableAutoLogin) { - Yii::$app->getResponse()->getCookies()->remove(new Cookie($this->identityCookie)); - } if ($destroySession) { Yii::$app->getSession()->destroy(); } @@ -225,7 +229,7 @@ class User extends Component */ public function getIsGuest() { - return $this->identity === null; + return $this->getIdentity() === null; } /** @@ -238,14 +242,6 @@ class User extends Component } /** - * @param string|integer $value the unique identifier for the user. If null, it means the user is a guest. - */ - public function setId($value) - { - Yii::$app->getSession()->set($this->idVar, $value); - } - - /** * 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. @@ -400,24 +396,37 @@ class User extends Component } /** - * Changes the current user with the specified identity information. - * This method is called by [[login()]] and [[loginByCookie()]] - * when the current user needs to be associated with the corresponding - * identity information. + * Switches to a new identity for the current user. + * + * 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. + * + * This method is mainly called by [[login()]], [[logout()]] and [[loginByCookie()]] + * when the current user needs to be associated with the corresponding identity information. + * * @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. */ - protected function switchIdentity($identity) + public function switchIdentity($identity, $duration = 0) { - Yii::$app->getSession()->regenerateID(true); - $this->identity = $identity; $session = Yii::$app->getSession(); + $session->regenerateID(true); + $this->setIdentity($identity); $session->remove($this->idVar); $session->remove($this->authTimeoutVar); if ($identity instanceof Identity) { - $this->setId($identity->getId()); + $session->set($this->idVar, $identity->getId()); if ($this->authTimeout !== null) { - Yii::$app->getSession()->set($this->authTimeoutVar, time() + $this->authTimeout); + $session->set($this->authTimeoutVar, time() + $this->authTimeout); + } + if ($duration > 0 && $this->enableAutoLogin) { + $this->sendIdentityCookie($identity, $duration); } + } elseif ($this->enableAutoLogin) { + Yii::$app->getResponse()->getCookies()->remove(new Cookie($this->identityCookie)); } } @@ -429,7 +438,7 @@ class User extends Component */ protected function renewAuthStatus() { - if ($this->authTimeout !== null && $this->identity !== null) { + if ($this->authTimeout !== null && !$this->getIsGuest()) { $expire = Yii::$app->getSession()->get($this->authTimeoutVar); if ($expire !== null && $expire < time()) { $this->logout(false); From 92e634db66272b6151392dbef74c1a40baa8437c Mon Sep 17 00:00:00 2001 From: Alexander Makarov <sam@rmcreative.ru> Date: Fri, 29 Mar 2013 22:54:45 +0400 Subject: [PATCH 26/41] Ability to configure session cookie, httponly by default --- framework/web/Session.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/framework/web/Session.php b/framework/web/Session.php index 3e0f599..4c0505f 100644 --- a/framework/web/Session.php +++ b/framework/web/Session.php @@ -60,6 +60,13 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co public $flashVar = '__flash'; /** + * @var array parameter-value pairs to override default session cookie parameters + */ + public $cookieParams = array( + 'httponly' => true + ); + + /** * Initializes the application component. * This method is required by IApplicationComponent and is invoked by application. */ @@ -111,6 +118,8 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co ); } + $this->setCookieParams($this->cookieParams); + @session_start(); if (session_id() == '') { From 7d27d65a136ec85fe8d7331444fd35fcd0f1460e Mon Sep 17 00:00:00 2001 From: Qiang Xue <qiang.xue@gmail.com> Date: Fri, 29 Mar 2013 15:04:15 -0400 Subject: [PATCH 27/41] bug fix. --- framework/YiiBase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/YiiBase.php b/framework/YiiBase.php index 16e237d..261a99e 100644 --- a/framework/YiiBase.php +++ b/framework/YiiBase.php @@ -238,7 +238,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); From 6a2bfae41c32f330facf8df516423e450b2003fc Mon Sep 17 00:00:00 2001 From: Qiang Xue <qiang.xue@gmail.com> Date: Fri, 29 Mar 2013 23:32:34 -0400 Subject: [PATCH 28/41] use parameter binding for query builder. --- framework/db/Command.php | 2 +- framework/db/Connection.php | 14 +- framework/db/QueryBuilder.php | 469 ++++++++++++++++++++++-------------------- 3 files changed, 251 insertions(+), 234 deletions(-) diff --git a/framework/db/Command.php b/framework/db/Command.php index a30aa14..2a13a89 100644 --- a/framework/db/Command.php +++ b/framework/db/Command.php @@ -543,7 +543,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 e84970b..1be43eb 100644 --- a/framework/db/Connection.php +++ b/framework/db/Connection.php @@ -248,15 +248,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 diff --git a/framework/db/QueryBuilder.php b/framework/db/QueryBuilder.php index 75375cc..62ef58f 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,208 @@ 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"; + } 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'; + } 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); + } } From 597082a11aa49b59826b2300fd90122bba1cf947 Mon Sep 17 00:00:00 2001 From: Qiang Xue <qiang.xue@gmail.com> Date: Sat, 30 Mar 2013 18:28:54 -0400 Subject: [PATCH 29/41] Automatic table and column name quoting. --- framework/caching/DbCache.php | 6 +++--- framework/db/Command.php | 25 ++-------------------- framework/db/Connection.php | 34 +++++++++++++++++++----------- framework/db/Schema.php | 10 ++++----- framework/logging/DbTarget.php | 3 ++- framework/web/DbSession.php | 4 ++-- tests/unit/framework/db/ConnectionTest.php | 1 - 7 files changed, 36 insertions(+), 47 deletions(-) diff --git a/framework/caching/DbCache.php b/framework/caching/DbCache.php index 3952852..dee8c7a 100644 --- a/framework/caching/DbCache.php +++ b/framework/caching/DbCache.php @@ -99,7 +99,7 @@ class DbCache extends Cache $query = new Query; $query->select(array('data')) ->from($this->cacheTable) - ->where('id = :id AND (expire = 0 OR expire >' . time() . ')', array(':id' => $key)); + ->where('[[id]] = :id AND ([[expire]] = 0 OR [[expire]] >' . time() . ')', array(':id' => $key)); if ($this->db->enableQueryCache) { // temporarily disable and re-enable query caching $this->db->enableQueryCache = false; @@ -125,7 +125,7 @@ class DbCache extends Cache $query->select(array('id', 'data')) ->from($this->cacheTable) ->where(array('id' => $keys)) - ->andWhere('(expire = 0 OR expire > ' . time() . ')'); + ->andWhere('([[expire]] = 0 OR [[expire]] > ' . time() . ')'); if ($this->db->enableQueryCache) { $this->db->enableQueryCache = false; @@ -227,7 +227,7 @@ class DbCache extends Cache { if ($force || mt_rand(0, 1000000) < $this->gcProbability) { $this->db->createCommand() - ->delete($this->cacheTable, 'expire > 0 AND expire < ' . time()) + ->delete($this->cacheTable, '[[expire]] > 0 AND [[expire]] < ' . time()) ->execute(); } } diff --git a/framework/db/Command.php b/framework/db/Command.php index 2a13a89..73a8346 100644 --- a/framework/db/Command.php +++ b/framework/db/Command.php @@ -84,42 +84,21 @@ 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 - */ - protected function expandSql($sql) - { - $db = $this->db; - return preg_replace_callback('/(\\{\\{(.*?)\\}\\}|\\[\\[(.*?)\\]\\])/', function($matches) use($db) { - if (isset($matches[3])) { - return $db->quoteColumnName($matches[3]); - } else { - $name = str_replace('%', $db->tablePrefix, $matches[2]); - return $db->quoteTableName($name); - } - }, $sql); - } - - /** * Prepares the SQL statement to be executed. * For complex SQL statement that is to be executed multiple times, * this may improve performance. diff --git a/framework/db/Connection.php b/framework/db/Connection.php index 1be43eb..695034a 100644 --- a/framework/db/Connection.php +++ b/framework/db/Connection.php @@ -223,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 @@ -518,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/Schema.php b/framework/db/Schema.php index 71bc9a2..d8d89bc 100644 --- a/framework/db/Schema.php +++ b/framework/db/Schema.php @@ -248,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 @@ -256,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) { @@ -273,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 @@ -320,13 +320,13 @@ abstract class Schema extends \yii\base\Object /** * Returns the real name of a 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) { - 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/logging/DbTarget.php b/framework/logging/DbTarget.php index e4e30ce..ce9d843 100644 --- a/framework/logging/DbTarget.php +++ b/framework/logging/DbTarget.php @@ -78,7 +78,8 @@ class DbTarget extends Target public function export($messages) { $tableName = $this->db->quoteTableName($this->logTable); - $sql = "INSERT INTO $tableName (level, category, log_time, message) VALUES (:level, :category, :log_time, :message)"; + $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( diff --git a/framework/web/DbSession.php b/framework/web/DbSession.php index d3afc76..2910b40 100644 --- a/framework/web/DbSession.php +++ b/framework/web/DbSession.php @@ -144,7 +144,7 @@ class DbSession extends Session $query = new Query; $data = $query->select(array('data')) ->from($this->sessionTable) - ->where('expire>:expire AND id=:id', array(':expire' => time(), ':id' => $id)) + ->where('[[expire]]>:expire AND [[id]]=:id', array(':expire' => time(), ':id' => $id)) ->createCommand($this->db) ->queryScalar(); return $data === false ? '' : $data; @@ -214,7 +214,7 @@ class DbSession extends Session public function gcSession($maxLifetime) { $this->db->createCommand() - ->delete($this->sessionTable, 'expire<:expire', array(':expire' => time())) + ->delete($this->sessionTable, '[[expire]]<:expire', array(':expire' => time())) ->execute(); return true; } 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)')); } From b0cc86dfcd08176b1df0267531f50a6dcdf874cb Mon Sep 17 00:00:00 2001 From: Qiang Xue <qiang.xue@gmail.com> Date: Sat, 30 Mar 2013 21:39:31 -0400 Subject: [PATCH 30/41] added Command::getRawSql() --- framework/db/Command.php | 57 ++++++++++++++++++++++++++++++++++-------------- framework/db/Schema.php | 6 ++--- 2 files changed, 44 insertions(+), 19 deletions(-) diff --git a/framework/db/Command.php b/framework/db/Command.php index 73a8346..dc6c972 100644 --- a/framework/db/Command.php +++ b/framework/db/Command.php @@ -99,6 +99,39 @@ class Command extends \yii\base\Component } /** + * 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 + */ + public function getRawSql() + { + 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 { + return strtr($this->_sql, $params); + } + } + } + + /** * Prepares the SQL statement to be executed. * For complex SQL statement that is to be executed multiple times, * this may improve performance. @@ -222,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); @@ -239,13 +273,9 @@ 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}", __METHOD__); + Yii::trace("Executing SQL: $rawSql", __METHOD__); if ($sql == '') { return 0; @@ -271,7 +301,7 @@ class Command extends \yii\base\Component } $message = $e->getMessage(); - Yii::error("$message\nFailed to execute SQL: {$sql}{$paramLog}", __METHOD__); + Yii::error("$message\nFailed to execute SQL: $rawSql", __METHOD__); $errorInfo = $e instanceof \PDOException ? $e->errorInfo : null; throw new Exception($message, $errorInfo, (int)$e->getCode()); @@ -357,13 +387,9 @@ 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}", __METHOD__); + Yii::trace("Querying SQL: $rawSql", __METHOD__); /** @var $cache \yii\caching\Cache */ if ($db->enableQueryCache && $method !== '') { @@ -375,8 +401,7 @@ class Command extends \yii\base\Component __CLASS__, $db->dsn, $db->username, - $sql, - $paramLog, + $rawSql, )); if (($result = $cache->get($cacheKey)) !== false) { Yii::trace('Query result served from cache', __METHOD__); @@ -418,7 +443,7 @@ class Command extends \yii\base\Component Yii::endProfile($token, __METHOD__); } $message = $e->getMessage(); - Yii::error("$message\nCommand::$method() failed: {$sql}{$paramLog}", __METHOD__); + Yii::error("$message\nCommand::$method() failed: $rawSql", __METHOD__); $errorInfo = $e instanceof \PDOException ? $e->errorInfo : null; throw new Exception($message, $errorInfo, (int)$e->getCode()); } diff --git a/framework/db/Schema.php b/framework/db/Schema.php index d8d89bc..9538e4c 100644 --- a/framework/db/Schema.php +++ b/framework/db/Schema.php @@ -83,7 +83,7 @@ abstract class Schema extends \yii\base\Object } $db = $this->db; - $realName = $this->getRealTableName($name); + $realName = $this->getRawTableName($name); if ($db->enableSchemaCache && !in_array($name, $db->schemaCacheExclude, true)) { /** @var $cache Cache */ @@ -318,13 +318,13 @@ 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 '%' 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 (strpos($name, '{{') !== false) { $name = preg_replace('/\\{\\{(.*?)\\}\\}/', '\1', $name); From c32a202d824eca55dfd86aea3b575c081bcad0cb Mon Sep 17 00:00:00 2001 From: Qiang Xue <qiang.xue@gmail.com> Date: Sun, 31 Mar 2013 20:21:29 -0400 Subject: [PATCH 31/41] refactored MVC. --- framework/base/Controller.php | 119 +++++++++++++++++++++++++-------- framework/base/View.php | 93 +++++++++----------------- framework/base/Widget.php | 57 +++++++++++----- framework/web/PageCache.php | 5 -- framework/widgets/Clip.php | 8 +-- framework/widgets/ContentDecorator.php | 21 ++---- framework/widgets/FragmentCache.php | 9 --- 7 files changed, 174 insertions(+), 138 deletions(-) diff --git a/framework/base/Controller.php b/framework/base/Controller.php index ff6d8f7..241c66c 100644 --- a/framework/base/Controller.php +++ b/framework/base/Controller.php @@ -47,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 @@ -135,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); } } @@ -293,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. @@ -301,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; } @@ -313,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); } /** @@ -332,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; } /** @@ -347,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/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: + * + * ~~~ + * <?php $this->beginContent('@app/view/layouts/base'); ?> + * ...layout content here... + * <?php $this->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/web/PageCache.php b/framework/web/PageCache.php index 29c8cc8..5a50825 100644 --- a/framework/web/PageCache.php +++ b/framework/web/PageCache.php @@ -25,11 +25,6 @@ class PageCache extends ActionFilter */ 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 application component ID of the [[\yii\caching\Cache|cache]] object. */ public $cache = 'cache'; 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 <qiang.xue@gmail.com> @@ -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 65bb86b..dae538a 100644 --- a/framework/widgets/FragmentCache.php +++ b/framework/widgets/FragmentCache.php @@ -64,11 +64,6 @@ class FragmentCache extends Widget */ 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. */ @@ -81,10 +76,6 @@ class FragmentCache extends Widget { parent::init(); - if ($this->view === null) { - $this->view = Yii::$app->getView(); - } - if (!$this->enabled) { $this->cache = null; } elseif (is_string($this->cache)) { From 906e402ba8823178d1ef7dd9435a030e80c2b488 Mon Sep 17 00:00:00 2001 From: Qiang Xue <qiang.xue@gmail.com> Date: Sun, 31 Mar 2013 21:20:42 -0400 Subject: [PATCH 32/41] ActiveRecord::update() and ActiveRecord::delete() now returns the number rows affected. --- framework/db/ActiveRecord.php | 38 +++++++++++++++++++++++++++++--------- framework/db/QueryBuilder.php | 10 ++++++++++ 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/framework/db/ActiveRecord.php b/framework/db/ActiveRecord.php index d8f2f65..8fd1db2 100644 --- a/framework/db/ActiveRecord.php +++ b/framework/db/ActiveRecord.php @@ -530,8 +530,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 +590,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 +696,24 @@ 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. */ public function update($runValidation = true, $attributes = null) { @@ -708,13 +725,15 @@ class ActiveRecord extends Model if ($values !== array()) { // 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, $this->getOldPrimaryKey(true)); 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 +782,18 @@ 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. */ 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)); + $rows = $this->deleteAll($this->getPrimaryKey(true)); $this->_oldAttributes = null; $this->afterDelete(); - return true; + return $rows; } else { return false; } diff --git a/framework/db/QueryBuilder.php b/framework/db/QueryBuilder.php index 62ef58f..da43940 100644 --- a/framework/db/QueryBuilder.php +++ b/framework/db/QueryBuilder.php @@ -751,6 +751,11 @@ class QueryBuilder extends \yii\base\Object } 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"; @@ -823,6 +828,11 @@ class QueryBuilder extends \yii\base\Object } 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; From 7de94d7fe904016280a8ef831e645e7ac2051a9f Mon Sep 17 00:00:00 2001 From: Qiang Xue <qiang.xue@gmail.com> Date: Sun, 31 Mar 2013 22:03:51 -0400 Subject: [PATCH 33/41] Implemented optimistic locking for AR. --- framework/db/ActiveRecord.php | 63 +++++++++++++++++++++++++++++++---- framework/db/StaleObjectException.php | 23 +++++++++++++ 2 files changed, 79 insertions(+), 7 deletions(-) create mode 100644 framework/db/StaleObjectException.php diff --git a/framework/db/ActiveRecord.php b/framework/db/ActiveRecord.php index 8fd1db2..0de0fb0 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 column name that stores the lock version of a table row. + * + * This is used to implement optimistic locking. Optimistic locking allows multiple users + * to access the same record for edits. In case when a user attempts to save the record upon + * some staled data (because another user has modified the data), a [[StaleObjectException]] + * will be thrown, and the update is ignored. + * + * Optimized locking is only supported by [[update()]] and [[delete()]]. + * + * To use optimized locking: + * + * 1. create a column to store the lock version. The column type should be integer (or bigint) + * and default to 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 static function lockVersion() + { + 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 @@ -714,6 +739,8 @@ class ActiveRecord extends Model * meaning all attributes that are loaded from DB will be saved. * @return integer|boolean the number of rows affected, or false if validation fails * or [[beforeSave()]] stops the updating process. + * @throws StaleObjectException if [[lockVersion|optimistic locking]] is enabled and the data + * being updated is outdated. */ public function update($runValidation = true, $attributes = null) { @@ -723,12 +750,24 @@ class ActiveRecord extends Model if ($this->beforeSave(false)) { $values = $this->getDirtyAttributes($attributes); if ($values !== array()) { + $condition = $this->getOldPrimaryKey(true); + $lock = $this->lockVersion(); + if ($lock !== null) { + $values[$lock] = $this->$lock + 1; + $condition[$lock] = new Expression("[[$lock]]+1"); + } // 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. - $rows = $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 { @@ -784,13 +823,23 @@ class ActiveRecord extends Model * * @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 [[lockVersion|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 - $rows = $this->deleteAll($this->getPrimaryKey(true)); + $condition = $this->getOldPrimaryKey(true); + $lock = $this->lockVersion(); + 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 $rows; 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 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\db; + +/** + * @author Qiang Xue <qiang.xue@gmail.com> + * @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 From c6a13278971c78238aec831ea9e362b6c8aa126d Mon Sep 17 00:00:00 2001 From: Qiang Xue <qiang.xue@gmail.com> Date: Sun, 31 Mar 2013 22:09:13 -0400 Subject: [PATCH 34/41] renamed lockVersion to optimisticLock --- framework/db/ActiveRecord.php | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/framework/db/ActiveRecord.php b/framework/db/ActiveRecord.php index 0de0fb0..82a3b10 100644 --- a/framework/db/ActiveRecord.php +++ b/framework/db/ActiveRecord.php @@ -277,19 +277,19 @@ class ActiveRecord extends Model } /** - * Returns the column name that stores the lock version of a table row. + * Returns the name of the column that stores the lock version for implementing optimistic locking. * - * This is used to implement optimistic locking. Optimistic locking allows multiple users - * to access the same record for edits. In case when a user attempts to save the record upon - * some staled data (because another user has modified the data), a [[StaleObjectException]] - * will be thrown, and the update is ignored. + * Optimistic locking allows multiple users to access the same record for edits. 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 ignored. * * Optimized locking is only supported by [[update()]] and [[delete()]]. * * To use optimized locking: * - * 1. create a column to store the lock version. The column type should be integer (or bigint) - * and default to 0. Override this method to return the name of this column. + * 1. create a column to store the lock version. 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]] @@ -299,7 +299,7 @@ class ActiveRecord extends Model * @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 static function lockVersion() + public function optimisticLock() { return null; } @@ -739,7 +739,7 @@ class ActiveRecord extends Model * meaning all attributes that are loaded from DB will be saved. * @return integer|boolean the number of rows affected, or false if validation fails * or [[beforeSave()]] stops the updating process. - * @throws StaleObjectException if [[lockVersion|optimistic locking]] is enabled and the data + * @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data * being updated is outdated. */ public function update($runValidation = true, $attributes = null) @@ -751,7 +751,7 @@ class ActiveRecord extends Model $values = $this->getDirtyAttributes($attributes); if ($values !== array()) { $condition = $this->getOldPrimaryKey(true); - $lock = $this->lockVersion(); + $lock = $this->optimisticLock(); if ($lock !== null) { $values[$lock] = $this->$lock + 1; $condition[$lock] = new Expression("[[$lock]]+1"); @@ -823,7 +823,7 @@ class ActiveRecord extends Model * * @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 [[lockVersion|optimistic locking]] is enabled and the data + * @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data * being deleted is outdated. */ public function delete() @@ -832,7 +832,7 @@ class ActiveRecord extends Model // 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 $condition = $this->getOldPrimaryKey(true); - $lock = $this->lockVersion(); + $lock = $this->optimisticLock(); if ($lock !== null) { $condition[$lock] = $this->$lock; } From 2026d3824bfc48caacb34d225b2046a2f7aab5a5 Mon Sep 17 00:00:00 2001 From: Qiang Xue <qiang.xue@gmail.com> Date: Mon, 1 Apr 2013 08:32:52 -0400 Subject: [PATCH 35/41] allow using existing column to store lock version. --- framework/db/ActiveRecord.php | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/framework/db/ActiveRecord.php b/framework/db/ActiveRecord.php index 82a3b10..45c53fb 100644 --- a/framework/db/ActiveRecord.php +++ b/framework/db/ActiveRecord.php @@ -279,16 +279,16 @@ 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. 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 ignored. + * 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 lock version. The column type should be `BIGINT DEFAULT 0`. + * 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. @@ -753,8 +753,10 @@ class ActiveRecord extends Model $condition = $this->getOldPrimaryKey(true); $lock = $this->optimisticLock(); if ($lock !== null) { - $values[$lock] = $this->$lock + 1; - $condition[$lock] = new Expression("[[$lock]]+1"); + 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. From 78457bf0091ca7119735516871a6c399499f6fba Mon Sep 17 00:00:00 2001 From: Qiang Xue <qiang.xue@gmail.com> Date: Mon, 1 Apr 2013 14:58:44 -0400 Subject: [PATCH 36/41] fetching plural rule data. --- build/.htaccess | 1 + build/build | 20 + build/build.bat | 23 + build/build.xml | 276 ++++++++++ build/controllers/LocaleController.php | 103 ++++ framework/YiiBase.php | 1 + framework/base/Module.php | 6 + framework/console/Controller.php | 1 - framework/console/controllers/HelpController.php | 4 +- framework/i18n/data/plurals.php | 627 +++++++++++++++++++++++ framework/i18n/data/plurals.xml | 109 ++++ 11 files changed, 1168 insertions(+), 3 deletions(-) create mode 100644 build/.htaccess create mode 100644 build/build create mode 100644 build/build.bat create mode 100644 build/build.xml create mode 100644 build/controllers/LocaleController.php create mode 100644 framework/i18n/data/plurals.php create mode 100644 framework/i18n/data/plurals.xml 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 100644 index 0000000..fff4282 --- /dev/null +++ b/build/build @@ -0,0 +1,20 @@ +#!/usr/bin/env php +<?php +/** + * build script file. + * + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +// fcgi doesn't have STDIN defined by default +defined('STDIN') or define('STDIN', fopen('php://stdin', 'r')); + +require(__DIR__ . '/../framework/yii.php'); + +$id = 'yiic-build'; +$basePath = __DIR__; + +$application = new yii\console\Application($id, $basePath); +$application->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 <qiang.xue@gmail.com> +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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Phing build file for Yii. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @link http://www.yiiframework.com/ + * @copyright 2008-2009 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ +--> +<project name="yii" basedir="." default="help"> + <!-- task definitions --> + <taskdef name="yii-init-build" classname="YiiInitTask" classpath="tasks" /> + <!-- + <taskdef name="yii-pear" classname="YiiPearTask" classpath="tasks"/> + --> + + <!-- init yii.version, yii.revision and yii.winbuild --> + <yii-init-build /> + + <!-- these are required external commands --> + <property name="php" value="php" /> <!-- PHP parser --> + <property name="hhc" value="hhc" /> <!-- compile phpdoc into CHM --> + <property name="pdflatex" value="pdflatex" /> <!-- generates PDF from LaTex --> + + <property name="pkgname" value="${phing.project.name}-${yii.version}.${yii.revision}"/> + <property name="docname" value="${phing.project.name}-docs-${yii.version}.${yii.revision}"/> + <property name="pearname" value="${phing.project.name}-${yii.release}.tgz" /> + + <!-- directory definitions --> + <property name="build.base.dir" value="release"/> + <property name="build.dist.dir" value="${build.base.dir}/dist"/> + <property name="build.src.dir" value="${build.base.dir}/${pkgname}"/> + <property name="build.pear.src.dir" value="${build.src.dir}/framework" /> + <property name="build.doc.dir" value="${build.base.dir}/${docname}"/> + <property name="build.web.dir" value="${build.base.dir}/web"/> + + <tstamp> + <format property="DATE" pattern="%b %e %Y" /> + </tstamp> + + <if> + <equals arg1="${yii.winbuild}" arg2="true"/> + <then> + <property name="build" value="build"/> + </then> + <else> + <property name="build" value="php build"/> + </else> + </if> + + <!-- source files in the framework --> + <fileset dir=".." id="framework"> + <exclude name="**/.gitignore"/> + <exclude name="**/*.bak"/> + <exclude name="**/*~"/> + <include name="framework/**/*"/> + <include name="requirements/**/*"/> + <include name="demos/**/*"/> + <include name="CHANGELOG"/> + <include name="UPGRADE"/> + <include name="LICENSE"/> + <include name="README"/> + </fileset> + + <!-- doc files --> + <fileset dir="../docs" id="docs"> + <exclude name="**/.gitignore"/> + <exclude name="**/*.bak"/> + <exclude name="**/*~"/> + <include name="guide/**/*"/> + <include name="blog/**/*"/> + </fileset> + + <fileset dir="../docs/guide" id="docs-guide"> + <exclude name="**/.gitignore"/> + <exclude name="**/*.bak"/> + <exclude name="**/*~"/> + <include name="**/*"/> + </fileset> + + <fileset dir="../docs/blog" id="docs-blog"> + <exclude name="**/.gitignore"/> + <exclude name="**/*.bak"/> + <exclude name="**/*~"/> + <include name="**/*"/> + </fileset> + + <fileset dir="." id="writables"> + <include name="${build.src.dir}/**/runtime" /> + <include name="${build.src.dir}/**/assets" /> + <include name="${build.src.dir}/demos/**/data" /> + </fileset> + + <fileset dir="." id="executables"> + <include name="${build.src.dir}/**/yiic" /> + </fileset> + + <target name="src" depends="sync"> + <echo>Building package ${pkgname}...</echo> + <echo>Copying files to build directory...</echo> + <copy todir="${build.src.dir}"> + <fileset refid="framework"/> + </copy> + + <echo>Changing file permissions...</echo> + <chmod mode="0777"> + <fileset refid="writables" /> + </chmod> + <chmod mode="0755"> + <fileset refid="executables" /> + </chmod> + + <echo>Generating source release file...</echo> + <mkdir dir="${build.dist.dir}" /> + <if> + <equals arg1="${yii.winbuild}" arg2="true"/> + <then> + <tar destfile="${build.dist.dir}/${pkgname}.tar.gz" compression="gzip"> + <fileset dir="${build.base.dir}"> + <include name="${pkgname}/**/*"/> + </fileset> + </tar> + </then> + <else> + <exec command="tar czpf ${pkgname}.tar.gz ${pkgname}" dir="${build.base.dir}"/> + <move file="${build.base.dir}/${pkgname}.tar.gz" todir="${build.dist.dir}" /> + </else> + </if> + <zip destfile="${build.dist.dir}/${pkgname}.zip"> + <fileset dir="${build.base.dir}"> + <include name="${pkgname}/**/*"/> + </fileset> + </zip> + </target> + + <target name="doc" depends="sync"> + <echo>Building documentation...</echo> + + <echo>Building Guide PDF...</echo> + <exec command="${build} guideLatex" dir="." passthru="true" /> + <exec command="${pdflatex} guide.tex -interaction=nonstopmode -max-print-line=120" dir="commands/guide" passthru="true"/> + <exec command="${pdflatex} guide.tex -interaction=nonstopmode -max-print-line=120" dir="commands/guide" passthru="true"/> + <exec command="${pdflatex} guide.tex -interaction=nonstopmode -max-print-line=120" dir="commands/guide" passthru="true"/> + <move file="commands/guide/guide.pdf" tofile="${build.doc.dir}/yii-guide-${yii.version}.pdf" /> + + <echo>Building Blog PDF...</echo> + <exec command="${build} blogLatex" dir="." passthru="true" /> + <exec command="${pdflatex} blog.tex -interaction=nonstopmode -max-print-line=120" dir="commands/blog" passthru="true"/> + <exec command="${pdflatex} blog.tex -interaction=nonstopmode -max-print-line=120" dir="commands/blog" passthru="true"/> + <exec command="${pdflatex} blog.tex -interaction=nonstopmode -max-print-line=120" dir="commands/blog" passthru="true"/> + <move file="commands/blog/blog.pdf" tofile="${build.doc.dir}/yii-blog-${yii.version}.pdf" /> + + <echo>Building API...</echo> + <exec command="${build} api ${build.doc.dir}" dir="." passthru="true" /> + + <!-- + <echo>Building API CHM...</echo> + <exec command="${hhc} ${build.doc.dir}/api/manual.hhp" /> + <move file="${build.doc.dir}/api/manual.chm" tofile="${build.doc.dir}/yii-api-${yii.version}.chm" /> + <delete> + <fileset dir="${build.doc.dir}/api"> + <include name="manual.*" /> + </fileset> + </delete> + --> + + <echo>Generating doc release file...</echo> + <mkdir dir="${build.dist.dir}" /> + <tar destfile="${build.dist.dir}/${docname}.tar.gz" compression="gzip"> + <fileset dir="${build.base.dir}"> + <include name="${docname}/**/*"/> + </fileset> + </tar> + <zip destfile="${build.dist.dir}/${docname}.zip"> + <fileset dir="${build.base.dir}"> + <include name="${docname}/**/*"/> + </fileset> + </zip> + </target> + + <target name="web" depends="sync"> + + <echo>Building online API...</echo> + <mkdir dir="${build.web.dir}/common/data/${yii.version}" /> + <exec command="${build} api ${build.web.dir}/common/data/${yii.version} online" dir="." passthru="true" /> + + <echo>Copying tutorials...</echo> + <copy todir="${build.web.dir}/common/data/${yii.version}/tutorials/guide"> + <fileset refid="docs-guide"/> + </copy> + <copy todir="${build.web.dir}/common/data/${yii.version}/tutorials/blog"> + <fileset refid="docs-blog"/> + </copy> + + <echo>Copying release text files...</echo> + <mkdir dir="${build.web.dir}/frontend/www/files" /> + <copy file="../CHANGELOG" tofile="${build.web.dir}/frontend/www/files/CHANGELOG-${yii.version}.txt" /> + <copy file="../UPGRADE" tofile="${build.web.dir}/frontend/www/files/UPGRADE-${yii.version}.txt" /> + + <echo> + +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, + ), + + </echo> + </target> + + <target name="sync"> + <echo>Synchronizing code changes for ${pkgname}...</echo> + + <echo>Building autoload map...</echo> + <exec command="${build} autoload" dir="." passthru="true"/> + + <echo>Building yiilite.php...</echo> + <exec command="${build} lite" dir="." passthru="true"/> + </target> + + <target name="message"> + <echo>Extracting i18n messages...</echo> + <exec command="${build} message ../framework/messages/config.php" dir="." passthru="true"/> + </target> + + <!-- + <target name="pear" depends="clean,build"> + <echo>Generating pear package for ${phing.project.name}-${yii.release}</echo> + <mkdir dir="${build.dist.dir}" /> + <yii-pear pkgdir="${build.pear.src.dir}" + channel="pear.php.net" + version="${yii.release}" + state="stable" + category="framework" + package="${phing.project.name}" + summary="Yii PHP Framework" + pkgdescription="Yii PHP Framework: Best for Web 2.0 Development" + notes="http://www.yiiframework.com/files/CHANGELOG-${yii.release}.txt" + license="BSD" + /> + <exec command="pear package" dir="${build.pear.src.dir}" passthru="true" /> + <move file="${build.pear.src.dir}/${pearname}" tofile="${build.dist.dir}/${pearname}" /> + </target> + --> + + <target name="clean"> + <echo>Cleaning up the build...</echo> + <delete dir="${build.base.dir}"/> + </target> + + <target name="help"> + <echo> + + 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 + + </echo> + </target> +</project> diff --git a/build/controllers/LocaleController.php b/build/controllers/LocaleController.php new file mode 100644 index 0000000..51de6a0 --- /dev/null +++ b/build/controllers/LocaleController.php @@ -0,0 +1,103 @@ +<?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +use yii\console\Controller; +use yii\helpers\FileHelper; + +/** + * http://www.unicode.org/cldr/charts/supplemental/language_plural_rules.html + * @author Qiang Xue <qiang.xue@gmail.com> + * @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|within)\s+(\d+)\.\.(\d+)/i' => '($1<$2||$1>$3)', //not in, not within + '/^(.*?)\s+within\s+(\d+)\.\.(\d+)/i' => '($1>=$2&&$1<=$3)', //within + '/^(.*?)\s+in\s+(\d+)\.\.(\d+)/i' => '($1>=$2&&$1<=$3&&fmod($1,1)==0)', //in + ); + 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); + } + $rules[] = preg_replace('/\\bn\\b/', '$n', implode('||', $expr_or)); + } + 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 "<?php\n"; + echo <<<EOD +/** + * Plural rules. + * + * This file is automatically generated by the "yiic locale/plural" command under the "build" folder. + * Do not modify it directly. + * + * The original plural rule data used for generating this file has the following copyright terms: + * + * Copyright © 1991-2007 Unicode, Inc. All rights reserved. + * Distributed under the Terms of Use in http://www.unicode.org/copyright.html. + * + * @revision $revision (of the original plural file) + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ +EOD; + echo "\nreturn " . var_export($allRules, true) . ';'; + } +} diff --git a/framework/YiiBase.php b/framework/YiiBase.php index 261a99e..e4a01b4 100644 --- a/framework/YiiBase.php +++ b/framework/YiiBase.php @@ -518,6 +518,7 @@ class YiiBase * @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 + * @see http://www.unicode.org/cldr/charts/supplemental/language_plural_rules.html */ public static function t($message, $params = array(), $language = null) { diff --git a/framework/base/Module.php b/framework/base/Module.php index cf751c0..3e7eb04 100644 --- a/framework/base/Module.php +++ b/framework/base/Module.php @@ -614,6 +614,12 @@ abstract class Module extends Component } 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."); + } } } } diff --git a/framework/console/Controller.php b/framework/console/Controller.php index 9924822..c7c5642 100644 --- a/framework/console/Controller.php +++ b/framework/console/Controller.php @@ -24,7 +24,6 @@ use yii\base\InvalidRouteException; * ~~~ * * @author Qiang Xue <qiang.xue@gmail.com> - * * @since 2.0 */ class Controller extends \yii\base\Controller 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/i18n/data/plurals.php b/framework/i18n/data/plurals.php new file mode 100644 index 0000000..3ed5619 --- /dev/null +++ b/framework/i18n/data/plurals.php @@ -0,0 +1,627 @@ +<?php +/** + * Plural rules. + * + * This file is automatically generated by the "yiic locale/plural" command under the "build" folder. + * Do not modify it directly. + * + * The original plural rule data used for generating this file has the following copyright terms: + * + * Copyright © 1991-2007 Unicode, Inc. All rights reserved. + * Distributed under the Terms of Use in http://www.unicode.org/copyright.html. + * + * @revision 6008 (of the original plural file) + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ +return array ( + 'ar' => + array ( + 0 => '$n==0', + 1 => '$n==1', + 2 => '$n==2', + 3 => '(fmod($n,100)>=3&&fmod($n,100)<=10&&fmod(fmod($n,100),1)==0)', + 4 => '(fmod($n,100)>=11&&fmod($n,100)<=99&&fmod(fmod($n,100),1)==0)', + ), + '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 => '($n>=3&&$n<=6&&fmod($n,1)==0)', + 3 => '($n>=7&&$n<=10&&fmod($n,1)==0)', + ), + 'ro' => + array ( + 0 => '$n==1', + 1 => '$n==0||$n!=1&&(fmod($n,100)>=1&&fmod($n,100)<=19&&fmod(fmod($n,100),1)==0)', + ), + 'mo' => + array ( + 0 => '$n==1', + 1 => '$n==0||$n!=1&&(fmod($n,100)>=1&&fmod($n,100)<=19&&fmod(fmod($n,100),1)==0)', + ), + 'lt' => + array ( + 0 => 'fmod($n,10)==1&&(fmod($n,100)<11||fmod($n,100)>19)', + 1 => '(fmod($n,10)>=2&&fmod($n,10)<=9&&fmod(fmod($n,10),1)==0)&&(fmod($n,100)<11||fmod($n,100)>19)', + ), + 'be' => + array ( + 0 => 'fmod($n,10)==1&&fmod($n,100)!=11', + 1 => '(fmod($n,10)>=2&&fmod($n,10)<=4&&fmod(fmod($n,10),1)==0)&&(fmod($n,100)<12||fmod($n,100)>14)', + 2 => 'fmod($n,10)==0||(fmod($n,10)>=5&&fmod($n,10)<=9&&fmod(fmod($n,10),1)==0)||(fmod($n,100)>=11&&fmod($n,100)<=14&&fmod(fmod($n,100),1)==0)', + ), + 'bs' => + array ( + 0 => 'fmod($n,10)==1&&fmod($n,100)!=11', + 1 => '(fmod($n,10)>=2&&fmod($n,10)<=4&&fmod(fmod($n,10),1)==0)&&(fmod($n,100)<12||fmod($n,100)>14)', + 2 => 'fmod($n,10)==0||(fmod($n,10)>=5&&fmod($n,10)<=9&&fmod(fmod($n,10),1)==0)||(fmod($n,100)>=11&&fmod($n,100)<=14&&fmod(fmod($n,100),1)==0)', + ), + 'hr' => + array ( + 0 => 'fmod($n,10)==1&&fmod($n,100)!=11', + 1 => '(fmod($n,10)>=2&&fmod($n,10)<=4&&fmod(fmod($n,10),1)==0)&&(fmod($n,100)<12||fmod($n,100)>14)', + 2 => 'fmod($n,10)==0||(fmod($n,10)>=5&&fmod($n,10)<=9&&fmod(fmod($n,10),1)==0)||(fmod($n,100)>=11&&fmod($n,100)<=14&&fmod(fmod($n,100),1)==0)', + ), + 'ru' => + array ( + 0 => 'fmod($n,10)==1&&fmod($n,100)!=11', + 1 => '(fmod($n,10)>=2&&fmod($n,10)<=4&&fmod(fmod($n,10),1)==0)&&(fmod($n,100)<12||fmod($n,100)>14)', + 2 => 'fmod($n,10)==0||(fmod($n,10)>=5&&fmod($n,10)<=9&&fmod(fmod($n,10),1)==0)||(fmod($n,100)>=11&&fmod($n,100)<=14&&fmod(fmod($n,100),1)==0)', + ), + 'sh' => + array ( + 0 => 'fmod($n,10)==1&&fmod($n,100)!=11', + 1 => '(fmod($n,10)>=2&&fmod($n,10)<=4&&fmod(fmod($n,10),1)==0)&&(fmod($n,100)<12||fmod($n,100)>14)', + 2 => 'fmod($n,10)==0||(fmod($n,10)>=5&&fmod($n,10)<=9&&fmod(fmod($n,10),1)==0)||(fmod($n,100)>=11&&fmod($n,100)<=14&&fmod(fmod($n,100),1)==0)', + ), + 'sr' => + array ( + 0 => 'fmod($n,10)==1&&fmod($n,100)!=11', + 1 => '(fmod($n,10)>=2&&fmod($n,10)<=4&&fmod(fmod($n,10),1)==0)&&(fmod($n,100)<12||fmod($n,100)>14)', + 2 => 'fmod($n,10)==0||(fmod($n,10)>=5&&fmod($n,10)<=9&&fmod(fmod($n,10),1)==0)||(fmod($n,100)>=11&&fmod($n,100)<=14&&fmod(fmod($n,100),1)==0)', + ), + 'uk' => + array ( + 0 => 'fmod($n,10)==1&&fmod($n,100)!=11', + 1 => '(fmod($n,10)>=2&&fmod($n,10)<=4&&fmod(fmod($n,10),1)==0)&&(fmod($n,100)<12||fmod($n,100)>14)', + 2 => 'fmod($n,10)==0||(fmod($n,10)>=5&&fmod($n,10)<=9&&fmod(fmod($n,10),1)==0)||(fmod($n,100)>=11&&fmod($n,100)<=14&&fmod(fmod($n,100),1)==0)', + ), + 'cs' => + array ( + 0 => '$n==1', + 1 => '($n>=2&&$n<=4&&fmod($n,1)==0)', + ), + 'sk' => + array ( + 0 => '$n==1', + 1 => '($n>=2&&$n<=4&&fmod($n,1)==0)', + ), + 'pl' => + array ( + 0 => '$n==1', + 1 => '(fmod($n,10)>=2&&fmod($n,10)<=4&&fmod(fmod($n,10),1)==0)&&(fmod($n,100)<12||fmod($n,100)>14)', + 2 => '$n!=1&&(fmod($n,10)>=0&&fmod($n,10)<=1&&fmod(fmod($n,10),1)==0)||(fmod($n,10)>=5&&fmod($n,10)<=9&&fmod(fmod($n,10),1)==0)||(fmod($n,100)>=12&&fmod($n,100)<=14&&fmod(fmod($n,100),1)==0)', + ), + 'sl' => + array ( + 0 => 'fmod($n,100)==1', + 1 => 'fmod($n,100)==2', + 2 => '(fmod($n,100)>=3&&fmod($n,100)<=4&&fmod(fmod($n,100),1)==0)', + ), + 'mt' => + array ( + 0 => '$n==1', + 1 => '$n==0||(fmod($n,100)>=2&&fmod($n,100)<=10&&fmod(fmod($n,100),1)==0)', + 2 => '(fmod($n,100)>=11&&fmod($n,100)<=19&&fmod(fmod($n,100),1)==0)', + ), + '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 => '($n>=2&&$n<=10&&fmod($n,1)==0)', + ), + '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)||($n>=11&&$n<=99&&fmod($n,1)==0)', + ), + 'gv' => + array ( + 0 => '(fmod($n,10)>=1&&fmod($n,10)<=2&&fmod(fmod($n,10),1)==0)||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 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<!DOCTYPE supplementalData SYSTEM "../../common/dtd/ldmlSupplemental.dtd"> +<supplementalData> + <version number="$Revision: 6008 $"/> + <generation date="$Date: 2011-07-12 13:18:01 -0500 (Tue, 12 Jul 2011) $"/> + <plurals> + <!-- if locale is known to have no plurals, there are no rules --> + <pluralRules locales="az bm bo dz fa id ig ii hu ja jv ka kde kea km kn ko lo ms my sah ses sg th to tr vi wo yo zh"/> + <pluralRules locales="ar"> + <pluralRule count="zero">n is 0</pluralRule> + <pluralRule count="one">n is 1</pluralRule> + <pluralRule count="two">n is 2</pluralRule> + <pluralRule count="few">n mod 100 in 3..10</pluralRule> + <pluralRule count="many">n mod 100 in 11..99</pluralRule> + </pluralRules> + <pluralRules locales="asa af bem bez bg bn brx ca cgg chr da de dv ee el en eo es et eu fi fo fur fy gl gsw gu ha haw he is it jmc kaj kcg kk kl ksb ku lb lg mas ml mn mr nah nb nd ne nl nn no nr ny nyn om or pa pap ps pt rof rm rwk saq seh sn so sq ss ssy st sv sw syr ta te teo tig tk tn ts ur wae ve vun xh xog zu"> + <pluralRule count="one">n is 1</pluralRule> + </pluralRules> + <pluralRules locales="ak am bh fil tl guw hi ln mg nso ti wa"> + <pluralRule count="one">n in 0..1</pluralRule> + </pluralRules> + <pluralRules locales="ff fr kab"> + <pluralRule count="one">n within 0..2 and n is not 2</pluralRule> + </pluralRules> + <pluralRules locales="lv"> + <pluralRule count="zero">n is 0</pluralRule> + <pluralRule count="one">n mod 10 is 1 and n mod 100 is not 11</pluralRule> + </pluralRules> + <pluralRules locales="iu kw naq se sma smi smj smn sms"> + <pluralRule count="one">n is 1</pluralRule> + <pluralRule count="two">n is 2</pluralRule> + </pluralRules> + <pluralRules locales="ga"> <!-- http://unicode.org/cldr/trac/ticket/3915 --> + <pluralRule count="one">n is 1</pluralRule> + <pluralRule count="two">n is 2</pluralRule> + <pluralRule count="few">n in 3..6</pluralRule> + <pluralRule count="many">n in 7..10</pluralRule> + </pluralRules> + <pluralRules locales="ro mo"> + <pluralRule count="one">n is 1</pluralRule> + <pluralRule count="few">n is 0 OR n is not 1 AND n mod 100 in 1..19</pluralRule> + </pluralRules> + <pluralRules locales="lt"> + <pluralRule count="one">n mod 10 is 1 and n mod 100 not in 11..19</pluralRule> + <pluralRule count="few">n mod 10 in 2..9 and n mod 100 not in 11..19</pluralRule> + </pluralRules> + <pluralRules locales="be bs hr ru sh sr uk"> + <pluralRule count="one">n mod 10 is 1 and n mod 100 is not 11</pluralRule> + <pluralRule count="few">n mod 10 in 2..4 and n mod 100 not in 12..14</pluralRule> + <pluralRule count="many">n mod 10 is 0 or n mod 10 in 5..9 or n mod 100 in 11..14</pluralRule> + <!-- others are fractions --> + </pluralRules> + <pluralRules locales="cs sk"> + <pluralRule count="one">n is 1</pluralRule> + <pluralRule count="few">n in 2..4</pluralRule> + </pluralRules> + <pluralRules locales="pl"> + <pluralRule count="one">n is 1</pluralRule> + <pluralRule count="few">n mod 10 in 2..4 and n mod 100 not in 12..14</pluralRule> + <pluralRule count="many">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</pluralRule> + <!-- others are fractions --> + <!-- and n mod 100 not in 22..24 from Tamplin --> + </pluralRules> + <pluralRules locales="sl"> + <pluralRule count="one">n mod 100 is 1</pluralRule> + <pluralRule count="two">n mod 100 is 2</pluralRule> + <pluralRule count="few">n mod 100 in 3..4</pluralRule> + </pluralRules> + <pluralRules locales="mt"> <!-- from Tamplin's data --> + <pluralRule count="one">n is 1</pluralRule> + <pluralRule count="few">n is 0 or n mod 100 in 2..10</pluralRule> + <pluralRule count="many">n mod 100 in 11..19</pluralRule> + </pluralRules> + <pluralRules locales="mk"> <!-- from Tamplin's data --> + <pluralRule count="one">n mod 10 is 1 and n is not 11</pluralRule> + </pluralRules> + <pluralRules locales="cy"> <!-- from http://www.saltcymru.org/wordpress/?p=99&lang=en --> + <pluralRule count="zero">n is 0</pluralRule> + <pluralRule count="one">n is 1</pluralRule> + <pluralRule count="two">n is 2</pluralRule> + <pluralRule count="few">n is 3</pluralRule> + <pluralRule count="many">n is 6</pluralRule> + </pluralRules> + <pluralRules locales="lag"> + <pluralRule count="zero">n is 0</pluralRule> + <pluralRule count="one">n within 0..2 and n is not 0 and n is not 2</pluralRule> + </pluralRules> + <pluralRules locales="shi"> + <pluralRule count="one">n within 0..1</pluralRule> + <pluralRule count="few">n in 2..10</pluralRule> + </pluralRules> + <pluralRules locales="br"> <!-- from http://unicode.org/cldr/trac/ticket/2886 --> + <pluralRule count="one">n mod 10 is 1 and n mod 100 not in 11,71,91</pluralRule> + <pluralRule count="two">n mod 10 is 2 and n mod 100 not in 12,72,92</pluralRule> + <pluralRule count="few">n mod 10 in 3..4,9 and n mod 100 not in 10..19,70..79,90..99</pluralRule> + <pluralRule count="many">n mod 1000000 is 0 and n is not 0</pluralRule> + </pluralRules> + <pluralRules locales="ksh"> + <pluralRule count="zero">n is 0</pluralRule> + <pluralRule count="one">n is 1</pluralRule> + </pluralRules> + <pluralRules locales="tzm"> + <pluralRule count="one">n in 0..1 or n in 11..99</pluralRule> + </pluralRules> + <pluralRules locales="gv"> + <pluralRule count="one">n mod 10 in 1..2 or n mod 20 is 0</pluralRule> + </pluralRules> + </plurals> +</supplementalData> From c629ad776abe36501fd6c034cff8fc115fdc7f59 Mon Sep 17 00:00:00 2001 From: Qiang Xue <qiang.xue@gmail.com> Date: Mon, 1 Apr 2013 17:59:14 -0400 Subject: [PATCH 37/41] Finished plural forms. --- build/build | 0 build/controllers/LocaleController.php | 17 ++++-- framework/YiiBase.php | 39 ++++++++------ framework/base/ErrorHandler.php | 10 ++-- framework/i18n/I18N.php | 97 ++++++++++++++++++++++++++++++---- framework/i18n/data/plurals.php | 66 +++++++++++------------ 6 files changed, 162 insertions(+), 67 deletions(-) mode change 100644 => 100755 build/build diff --git a/build/build b/build/build old mode 100644 new mode 100755 diff --git a/build/controllers/LocaleController.php b/build/controllers/LocaleController.php index 51de6a0..00de787 100644 --- a/build/controllers/LocaleController.php +++ b/build/controllers/LocaleController.php @@ -42,9 +42,10 @@ class LocaleController extends Controller '/\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|within)\s+(\d+)\.\.(\d+)/i' => '($1<$2||$1>$3)', //not in, not within + '/^(.*?)\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 - '/^(.*?)\s+in\s+(\d+)\.\.(\d+)/i' => '($1>=$2&&$1<=$3&&fmod($1,1)==0)', //in ); foreach ($xml->plurals->pluralRules as $node) { $attributes = $node->attributes(); @@ -59,7 +60,15 @@ class LocaleController extends Controller $expr_and = preg_replace(array_keys($patterns), array_values($patterns), $expr_and); $expr_or[$key_or] = implode('&&', $expr_and); } - $rules[] = preg_replace('/\\bn\\b/', '$n', implode('||', $expr_or)); + $expr = preg_replace('/\\bn\\b/', '$n', implode('||', $expr_or)); + $rules[] = preg_replace_callback('/range\((\d+),(\d+)\)/', function ($matches) { + if ($matches[2] - $matches[1] <= 2) { + return 'array(' . implode(',', range($matches[1], $matches[2])) . ')'; + } else { + return $matches[0]; + } + }, $expr); + } foreach ($locales as $locale) { $allRules[$locale] = $rules; @@ -70,7 +79,7 @@ class LocaleController extends Controller $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))))', + 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)) { diff --git a/framework/YiiBase.php b/framework/YiiBase.php index e4a01b4..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 */ @@ -504,21 +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 <code>strtr</code>. - * 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 - * @see http://www.unicode.org/cldr/charts/supplemental/language_plural_rules.html + * + * 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/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 <qiang.xue@gmail.com> * @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/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 @@ <?php +/** + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ namespace yii\i18n; use Yii; use yii\base\Component; use yii\base\InvalidConfigException; +use yii\base\InvalidParamException; +/** + * I18N provides features related with internationalization (I18N) and localization (L10N). + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @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/data/plurals.php b/framework/i18n/data/plurals.php index 3ed5619..b82fec0 100644 --- a/framework/i18n/data/plurals.php +++ b/framework/i18n/data/plurals.php @@ -21,8 +21,8 @@ return array ( 0 => '$n==0', 1 => '$n==1', 2 => '$n==2', - 3 => '(fmod($n,100)>=3&&fmod($n,100)<=10&&fmod(fmod($n,100),1)==0)', - 4 => '(fmod($n,100)>=11&&fmod($n,100)<=99&&fmod(fmod($n,100),1)==0)', + 3 => 'in_array(fmod($n,100),range(3,10))', + 4 => 'in_array(fmod($n,100),range(11,99))', ), 'asa' => array ( @@ -494,93 +494,93 @@ return array ( array ( 0 => '$n==1', 1 => '$n==2', - 2 => '($n>=3&&$n<=6&&fmod($n,1)==0)', - 3 => '($n>=7&&$n<=10&&fmod($n,1)==0)', + 2 => 'in_array($n,range(3,6))', + 3 => 'in_array($n,range(7,10))', ), 'ro' => array ( 0 => '$n==1', - 1 => '$n==0||$n!=1&&(fmod($n,100)>=1&&fmod($n,100)<=19&&fmod(fmod($n,100),1)==0)', + 1 => '$n==0||$n!=1&&in_array(fmod($n,100),range(1,19))', ), 'mo' => array ( 0 => '$n==1', - 1 => '$n==0||$n!=1&&(fmod($n,100)>=1&&fmod($n,100)<=19&&fmod(fmod($n,100),1)==0)', + 1 => '$n==0||$n!=1&&in_array(fmod($n,100),range(1,19))', ), 'lt' => array ( - 0 => 'fmod($n,10)==1&&(fmod($n,100)<11||fmod($n,100)>19)', - 1 => '(fmod($n,10)>=2&&fmod($n,10)<=9&&fmod(fmod($n,10),1)==0)&&(fmod($n,100)<11||fmod($n,100)>19)', + 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 => '(fmod($n,10)>=2&&fmod($n,10)<=4&&fmod(fmod($n,10),1)==0)&&(fmod($n,100)<12||fmod($n,100)>14)', - 2 => 'fmod($n,10)==0||(fmod($n,10)>=5&&fmod($n,10)<=9&&fmod(fmod($n,10),1)==0)||(fmod($n,100)>=11&&fmod($n,100)<=14&&fmod(fmod($n,100),1)==0)', + 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),range(5,9))||in_array(fmod($n,100),range(11,14))', ), 'bs' => array ( 0 => 'fmod($n,10)==1&&fmod($n,100)!=11', - 1 => '(fmod($n,10)>=2&&fmod($n,10)<=4&&fmod(fmod($n,10),1)==0)&&(fmod($n,100)<12||fmod($n,100)>14)', - 2 => 'fmod($n,10)==0||(fmod($n,10)>=5&&fmod($n,10)<=9&&fmod(fmod($n,10),1)==0)||(fmod($n,100)>=11&&fmod($n,100)<=14&&fmod(fmod($n,100),1)==0)', + 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),range(5,9))||in_array(fmod($n,100),range(11,14))', ), 'hr' => array ( 0 => 'fmod($n,10)==1&&fmod($n,100)!=11', - 1 => '(fmod($n,10)>=2&&fmod($n,10)<=4&&fmod(fmod($n,10),1)==0)&&(fmod($n,100)<12||fmod($n,100)>14)', - 2 => 'fmod($n,10)==0||(fmod($n,10)>=5&&fmod($n,10)<=9&&fmod(fmod($n,10),1)==0)||(fmod($n,100)>=11&&fmod($n,100)<=14&&fmod(fmod($n,100),1)==0)', + 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),range(5,9))||in_array(fmod($n,100),range(11,14))', ), 'ru' => array ( 0 => 'fmod($n,10)==1&&fmod($n,100)!=11', - 1 => '(fmod($n,10)>=2&&fmod($n,10)<=4&&fmod(fmod($n,10),1)==0)&&(fmod($n,100)<12||fmod($n,100)>14)', - 2 => 'fmod($n,10)==0||(fmod($n,10)>=5&&fmod($n,10)<=9&&fmod(fmod($n,10),1)==0)||(fmod($n,100)>=11&&fmod($n,100)<=14&&fmod(fmod($n,100),1)==0)', + 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),range(5,9))||in_array(fmod($n,100),range(11,14))', ), 'sh' => array ( 0 => 'fmod($n,10)==1&&fmod($n,100)!=11', - 1 => '(fmod($n,10)>=2&&fmod($n,10)<=4&&fmod(fmod($n,10),1)==0)&&(fmod($n,100)<12||fmod($n,100)>14)', - 2 => 'fmod($n,10)==0||(fmod($n,10)>=5&&fmod($n,10)<=9&&fmod(fmod($n,10),1)==0)||(fmod($n,100)>=11&&fmod($n,100)<=14&&fmod(fmod($n,100),1)==0)', + 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),range(5,9))||in_array(fmod($n,100),range(11,14))', ), 'sr' => array ( 0 => 'fmod($n,10)==1&&fmod($n,100)!=11', - 1 => '(fmod($n,10)>=2&&fmod($n,10)<=4&&fmod(fmod($n,10),1)==0)&&(fmod($n,100)<12||fmod($n,100)>14)', - 2 => 'fmod($n,10)==0||(fmod($n,10)>=5&&fmod($n,10)<=9&&fmod(fmod($n,10),1)==0)||(fmod($n,100)>=11&&fmod($n,100)<=14&&fmod(fmod($n,100),1)==0)', + 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),range(5,9))||in_array(fmod($n,100),range(11,14))', ), 'uk' => array ( 0 => 'fmod($n,10)==1&&fmod($n,100)!=11', - 1 => '(fmod($n,10)>=2&&fmod($n,10)<=4&&fmod(fmod($n,10),1)==0)&&(fmod($n,100)<12||fmod($n,100)>14)', - 2 => 'fmod($n,10)==0||(fmod($n,10)>=5&&fmod($n,10)<=9&&fmod(fmod($n,10),1)==0)||(fmod($n,100)>=11&&fmod($n,100)<=14&&fmod(fmod($n,100),1)==0)', + 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),range(5,9))||in_array(fmod($n,100),range(11,14))', ), 'cs' => array ( 0 => '$n==1', - 1 => '($n>=2&&$n<=4&&fmod($n,1)==0)', + 1 => 'in_array($n,array(2,3,4))', ), 'sk' => array ( 0 => '$n==1', - 1 => '($n>=2&&$n<=4&&fmod($n,1)==0)', + 1 => 'in_array($n,array(2,3,4))', ), 'pl' => array ( 0 => '$n==1', - 1 => '(fmod($n,10)>=2&&fmod($n,10)<=4&&fmod(fmod($n,10),1)==0)&&(fmod($n,100)<12||fmod($n,100)>14)', - 2 => '$n!=1&&(fmod($n,10)>=0&&fmod($n,10)<=1&&fmod(fmod($n,10),1)==0)||(fmod($n,10)>=5&&fmod($n,10)<=9&&fmod(fmod($n,10),1)==0)||(fmod($n,100)>=12&&fmod($n,100)<=14&&fmod(fmod($n,100),1)==0)', + 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),range(5,9))||in_array(fmod($n,100),array(12,13,14))', ), 'sl' => array ( 0 => 'fmod($n,100)==1', 1 => 'fmod($n,100)==2', - 2 => '(fmod($n,100)>=3&&fmod($n,100)<=4&&fmod(fmod($n,100),1)==0)', + 2 => 'in_array(fmod($n,100),array(3,4))', ), 'mt' => array ( 0 => '$n==1', - 1 => '$n==0||(fmod($n,100)>=2&&fmod($n,100)<=10&&fmod(fmod($n,100),1)==0)', - 2 => '(fmod($n,100)>=11&&fmod($n,100)<=19&&fmod(fmod($n,100),1)==0)', + 1 => '$n==0||in_array(fmod($n,100),range(2,10))', + 2 => 'in_array(fmod($n,100),range(11,19))', ), 'mk' => array ( @@ -602,13 +602,13 @@ return array ( 'shi' => array ( 0 => '($n>=0&&$n<=1)', - 1 => '($n>=2&&$n<=10&&fmod($n,1)==0)', + 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))))', + 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' => @@ -618,10 +618,10 @@ return array ( ), 'tzm' => array ( - 0 => '($n==0||$n==1)||($n>=11&&$n<=99&&fmod($n,1)==0)', + 0 => '($n==0||$n==1)||in_array($n,range(11,99))', ), 'gv' => array ( - 0 => '(fmod($n,10)>=1&&fmod($n,10)<=2&&fmod(fmod($n,10),1)==0)||fmod($n,20)==0', + 0 => 'in_array(fmod($n,10),array(1,2))||fmod($n,20)==0', ), ); \ No newline at end of file From 0e8e94bcb9079adbcbc37a6d5020a4372fedaf0a Mon Sep 17 00:00:00 2001 From: Qiang Xue <qiang.xue@gmail.com> Date: Tue, 2 Apr 2013 17:59:54 -0400 Subject: [PATCH 38/41] cleanup. --- build/controllers/LocaleController.php | 2 +- framework/i18n/data/plurals.php | 20 +++++++++--------- framework/widgets/ActiveForm.php | 38 +++++++++++++++++++++++++++------- 3 files changed, 42 insertions(+), 18 deletions(-) diff --git a/build/controllers/LocaleController.php b/build/controllers/LocaleController.php index 00de787..d471c0d 100644 --- a/build/controllers/LocaleController.php +++ b/build/controllers/LocaleController.php @@ -62,7 +62,7 @@ class LocaleController extends Controller } $expr = preg_replace('/\\bn\\b/', '$n', implode('||', $expr_or)); $rules[] = preg_replace_callback('/range\((\d+),(\d+)\)/', function ($matches) { - if ($matches[2] - $matches[1] <= 2) { + if ($matches[2] - $matches[1] <= 5) { return 'array(' . implode(',', range($matches[1], $matches[2])) . ')'; } else { return $matches[0]; diff --git a/framework/i18n/data/plurals.php b/framework/i18n/data/plurals.php index b82fec0..52c733b 100644 --- a/framework/i18n/data/plurals.php +++ b/framework/i18n/data/plurals.php @@ -494,8 +494,8 @@ return array ( array ( 0 => '$n==1', 1 => '$n==2', - 2 => 'in_array($n,range(3,6))', - 3 => 'in_array($n,range(7,10))', + 2 => 'in_array($n,array(3,4,5,6))', + 3 => 'in_array($n,array(7,8,9,10))', ), 'ro' => array ( @@ -516,43 +516,43 @@ return array ( 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),range(5,9))||in_array(fmod($n,100),range(11,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),range(5,9))||in_array(fmod($n,100),range(11,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),range(5,9))||in_array(fmod($n,100),range(11,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),range(5,9))||in_array(fmod($n,100),range(11,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),range(5,9))||in_array(fmod($n,100),range(11,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),range(5,9))||in_array(fmod($n,100),range(11,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),range(5,9))||in_array(fmod($n,100),range(11,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 ( @@ -568,7 +568,7 @@ return array ( 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),range(5,9))||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 ( 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]; From 4fe13c937f139a7537893ccccb0ed7ebe76032c8 Mon Sep 17 00:00:00 2001 From: resurtm <resurtm@gmail.com> Date: Wed, 3 Apr 2013 23:10:54 +0600 Subject: [PATCH 39/41] ChainedDependency typo fix. --- framework/caching/ChainedDependency.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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(); } } From aad6e6c8ce17cf2ea239d556b34312aab5886c76 Mon Sep 17 00:00:00 2001 From: resurtm <resurtm@gmail.com> Date: Thu, 4 Apr 2013 00:01:09 +0600 Subject: [PATCH 40/41] Unused `use` has been removed. --- framework/widgets/FragmentCache.php | 1 - 1 file changed, 1 deletion(-) diff --git a/framework/widgets/FragmentCache.php b/framework/widgets/FragmentCache.php index dae538a..637d115 100644 --- a/framework/widgets/FragmentCache.php +++ b/framework/widgets/FragmentCache.php @@ -8,7 +8,6 @@ namespace yii\widgets; use Yii; -use yii\base\InvalidConfigException; use yii\base\Widget; use yii\caching\Cache; use yii\caching\Dependency; From b9a835518a581ccf517d163ad662d322e13e7fc3 Mon Sep 17 00:00:00 2001 From: resurtm <resurtm@gmail.com> Date: Thu, 4 Apr 2013 00:05:09 +0600 Subject: [PATCH 41/41] Fixes issue related to the cache dependencies constructor parameters. (Discussed via Skype with Qiang.) --- framework/caching/DbDependency.php | 18 ++++++++---------- framework/caching/ExpressionDependency.php | 13 +------------ framework/caching/FileDependency.php | 16 +++++++++------- 3 files changed, 18 insertions(+), 29 deletions(-) diff --git a/framework/caching/DbDependency.php b/framework/caching/DbDependency.php index cbe0ae1..4308dc1 100644 --- a/framework/caching/DbDependency.php +++ b/framework/caching/DbDependency.php @@ -28,25 +28,23 @@ class DbDependency extends Dependency 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); + if ($this->sql === null) { + throw new InvalidConfigException('DbDependency::sql must be set.'); + } } /** 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.'); + } } /**