From 7aa80d86f50d6a0fee4323aa31c5005970888d52 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Fri, 1 Feb 2013 17:18:49 -0500 Subject: [PATCH 001/117] refactored execute() and query() methods in db/Command. --- framework/base/HttpException.php | 69 +++++++++++++++++++++++++++++++++ framework/db/Command.php | 59 +++++++--------------------- framework/db/QueryBuilder.php | 3 +- tests/unit/framework/db/CommandTest.php | 8 ---- 4 files changed, 84 insertions(+), 55 deletions(-) diff --git a/framework/base/HttpException.php b/framework/base/HttpException.php index d2de5bc..ca753b9 100644 --- a/framework/base/HttpException.php +++ b/framework/base/HttpException.php @@ -41,4 +41,73 @@ class HttpException extends Exception $this->statusCode = $status; parent::__construct($message, $code); } + + /** + * @return string the user-friendly name of this exception + */ + public function getName() + { + static $httpCodes = array( + 100 => 'Continue', + 101 => 'Switching Protocols', + 102 => 'Processing', + 118 => 'Connection timed out', + 200 => 'OK', + 201 => 'Created', + 202 => 'Accepted', + 203 => 'Non-Authoritative', + 204 => 'No Content', + 205 => 'Reset Content', + 206 => 'Partial Content', + 207 => 'Multi-Status', + 210 => 'Content Different', + 300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', + 303 => 'See Other', + 304 => 'Not Modified', + 305 => 'Use Proxy', + 307 => 'Temporary Redirect', + 310 => 'Too many Redirect', + 400 => 'Bad Request', + 401 => 'Unauthorized', + 402 => 'Payment Required', + 403 => 'Forbidden', + 404 => 'Not Found', + 405 => 'Method Not Allowed', + 406 => 'Not Acceptable', + 407 => 'Proxy Authentication Required', + 408 => 'Request Time-out', + 409 => 'Conflict', + 410 => 'Gone', + 411 => 'Length Required', + 412 => 'Precondition Failed', + 413 => 'Request Entity Too Large', + 414 => 'Request-URI Too Long', + 415 => 'Unsupported Media Type', + 416 => 'Requested range unsatisfiable', + 417 => 'Expectation failed', + 418 => 'I’m a teapot', + 422 => 'Unprocessable entity', + 423 => 'Locked', + 424 => 'Method failure', + 425 => 'Unordered Collection', + 426 => 'Upgrade Required', + 449 => 'Retry With', + 450 => 'Blocked by Windows Parental Controls', + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway ou Proxy Error', + 503 => 'Service Unavailable', + 504 => 'Gateway Time-out', + 505 => 'HTTP Version not supported', + 507 => 'Insufficient storage', + 509 => 'Bandwidth Limit Exceeded', + ); + + if(isset($httpCodes[$this->statusCode])) + return $httpCodes[$this->statusCode]; + else + return \Yii::t('yii', 'Error'); + } } diff --git a/framework/db/Command.php b/framework/db/Command.php index 3531fa7..f893c8c 100644 --- a/framework/db/Command.php +++ b/framework/db/Command.php @@ -253,17 +253,13 @@ class Command extends \yii\base\Component * Executes the SQL statement. * This method should only be used for executing non-query SQL statement, such as `INSERT`, `DELETE`, `UPDATE` SQLs. * No result set will be returned. - * @param array $params input parameters (name=>value) for the SQL execution. This is an alternative - * to [[bindValues()]]. Note that if you pass parameters in this way, any previous call to [[bindParam()]] - * or [[bindValue()]] will be ignored. * @return integer number of rows affected by the execution. * @throws Exception execution failed */ - public function execute($params = array()) + public function execute() { $sql = $this->getSql(); - $this->_params = array_merge($this->_params, $params); if ($this->_params === array()) { $paramLog = ''; } else { @@ -282,11 +278,7 @@ class Command extends \yii\base\Component } $this->prepare(); - if ($params === array()) { - $this->pdoStatement->execute(); - } else { - $this->pdoStatement->execute($params); - } + $this->pdoStatement->execute(); $n = $this->pdoStatement->rowCount(); if ($this->db->enableProfiling) { @@ -309,63 +301,51 @@ class Command extends \yii\base\Component /** * Executes the SQL statement and returns query result. * This method is for executing a SQL query that returns result set, such as `SELECT`. - * @param array $params input parameters (name=>value) for the SQL execution. This is an alternative - * to [[bindValues()]]. Note that if you pass parameters in this way, any previous call to [[bindParam()]] - * or [[bindValue()]] will be ignored. * @return DataReader the reader object for fetching the query result * @throws Exception execution failed */ - public function query($params = array()) + public function query() { - return $this->queryInternal('', $params); + return $this->queryInternal(''); } /** * Executes the SQL statement and returns ALL rows at once. - * @param array $params input parameters (name=>value) for the SQL execution. This is an alternative - * to [[bindValues()]]. Note that if you pass parameters in this way, any previous call to [[bindParam()]] - * or [[bindValue()]] will be ignored. * @param mixed $fetchMode the result fetch mode. Please refer to [PHP manual](http://www.php.net/manual/en/function.PDOStatement-setFetchMode.php) * for valid fetch modes. If this parameter is null, the value set in [[fetchMode]] will be used. * @return array all rows of the query result. Each array element is an array representing a row of data. * An empty array is returned if the query results in nothing. * @throws Exception execution failed */ - public function queryAll($params = array(), $fetchMode = null) + public function queryAll($fetchMode = null) { - return $this->queryInternal('fetchAll', $params, $fetchMode); + return $this->queryInternal('fetchAll', $fetchMode); } /** * Executes the SQL statement and returns the first row of the result. * This method is best used when only the first row of result is needed for a query. - * @param array $params input parameters (name=>value) for the SQL execution. This is an alternative - * to [[bindValues()]]. Note that if you pass parameters in this way, any previous call to [[bindParam()]] - * or [[bindValue()]] will be ignored. * @param mixed $fetchMode the result fetch mode. Please refer to [PHP manual](http://www.php.net/manual/en/function.PDOStatement-setFetchMode.php) * for valid fetch modes. If this parameter is null, the value set in [[fetchMode]] will be used. * @return array|boolean the first row (in terms of an array) of the query result. False is returned if the query * results in nothing. * @throws Exception execution failed */ - public function queryRow($params = array(), $fetchMode = null) + public function queryRow($fetchMode = null) { - return $this->queryInternal('fetch', $params, $fetchMode); + return $this->queryInternal('fetch', $fetchMode); } /** * Executes the SQL statement and returns the value of the first column in the first row of data. * This method is best used when only a single value is needed for a query. - * @param array $params input parameters (name=>value) for the SQL execution. This is an alternative - * to [[bindValues()]]. Note that if you pass parameters in this way, any previous call to [[bindParam()]] - * or [[bindValue()]] will be ignored. * @return string|boolean the value of the first column in the first row of the query result. * False is returned if there is no value. * @throws Exception execution failed */ - public function queryScalar($params = array()) + public function queryScalar() { - $result = $this->queryInternal('fetchColumn', $params, 0); + $result = $this->queryInternal('fetchColumn', 0); if (is_resource($result) && get_resource_type($result) === 'stream') { return stream_get_contents($result); } else { @@ -377,33 +357,26 @@ class Command extends \yii\base\Component * Executes the SQL statement and returns the first column of the result. * This method is best used when only the first column of result (i.e. the first element in each row) * is needed for a query. - * @param array $params input parameters (name=>value) for the SQL execution. This is an alternative - * to [[bindValues()]]. Note that if you pass parameters in this way, any previous call to [[bindParam()]] - * or [[bindValue()]] will be ignored. * @return array the first column of the query result. Empty array is returned if the query results in nothing. * @throws Exception execution failed */ - public function queryColumn($params = array()) + public function queryColumn() { - return $this->queryInternal('fetchAll', $params, \PDO::FETCH_COLUMN); + return $this->queryInternal('fetchAll', \PDO::FETCH_COLUMN); } /** * Performs the actual DB query of a SQL statement. * @param string $method method of PDOStatement to be called - * @param array $params input parameters (name=>value) for the SQL execution. This is an alternative - * to [[bindValues()]]. Note that if you pass parameters in this way, any previous call to [[bindParam()]] - * or [[bindValue()]] will be ignored. * @param mixed $fetchMode the result fetch mode. Please refer to [PHP manual](http://www.php.net/manual/en/function.PDOStatement-setFetchMode.php) * for valid fetch modes. If this parameter is null, the value set in [[fetchMode]] will be used. * @return mixed the method execution result * @throws Exception if the query causes any problem */ - private function queryInternal($method, $params, $fetchMode = null) + private function queryInternal($method, $fetchMode = null) { $db = $this->db; $sql = $this->getSql(); - $this->_params = array_merge($this->_params, $params); if ($this->_params === array()) { $paramLog = ''; } else { @@ -431,11 +404,7 @@ class Command extends \yii\base\Component } $this->prepare(); - if ($params === array()) { - $this->pdoStatement->execute(); - } else { - $this->pdoStatement->execute($params); - } + $this->pdoStatement->execute(); if ($method === '') { $result = new DataReader($this); diff --git a/framework/db/QueryBuilder.php b/framework/db/QueryBuilder.php index 35bfcb3..ebca888 100644 --- a/framework/db/QueryBuilder.php +++ b/framework/db/QueryBuilder.php @@ -131,11 +131,10 @@ class QueryBuilder extends \yii\base\Object * @param string $table the table that new rows will be inserted into. * @param array $columns the column names * @param array $rows the rows to be batch inserted into the table - * @param array $params the parameters to be bound to the command * @return string the batch INSERT SQL statement * @throws NotSupportedException if this is not supported by the underlying DBMS */ - public function batchInsert($table, $columns, $rows, $params = array()) + public function batchInsert($table, $columns, $rows) { throw new NotSupportedException($this->db->getDriverName() . ' does not support batch insert.'); diff --git a/tests/unit/framework/db/CommandTest.php b/tests/unit/framework/db/CommandTest.php index 2a11fcb..d505f6d 100644 --- a/tests/unit/framework/db/CommandTest.php +++ b/tests/unit/framework/db/CommandTest.php @@ -189,14 +189,6 @@ class CommandTest extends \yiiunit\MysqlTestCase $command = $db->createCommand($sql); $command->bindValue(':name', 'user5'); $this->assertEquals('user5@example.com', $command->queryScalar()); - - // bind value via query or execute method - $sql = 'INSERT INTO tbl_customer(email, name, address) VALUES (:email, \'user6\', \'address6\')'; - $command = $db->createCommand($sql); - $command->execute(array(':email' => 'user6@example.com')); - $sql = 'SELECT email FROM tbl_customer WHERE name=:name'; - $command = $db->createCommand($sql); - $this->assertEquals('user5@example.com', $command->queryScalar(array(':name' => 'user5'))); } function testFetchMode() From 2d7f048b747b8e487f1fbc5231c8699a7dc1738b Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Fri, 1 Feb 2013 19:28:38 -0500 Subject: [PATCH 002/117] Refactored usage error in console commands. --- framework/console/Application.php | 12 +++------ framework/console/BadUsageException.php | 33 +++++++++++++++++++++++ framework/console/Controller.php | 13 +++------ framework/console/controllers/HelpController.php | 34 +++++++++++------------- 4 files changed, 54 insertions(+), 38 deletions(-) create mode 100644 framework/console/BadUsageException.php diff --git a/framework/console/Application.php b/framework/console/Application.php index 237be05..47c3142 100644 --- a/framework/console/Application.php +++ b/framework/console/Application.php @@ -93,8 +93,7 @@ class Application extends \yii\base\Application if ($request->getIsConsoleRequest()) { return $this->runAction($request->route, $request->params); } else { - echo "Error: this script must be run from the command line."; - return 1; + throw new BadUsageException(\Yii::t('yii', 'this script must be run from the command line.')); } } @@ -106,14 +105,14 @@ class Application extends \yii\base\Application * @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 BadUsageException if the route is invalid */ public function runAction($route, $params = array()) { try { return parent::runAction($route, $params); } catch (InvalidRouteException $e) { - echo "Error: unknown command \"$route\".\n"; - return 1; + throw new BadUsageException(\Yii::t('yii', 'Unknown command "{command}".', array('{command}' => $route))); } } @@ -148,9 +147,4 @@ class Application extends \yii\base\Application ), )); } - - public function usageError($message) - { - - } } diff --git a/framework/console/BadUsageException.php b/framework/console/BadUsageException.php new file mode 100644 index 0000000..abf02b0 --- /dev/null +++ b/framework/console/BadUsageException.php @@ -0,0 +1,33 @@ + + * @since 2.0 + */ +class BadUsageException extends \yii\base\Exception +{ + /** + * @var boolean whether this exception is caused by end user's mistake (e.g. wrong URL) + */ + public $causedByUser = true; + + /** + * @return string the user-friendly name of this exception + */ + public function getName() + { + return \Yii::t('yii', 'Bad Usage'); + } +} + diff --git a/framework/console/Controller.php b/framework/console/Controller.php index 16968f2..0880c44 100644 --- a/framework/console/Controller.php +++ b/framework/console/Controller.php @@ -11,7 +11,6 @@ namespace yii\console; use Yii; use yii\base\Action; -use yii\base\InvalidRequestException; use yii\base\InvalidRouteException; /** @@ -70,16 +69,16 @@ class Controller extends \yii\base\Controller * @param Action $action the currently requested action * @param array $missingParams the names of the missing parameters * @param array $unknownParams the unknown parameters (name=>value) - * @throws InvalidRequestException if there are missing or unknown parameters + * @throws BadUsageException if there are missing or unknown parameters */ public function validateActionParams($action, $missingParams, $unknownParams) { if (!empty($missingParams)) { - throw new InvalidRequestException(Yii::t('yii', 'Missing required options: {params}', array( + throw new BadUsageException(Yii::t('yii', 'Missing required options: {params}', array( '{params}' => implode(', ', $missingParams), ))); } elseif (!empty($unknownParams)) { - throw new InvalidRequestException(Yii::t('yii', 'Unknown options: {params}', array( + throw new BadUsageException(Yii::t('yii', 'Unknown options: {params}', array( '{params}' => implode(', ', $unknownParams), ))); } @@ -103,12 +102,6 @@ class Controller extends \yii\base\Controller } } - public function usageError($message) - { - echo "\nError: $message\n"; - Yii::$application->end(1); - } - public function globalOptions() { return array(); diff --git a/framework/console/controllers/HelpController.php b/framework/console/controllers/HelpController.php index 6e4b397..d1dc697 100644 --- a/framework/console/controllers/HelpController.php +++ b/framework/console/controllers/HelpController.php @@ -9,7 +9,9 @@ namespace yii\console\controllers; +use Yii; use yii\base\Application; +use yii\console\BadUsageException; use yii\base\InlineAction; use yii\console\Controller; use yii\util\StringHelper; @@ -47,27 +49,28 @@ class HelpController extends Controller * @param array $args additional anonymous command line arguments. * You may provide a command name to display its detailed information. * @return integer the exit status + * @throws BadUsageException if the command for help is unknown */ public function actionIndex($args = array()) { if (empty($args)) { - $status = $this->getHelp(); + $this->getHelp(); } else { - $result = \Yii::$application->createController($args[0]); + $result = Yii::$application->createController($args[0]); if ($result === false) { - echo "Error: no help for unknown command \"{$args[0]}\".\n"; - return 1; + throw new BadUsageException(Yii::t('yii', 'No help for unknown command "{command}".', array( + '{command}' => $args[0], + ))); } list($controller, $actionID) = $result; if ($actionID === '') { - $status = $this->getControllerHelp($controller); + $this->getControllerHelp($controller); } else { - $status = $this->getActionHelp($controller, $actionID); + $this->getActionHelp($controller, $actionID); } } - return $status; } /** @@ -76,7 +79,7 @@ class HelpController extends Controller */ public function getCommands() { - $commands = $this->getModuleCommands(\Yii::$application); + $commands = $this->getModuleCommands(Yii::$application); sort($commands); return array_unique($commands); } @@ -91,7 +94,6 @@ class HelpController extends Controller $actions = array_keys($controller->actions()); $class = new \ReflectionClass($controller); foreach ($class->getMethods() as $method) { - /** @var $method \ReflectionMethod */ $name = $method->getName(); if ($method->isPublic() && !$method->isStatic() && strpos($name, 'action') === 0 && $name !== 'actions') { $actions[] = StringHelper::camel2id(substr($name, 6)); @@ -136,7 +138,6 @@ class HelpController extends Controller /** * Displays all available commands. - * @return integer the exit status */ protected function getHelp() { @@ -152,13 +153,11 @@ class HelpController extends Controller } else { echo "\nNo commands are found.\n"; } - return 0; } /** * Displays the overall information of the command. * @param Controller $controller the controller instance - * @return integer the exit status */ protected function getControllerHelp($controller) { @@ -199,22 +198,21 @@ class HelpController extends Controller } echo "\n"; } - - return 0; } /** * Displays the detailed information of a command action. * @param Controller $controller the controller instance * @param string $actionID action ID - * @return integer the exit status + * @throws BadUsageException if the action does not exist */ protected function getActionHelp($controller, $actionID) { $action = $controller->createAction($actionID); if ($action === null) { - echo 'Error: no help for unknown sub-command "' . $controller->getUniqueId() . "/$actionID\".\n"; - return 1; + throw new BadUsageException(Yii::t('yii', 'No help for unknown sub-command "{command}".', array( + '{command}' => $controller->getUniqueId() . "/$actionID", + ))); } if ($action instanceof InlineAction) { $method = new \ReflectionMethod($controller, 'action' . $action->id); @@ -245,8 +243,6 @@ class HelpController extends Controller } echo "\n"; } - - return 0; } /** From b2662c0cd9f1f97c822b6a05d03ae117c2dcdeaf Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Sat, 2 Feb 2013 23:36:20 -0500 Subject: [PATCH 003/117] clean up help command. --- framework/base/Action.php | 9 + framework/base/Controller.php | 2 +- framework/console/Application.php | 8 +- framework/console/BadUsageException.php | 33 --- framework/console/Controller.php | 29 ++- framework/console/Exception.php | 33 +++ framework/console/Request.php | 6 +- framework/console/controllers/HelpController.php | 256 ++++++++++++--------- .../console/controllers/MigrateController.php | 116 +++++++--- 9 files changed, 289 insertions(+), 203 deletions(-) delete mode 100644 framework/console/BadUsageException.php create mode 100644 framework/console/Exception.php diff --git a/framework/base/Action.php b/framework/base/Action.php index 8d4ec5a..a852e0b 100644 --- a/framework/base/Action.php +++ b/framework/base/Action.php @@ -56,6 +56,15 @@ class Action extends Component } /** + * Returns the unique ID of this action among the whole application. + * @return string the unique ID of this action among the whole application. + */ + public function getUniqueId() + { + return $this->controller->getUniqueId() . '/' . $this->id; + } + + /** * Runs this action with the specified parameters. * This method is mainly invoked by the controller. * @param array $params the parameters to be bound to the action's run() method. diff --git a/framework/base/Controller.php b/framework/base/Controller.php index 804b339..3cbee77 100644 --- a/framework/base/Controller.php +++ b/framework/base/Controller.php @@ -250,7 +250,7 @@ class Controller extends Component */ public function getRoute() { - return $this->action !== null ? $this->getUniqueId() . '/' . $this->action->id : $this->getUniqueId(); + return $this->action !== null ? $this->action->getUniqueId() : $this->getUniqueId(); } /** diff --git a/framework/console/Application.php b/framework/console/Application.php index 47c3142..5f86cac 100644 --- a/framework/console/Application.php +++ b/framework/console/Application.php @@ -9,7 +9,6 @@ namespace yii\console; -use yii\base\Exception; use yii\base\InvalidRouteException; /** @@ -85,6 +84,7 @@ class Application extends \yii\base\Application * Processes the request. * The request is represented in terms of a controller route and action parameters. * @return integer the exit status of the controller action (0 means normal, non-zero values mean abnormal) + * @throws Exception if the script is not running from the command line */ public function processRequest() { @@ -93,7 +93,7 @@ class Application extends \yii\base\Application if ($request->getIsConsoleRequest()) { return $this->runAction($request->route, $request->params); } else { - throw new BadUsageException(\Yii::t('yii', 'this script must be run from the command line.')); + throw new Exception(\Yii::t('yii', 'this script must be run from the command line.')); } } @@ -105,14 +105,14 @@ class Application extends \yii\base\Application * @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 BadUsageException if the route is invalid + * @throws Exception if the route is invalid */ public function runAction($route, $params = array()) { try { return parent::runAction($route, $params); } catch (InvalidRouteException $e) { - throw new BadUsageException(\Yii::t('yii', 'Unknown command "{command}".', array('{command}' => $route))); + throw new Exception(\Yii::t('yii', 'Unknown command "{command}".', array('{command}' => $route))); } } diff --git a/framework/console/BadUsageException.php b/framework/console/BadUsageException.php deleted file mode 100644 index abf02b0..0000000 --- a/framework/console/BadUsageException.php +++ /dev/null @@ -1,33 +0,0 @@ - - * @since 2.0 - */ -class BadUsageException extends \yii\base\Exception -{ - /** - * @var boolean whether this exception is caused by end user's mistake (e.g. wrong URL) - */ - public $causedByUser = true; - - /** - * @return string the user-friendly name of this exception - */ - public function getName() - { - return \Yii::t('yii', 'Bad Usage'); - } -} - diff --git a/framework/console/Controller.php b/framework/console/Controller.php index 0880c44..faab435 100644 --- a/framework/console/Controller.php +++ b/framework/console/Controller.php @@ -48,14 +48,11 @@ class Controller extends \yii\base\Controller public function runAction($id, $params = array()) { if ($params !== array()) { - $class = new \ReflectionClass($this); + $options = $this->globalOptions(); foreach ($params as $name => $value) { - if ($class->hasProperty($name)) { - $property = $class->getProperty($name); - if ($property->isPublic() && !$property->isStatic() && $property->getDeclaringClass()->getName() === get_class($this)) { - $this->$name = $value; - unset($params[$name]); - } + if (in_array($name, $options, true)) { + $this->$name = $value; + unset($params[$name]); } } } @@ -69,16 +66,19 @@ class Controller extends \yii\base\Controller * @param Action $action the currently requested action * @param array $missingParams the names of the missing parameters * @param array $unknownParams the unknown parameters (name=>value) - * @throws BadUsageException if there are missing or unknown parameters + * @throws Exception if there are missing or unknown parameters */ public function validateActionParams($action, $missingParams, $unknownParams) { + unset($missingParams[Request::ANONYMOUS_PARAMS], $unknownParams[Request::ANONYMOUS_PARAMS]); + if (!empty($missingParams)) { - throw new BadUsageException(Yii::t('yii', 'Missing required options: {params}', array( + throw new Exception(Yii::t('yii', 'Missing required options: {params}', array( '{params}' => implode(', ', $missingParams), ))); - } elseif (!empty($unknownParams)) { - throw new BadUsageException(Yii::t('yii', 'Unknown options: {params}', array( + } + if (!empty($unknownParams)) { + throw new Exception(Yii::t('yii', 'Unknown options: {params}', array( '{params}' => implode(', ', $unknownParams), ))); } @@ -102,6 +102,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 + * name is the option name. + * Child classes may override this method to specify possible global options. + * @return array the names of the global options for this command. + */ public function globalOptions() { return array(); diff --git a/framework/console/Exception.php b/framework/console/Exception.php new file mode 100644 index 0000000..0ed5a42 --- /dev/null +++ b/framework/console/Exception.php @@ -0,0 +1,33 @@ + + * @since 2.0 + */ +class Exception extends \yii\base\Exception +{ + /** + * @var boolean whether this exception is caused by end user's mistake (e.g. wrong URL) + */ + public $causedByUser = true; + + /** + * @return string the user-friendly name of this exception + */ + public function getName() + { + return \Yii::t('yii', 'Error'); + } +} + diff --git a/framework/console/Request.php b/framework/console/Request.php index dbf80ba..4976fd2 100644 --- a/framework/console/Request.php +++ b/framework/console/Request.php @@ -15,6 +15,8 @@ namespace yii\console; */ class Request extends \yii\base\Request { + const ANONYMOUS_PARAMS = 'args'; + /** * @var string the controller route specified by this request. If this is an empty string, * it means the [[Application::defaultRoute|default route]] will be used. @@ -50,13 +52,13 @@ class Request extends \yii\base\Request $this->route = ''; } - $this->params = array(); + $this->params = array(self::ANONYMOUS_PARAMS => array()); foreach ($rawParams as $param) { if (preg_match('/^--(\w+)(=(.*))?$/', $param, $matches)) { $name = $matches[1]; $this->params[$name] = isset($matches[3]) ? $matches[3] : true; } else { - $this->params['args'][] = $param; + $this->params[self::ANONYMOUS_PARAMS][] = $param; } } } diff --git a/framework/console/controllers/HelpController.php b/framework/console/controllers/HelpController.php index d1dc697..2d86285 100644 --- a/framework/console/controllers/HelpController.php +++ b/framework/console/controllers/HelpController.php @@ -11,9 +11,10 @@ namespace yii\console\controllers; use Yii; use yii\base\Application; -use yii\console\BadUsageException; +use yii\console\Exception; use yii\base\InlineAction; use yii\console\Controller; +use yii\console\Request; use yii\util\StringHelper; /** @@ -46,19 +47,17 @@ class HelpController extends Controller * yiic help message # display help info about "message" * ~~~ * - * @param array $args additional anonymous command line arguments. - * You may provide a command name to display its detailed information. + * @param array $args The name of the command to show help about. + * If not provided, all available commands will be displayed. * @return integer the exit status - * @throws BadUsageException if the command for help is unknown + * @throws Exception if the command for help is unknown */ public function actionIndex($args = array()) { - if (empty($args)) { - $this->getHelp(); - } else { + if (isset($args[0])) { $result = Yii::$application->createController($args[0]); if ($result === false) { - throw new BadUsageException(Yii::t('yii', 'No help for unknown command "{command}".', array( + throw new Exception(Yii::t('yii', 'No help for unknown command "{command}".', array( '{command}' => $args[0], ))); } @@ -70,6 +69,8 @@ class HelpController extends Controller } else { $this->getActionHelp($controller, $actionID); } + } else { + $this->getHelp(); } } @@ -143,7 +144,7 @@ class HelpController extends Controller { $commands = $this->getCommands(); if ($commands !== array()) { - echo "\nUsage: yiic [...options...]\n\n"; + echo "Usage: yiic [...options...] [...arguments...]\n\n"; echo "The following commands are available:\n\n"; foreach ($commands as $command) { echo " * $command\n"; @@ -168,35 +169,23 @@ class HelpController extends Controller } if ($comment !== '') { - echo "\n" . $comment . "\n"; - } - - $options = $this->getGlobalOptions($class, $controller); - if ($options !== array()) { - echo "\nGLOBAL OPTIONS"; - echo "\n--------------\n\n"; - foreach ($options as $name => $description) { - echo " --$name"; - if ($description != '') { - echo ": $description\n"; - } - echo "\n"; - } + echo "\nDESCRIPTION\n"; + echo "\n" . $comment . "\n\n"; } $actions = $this->getActions($controller); if ($actions !== array()) { - echo "\nSUB-COMMANDS"; - echo "\n------------\n\n"; + echo "\nSUB-COMMANDS\n\n"; $prefix = $controller->getUniqueId(); foreach ($actions as $action) { if ($controller->defaultAction === $action) { - echo " * $prefix (default)\n"; + echo " * $prefix/$action (or $prefix)\n"; } else { echo " * $prefix/$action\n"; } } - echo "\n"; + echo "\nTo see the help of each sub-command, enter:\n"; + echo "\n yiic help \n\n"; } } @@ -204,45 +193,78 @@ class HelpController extends Controller * Displays the detailed information of a command action. * @param Controller $controller the controller instance * @param string $actionID action ID - * @throws BadUsageException if the action does not exist + * @throws Exception if the action does not exist */ protected function getActionHelp($controller, $actionID) { $action = $controller->createAction($actionID); if ($action === null) { - throw new BadUsageException(Yii::t('yii', 'No help for unknown sub-command "{command}".', array( - '{command}' => $controller->getUniqueId() . "/$actionID", + throw new Exception(Yii::t('yii', 'No help for unknown sub-command "{command}".', array( + '{command}' => $action->getUniqueId(), ))); } if ($action instanceof InlineAction) { - $method = new \ReflectionMethod($controller, 'action' . $action->id); + $method = new \ReflectionMethod($controller, $action->actionMethod); } else { $method = new \ReflectionMethod($action, 'run'); } - $comment = strtr(trim(preg_replace('/^\s*\**( |\t)?/m', '', trim($method->getDocComment(), '/'))), "\r", ''); - if (preg_match('/^\s*@\w+/m', $comment, $matches, PREG_OFFSET_CAPTURE)) { - $meta = substr($comment, $matches[0][1]); - $comment = trim(substr($comment, 0, $matches[0][1])); + + $tags = $this->parseComment($method->getDocComment()); + $options = $this->getOptions($method, isset($tags['param']) ? $tags['param'] : array()); + $globalOptions = $this->getGlobalOptions($controller); + $options = array_merge($options, $globalOptions); + + echo "\nUSAGE\n\n"; + if ($action->id === $controller->defaultAction) { + echo 'yiic ' . $controller->getUniqueId(); } else { - $meta = ''; + echo "yiic " . $action->getUniqueId(); + } + if (isset($options[Request::ANONYMOUS_PARAMS])) { + if (count($options) > 1) { + echo ' [...options...]'; + } + echo " [...arguments...]"; + } elseif (count($options)) { + echo " [...options...]"; } + echo "\n\n"; - if ($comment !== '') { - echo "\n" . $comment . "\n"; + if ($tags['description'] !== '') { + echo "\nDESCRIPTION"; + echo "\n\n" . $tags['description'] . "\n\n"; + } + + if (isset($options[Request::ANONYMOUS_PARAMS])) { + echo "\nARGUMENTS\n\n"; + echo $options[Request::ANONYMOUS_PARAMS] . "\n\n"; + unset($options[Request::ANONYMOUS_PARAMS]); } - $options = $this->getOptions($method, $meta); if ($options !== array()) { - echo "\nOPTIONS"; - echo "\n-------\n\n"; - foreach ($options as $name => $description) { - echo " --$name"; - if ($description != '') { - echo ": $description\n"; + echo "\nOPTIONS\n\n"; + echo implode("\n\n", $options) . "\n\n"; + } + } + + function parseComment($comment) + { + $tags = array(); + $comment = "@description \n" . strtr(trim(preg_replace('/^\s*\**( |\t)?/m', '', trim($comment, '/'))), "\r", ''); + $parts = preg_split('/^\s*@/m', $comment, -1, PREG_SPLIT_NO_EMPTY); + foreach ($parts as $part) { + if (preg_match('/^(\w+)(.*)/ms', trim($part), $matches)) { + $name = $matches[1]; + if (!isset($tags[$name])) { + $tags[$name] = trim($matches[2]); + } elseif (is_array($tags[$name])) { + $tags[$name][] = trim($matches[2]); + } else { + $tags[$name] = array($tags[$name], trim($matches[2])); } } - echo "\n"; } + return $tags; } /** @@ -250,93 +272,99 @@ class HelpController extends Controller * @param string $meta * @return array */ - protected function getOptions($method, $meta) + protected function getOptions($method, $tags) { + if (is_string($tags)) { + $tags = array($tags); + } $params = $method->getParameters(); - $tags = preg_split('/^\s*@/m', $meta, -1, PREG_SPLIT_NO_EMPTY); - $options = array(); - $count = 0; - foreach ($tags as $tag) { - $parts = preg_split('/\s+/', trim($tag), 2); - if ($parts[0] === 'param' && isset($params[$count])) { - $param = $params[$count]; - $comment = isset($parts[1]) ? $parts[1] : ''; - if (preg_match('/^([^\s]+)\s+(\$\w+\s+)?(.*)/s', $comment, $matches)) { - $type = $matches[1]; - $doc = $matches[3]; - } else { - $type = $comment; - $doc = ''; - } - $comment = $type === '' ? '' : ($type . ', '); - if ($param->isDefaultValueAvailable()) { - $value = $param->getDefaultValue(); - if (!is_array($value)) { - $comment .= 'optional (defaults to ' . var_export($value, true) . ').'; - } else { - $comment .= 'optional.'; - } - } else { - $comment .= 'required.'; - } - if (trim($doc) !== '') { - $comment .= "\n" . preg_replace("/^/m", " ", $doc); - } - $options[$param->getName()] = $comment; - $count++; + $optional = $required = array(); + foreach ($params as $i => $param) { + $name = $param->getName(); + $tag = isset($tags[$i]) ? $tags[$i] : ''; + if (preg_match('/^([^\s]+)\s+(\$\w+\s+)?(.*)/s', $tag, $matches)) { + $type = $matches[1]; + $comment = $matches[3]; + } else { + $type = null; + $comment = $tag; + } + if ($param->isDefaultValueAvailable()) { + $optional[$name] = $this->formatOptionHelp($name, false, $type, $param->getDefaultValue(), $comment); + } else { + $required[$name] = $this->formatOptionHelp($name, true, $type, null, $comment); } } - if ($count < count($params)) { - for ($i = $count; $i < count($params); ++$i) { - $options[$params[$i]->getName()] = ''; + + ksort($required); + ksort($optional); + + return array_merge($required, $optional); + } + + protected function formatOptionHelp($name, $required, $type, $defaultValue, $comment) + { + $doc = ''; + $comment = trim($comment); + + if ($name === Request::ANONYMOUS_PARAMS) { + return $comment; + } + + if ($defaultValue !== null && !is_array($defaultValue)) { + if ($type === null) { + $type = gettype($defaultValue); } + $doc = "$type (defaults to " . var_export($defaultValue, true) . ")"; + } elseif (trim($type) !== '') { + $doc = $type; } - ksort($options); - return $options; + if ($doc === '') { + $doc = $comment; + } elseif ($comment !== '') { + $doc .= "\n" . preg_replace("/^/m", " ", $comment); + } + + $name = $required ? "--$name (required)" : "--$name"; + return $doc === '' ? $name : "$name: $doc"; } /** - * @param \ReflectionClass $class * @param Controller $controller * @return array */ - protected function getGlobalOptions($class, $controller) + protected function getGlobalOptions($controller) { + $optionNames = $controller->globalOptions(); + if (empty($optionNames)) { + return array(); + } + + $class = new \ReflectionClass($controller); $options = array(); foreach ($class->getProperties() as $property) { - if (!$property->isPublic() || $property->isStatic() || $property->getDeclaringClass()->getName() !== get_class($controller)) { - continue; - } $name = $property->getName(); - $comment = strtr(trim(preg_replace('/^\s*\**( |\t)?/m', '', trim($property->getDocComment(), '/'))), "\r", ''); - if (preg_match('/^\s*@\w+/m', $comment, $matches, PREG_OFFSET_CAPTURE)) { - $meta = substr($comment, $matches[0][1]); - } else { - $meta = ''; + if (!in_array($name, $optionNames, true)) { + continue; } - $tags = preg_split('/^\s*@/m', $meta, -1, PREG_SPLIT_NO_EMPTY); - foreach ($tags as $tag) { - $parts = preg_split('/\s+/', trim($tag), 2); - $comment = isset($parts[1]) ? $parts[1] : ''; - if ($parts[0] === 'var' || $parts[0] === 'property') { - if (preg_match('/^([^\s]+)(\s+.*)?/s', $comment, $matches)) { - $type = $matches[1]; - $doc = trim($matches[2]); - } else { - $type = $comment; - $doc = ''; - } - $comment = $type === '' ? '' : ($type); - if (trim($doc) !== '') { - $comment .= ', ' . preg_replace("/^/m", "", $doc); - } - $options[$name] = $comment; - break; + $defaultValue = $property->getValue($controller); + $tags = $this->parseComment($property->getDocComment()); + if (isset($tags['var']) || isset($tags['property'])) { + $doc = isset($tags['var']) ? $tags['var'] : $tags['property']; + if (is_array($doc)) { + $doc = reset($doc); } - } - if (!isset($options[$name])) { - $options[$name] = ''; + if (preg_match('/^([^\s]+)(.*)/s', $doc, $matches)) { + $type = $matches[1]; + $comment = $matches[2]; + } else { + $type = null; + $comment = $doc; + } + $options[$name] = $this->formatOptionHelp($name, false, $type, $defaultValue, $comment); + } else { + $options[$name] = $this->formatOptionHelp($name, false, null, $defaultValue, ''); } } ksort($options); diff --git a/framework/console/controllers/MigrateController.php b/framework/console/controllers/MigrateController.php index e104856..b8076fc 100644 --- a/framework/console/controllers/MigrateController.php +++ b/framework/console/controllers/MigrateController.php @@ -11,22 +11,20 @@ namespace yii\console\controllers; use Yii; +use yii\console\Exception; use yii\console\Controller; +use yii\db\Connection; /** * This command provides support for database migrations. * - * The implementation of this command and other supporting classes referenced - * the yii-dbmigrations extension ((https://github.com/pieterclaerhout/yii-dbmigrations), - * authored by Pieter Claerhout. - * * EXAMPLES * * - yiic migrate * Applies ALL new migrations. This is equivalent to 'yiic migrate up'. * * - yiic migrate create create_user_table - * Creates a new migration named 'create_user_table'. + * Creates a new migration named 'create_user_table'. * * - yiic migrate up 3 * Applies the next 3 new migrations. @@ -45,69 +43,86 @@ use yii\console\Controller; * No actual migration will be performed. * * - yiic migrate history - * Shows all previously applied migration information. + * Shows all previously applied migration information. * * - yiic migrate history 10 - * Shows the last 10 applied migrations. + * Shows the last 10 applied migrations. * * - yiic migrate new - * Shows all new migrations. + * Shows all new migrations. * * - yiic migrate new 10 - * Shows the next 10 migrations that have not been applied. + * Shows the next 10 migrations that have not been applied. * * @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 directory that stores the migrations. This must be specified - * in terms of a path alias, and the corresponding directory must exist. - * Defaults to 'application.migrations' (meaning 'protected/migrations'). + * @var string the directory that stores the migrations. This can be either a path alias + * or a directory. Defaults to '@application/migrations'. */ public $migrationPath = '@application/migrations'; /** * @var string the name of the table for keeping applied migration information. - * This table will be automatically created if not exists. Defaults to 'tbl_migration'. - * The table structure is: (version varchar(255) primary key, apply_time integer) + * This table will be automatically created if not exists. + * The table structure is as follows: + * + * ~~~ + * CREATE TABLE tbl_migration ( + * version varchar(255) PRIMARY KEY, + * apply_time integer + * ) + * ~~~ */ public $migrationTable = 'tbl_migration'; /** - * @var string the application component ID that specifies the database connection for - * storing migration information. Defaults to 'db'. + * @var string the component ID that specifies the database connection for + * storing migration information. */ public $connectionID = 'db'; /** - * @var string the path of the template file for generating new migrations. This - * must be specified in terms of a path alias (e.g. application.migrations.template). - * If not set, an internal template will be used. + * @var string the path of the template file for generating new migrations. + * This can be either a path alias (e.g. "@application/migrations/template.php") + * or a file path. If not set, an internal template will be used. */ public $templateFile; /** - * @var string the default command action. It defaults to 'up'. + * @var string the default command action. */ public $defaultAction = 'up'; /** - * @var boolean whether to execute the migration in an interactive mode. Defaults to true. - * Set this to false when performing migration in a cron job or background process. + * @var boolean whether to execute the migration in an interactive mode. */ public $interactive = true; - + 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 ($path === false || !is_dir($path)) { - echo 'Error: the migration directory does not exist "' . $this->migrationPath . "\"\n"; - return false; + throw new Exception("The migration directory \"{$this->migrationPath}\" does not exist."); } $this->migrationPath = $path; $version = Yii::getVersion(); - echo "\nYii Migration Tool v2.0 (based on Yii v{$version})\n\n"; + echo "\nYii Migration Tool (based on Yii v{$version})\n\n"; return true; } else { return false; @@ -115,7 +130,15 @@ class MigrateController extends Controller } /** - * @param array $args + * 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 array $args the number of new migrations to be applied. If not provided, + * all new migrations will be applied. */ public function actionUp($args) { @@ -145,7 +168,7 @@ class MigrateController extends Controller if ($this->confirm('Apply the above ' . ($n === 1 ? 'migration' : 'migrations') . "?")) { foreach ($migrations as $migration) { if ($this->migrateUp($migration) === false) { - echo "\nMigration failed. All later migrations are canceled.\n"; + echo "\nMigration failed. The rest of the new migrations are canceled.\n"; return; } } @@ -153,11 +176,23 @@ class MigrateController extends Controller } } + /** + * 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 array $args the number of migrations to be reverted. If not provided, + * the last applied migration will be reverted. + * @throws Exception if the number of the steps is less than 1. + */ public function actionDown($args) { $step = isset($args[0]) ? (int)$args[0] : 1; if ($step < 1) { - die("Error: The step parameter must be greater than 0.\n"); + throw new Exception("The step parameter must be greater than 0."); } if (($migrations = $this->getMigrationHistory($step)) === array()) { @@ -226,14 +261,14 @@ class MigrateController extends Controller if (isset($args[0])) { $version = $args[0]; } else { - $this->usageError('Please specify which version to migrate to.'); + throw new Exception("Please specify which version to migrate to."); } $originalVersion = $version; if (preg_match('/^m?(\d{6}_\d{6})(_.*?)?$/', $version, $matches)) { $version = 'm' . $matches[1]; } else { - die("Error: The version option must be either a timestamp (e.g. 101129_185401)\nor the full name of a migration (e.g. m101129_185401_create_user_table).\n"); + throw new Exception("The version option 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 @@ -258,7 +293,7 @@ class MigrateController extends Controller } } - die("Error: Unable to find the version '$originalVersion'.\n"); + throw new Exception("Unable to find the version '$originalVersion'."); } public function actionMark($args) @@ -266,13 +301,13 @@ class MigrateController extends Controller if (isset($args[0])) { $version = $args[0]; } else { - $this->usageError('Please specify which version to mark to.'); + throw new Exception('Please specify which version to mark to.'); } $originalVersion = $version; if (preg_match('/^m?(\d{6}_\d{6})(_.*?)?$/', $version, $matches)) { $version = 'm' . $matches[1]; } else { - die("Error: The version option must be either a timestamp (e.g. 101129_185401)\nor the full name of a migration (e.g. m101129_185401_create_user_table).\n"); + throw new Exception("Error: The version option must be either a timestamp (e.g. 101129_185401)\nor the full name of a migration (e.g. m101129_185401_create_user_table)."); } $db = $this->getDb(); @@ -357,12 +392,17 @@ class MigrateController extends Controller } } + /** + * Creates a new migration. + * @param array $args the name of the new migration. + * @throws Exception if the name of the new migration is not provided + */ public function actionCreate($args) { if (isset($args[0])) { $name = $args[0]; } else { - $this->usageError('Please provide the name of the new migration.'); + throw new Exception('Please provide the name of the new migration.'); } if (!preg_match('/^\w+$/', $name)) { @@ -428,7 +468,7 @@ class MigrateController extends Controller $file = $this->migrationPath . DIRECTORY_SEPARATOR . $class . '.php'; require_once($file); $migration = new $class; - $migration->setDb($this->getDb()); + $migration->db = $this->getDb(); return $migration; } @@ -445,7 +485,7 @@ class MigrateController extends Controller if (($this->_db = Yii::$application->getComponent($this->connectionID)) instanceof CDbConnection) { return $this->_db; } else { - die("Error: CMigrationCommand.connectionID '{$this->connectionID}' is invalid. Please make sure it refers to the ID of a CDbConnection application component.\n"); + throw new Exception("Invalid DB connection: {$this->connectionID}."); } } } @@ -505,7 +545,7 @@ class MigrateController extends Controller protected function getTemplate() { if ($this->templateFile !== null) { - return file_get_contents(Yii::getPathOfAlias($this->templateFile) . '.php'); + return file_get_contents(Yii::getAlias($this->templateFile)); } else { return << Date: Sun, 3 Feb 2013 09:52:10 -0500 Subject: [PATCH 004/117] help cleanup --- framework/console/controllers/HelpController.php | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/framework/console/controllers/HelpController.php b/framework/console/controllers/HelpController.php index 2d86285..d2e50b8 100644 --- a/framework/console/controllers/HelpController.php +++ b/framework/console/controllers/HelpController.php @@ -178,14 +178,10 @@ class HelpController extends Controller echo "\nSUB-COMMANDS\n\n"; $prefix = $controller->getUniqueId(); foreach ($actions as $action) { - if ($controller->defaultAction === $action) { - echo " * $prefix/$action (or $prefix)\n"; - } else { - echo " * $prefix/$action\n"; - } + echo "* $prefix/$action\n"; } - echo "\nTo see the help of each sub-command, enter:\n"; - echo "\n yiic help \n\n"; + echo "\n\nTo see the help of each sub-command, enter:\n"; + echo "\n yiic help \n\n"; } } From 6a595de4be7347d2937f027a17881eec8525cccf Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Sun, 3 Feb 2013 13:54:27 -0500 Subject: [PATCH 005/117] refactored MVC. --- framework/base/Action.php | 33 +--- framework/base/Controller.php | 42 +++++ framework/base/InlineAction.php | 5 +- framework/console/Controller.php | 63 ++++++-- framework/console/controllers/HelpController.php | 191 ++++++++++++----------- 5 files changed, 194 insertions(+), 140 deletions(-) diff --git a/framework/base/Action.php b/framework/base/Action.php index a852e0b..f72aa1b 100644 --- a/framework/base/Action.php +++ b/framework/base/Action.php @@ -76,36 +76,7 @@ class Action extends Component if (!method_exists($this, 'run')) { throw new InvalidConfigException(get_class($this) . ' must define a "run()" method.'); } - $method = new \ReflectionMethod($this, 'run'); - $args = $this->bindActionParams($method, $params); - return (int)$method->invokeArgs($this, $args); - } - - /** - * Binds the given parameters to the action method. - * The returned array contains the parameters that need to be passed to the action method. - * This method calls [[Controller::validateActionParams()]] to check if any exception - * should be raised if there are missing or unknown parameters. - * @param \ReflectionMethod $method the action method reflection object - * @param array $params the supplied parameters - * @return array the parameters that can be passed to the action method - */ - protected function bindActionParams($method, $params) - { - $args = array(); - $missing = array(); - foreach ($method->getParameters() as $param) { - $name = $param->getName(); - if (array_key_exists($name, $params)) { - $args[] = $params[$name]; - unset($params[$name]); - } elseif ($param->isDefaultValueAvailable()) { - $args[] = $param->getDefaultValue(); - } else { - $missing[] = $name; - } - } - $this->controller->validateActionParams($this, $missing, $params); - return $args; + $args = $this->controller->bindActionParams($this, $params); + return (int)call_user_func_array(array($this, 'run'), $args); } } diff --git a/framework/base/Controller.php b/framework/base/Controller.php index 3cbee77..b9d2916 100644 --- a/framework/base/Controller.php +++ b/framework/base/Controller.php @@ -144,6 +144,48 @@ class Controller extends Component } /** + * Binds the parameters to the action. + * This method is invoked by [[Action]] when it begins to run with the given parameters. + * This method will check the parameter names that the action requires and return + * the provided parameters according to the requirement. If there is any missing parameter, + * an exception will be thrown. + * @param Action $action the action to be bound with parameters + * @param array $params the parameters to be bound to the action + * @return array the valid parameters that the action can run with. + * @throws InvalidRequestException if there are missing parameters. + */ + public function bindActionParams($action, $params) + { + if ($action instanceof InlineAction) { + $method = new \ReflectionMethod($this, $action->actionMethod); + } else { + $method = new \ReflectionMethod($action, 'run'); + } + + $args = array(); + $missing = array(); + foreach ($method->getParameters() as $param) { + $name = $param->getName(); + if (array_key_exists($name, $params)) { + $args[] = $params[$name]; + unset($params[$name]); + } elseif ($param->isDefaultValueAvailable()) { + $args[] = $param->getDefaultValue(); + } else { + $missing[] = $name; + } + } + + if ($missing !== array()) { + throw new InvalidRequestException(Yii::t('yii', 'Missing required parameters: {params}', array( + '{params}' => implode(', ', $missing), + ))); + } + + return $args; + } + + /** * Forwards the current execution flow to handle a new request specified by a route. * The only difference between this method and [[run()]] is that after calling this method, * the application will exit. diff --git a/framework/base/InlineAction.php b/framework/base/InlineAction.php index 4cd5413..c315675 100644 --- a/framework/base/InlineAction.php +++ b/framework/base/InlineAction.php @@ -45,8 +45,7 @@ class InlineAction extends Action */ public function runWithParams($params) { - $method = new \ReflectionMethod($this->controller, $this->actionMethod); - $args = $this->bindActionParams($method, $params); - return (int)$method->invokeArgs($this->controller, $args); + $args = $this->controller->bindActionParams($this, $params); + return (int)call_user_func_array(array($this->controller, $this->actionMethod), $args); } } diff --git a/framework/console/Controller.php b/framework/console/Controller.php index faab435..f51588f 100644 --- a/framework/console/Controller.php +++ b/framework/console/Controller.php @@ -11,6 +11,7 @@ namespace yii\console; use Yii; use yii\base\Action; +use yii\base\InlineAction; use yii\base\InvalidRouteException; /** @@ -60,28 +61,60 @@ class Controller extends \yii\base\Controller } /** - * Validates the parameter being bound to actions. - * This method is invoked when parameters are being bound to the currently requested action. - * Child classes may override this method to throw exceptions when there are missing and/or unknown parameters. - * @param Action $action the currently requested action - * @param array $missingParams the names of the missing parameters - * @param array $unknownParams the unknown parameters (name=>value) - * @throws Exception if there are missing or unknown parameters + * Binds the parameters to the action. + * This method is invoked by [[Action]] when it begins to run with the given parameters. + * This method will first bind the parameters with the [[globalOptions()|global options]] + * available to the action. It then validates the given arguments. + * @param Action $action the action to be bound with parameters + * @param array $params the parameters to be bound to the action + * @return array the valid parameters that the action can run with. + * @throws Exception if there are unknown options or missing arguments */ - public function validateActionParams($action, $missingParams, $unknownParams) + public function bindActionParams($action, $params) { - unset($missingParams[Request::ANONYMOUS_PARAMS], $unknownParams[Request::ANONYMOUS_PARAMS]); + if ($params !== array()) { + $options = $this->globalOptions(); + foreach ($params as $name => $value) { + if (in_array($name, $options, true)) { + $this->$name = $value; + unset($params[$name]); + } + } + } - if (!empty($missingParams)) { - throw new Exception(Yii::t('yii', 'Missing required options: {params}', array( - '{params}' => implode(', ', $missingParams), + $args = isset($params[Request::ANONYMOUS_PARAMS]) ? $params[Request::ANONYMOUS_PARAMS] : array(); + unset($params[Request::ANONYMOUS_PARAMS]); + if ($params !== array()) { + throw new Exception(Yii::t('yii', 'Unknown options: {params}', array( + '{params}' => implode(', ', array_keys($params)), ))); } - if (!empty($unknownParams)) { - throw new Exception(Yii::t('yii', 'Unknown options: {params}', array( - '{params}' => implode(', ', $unknownParams), + + if ($action instanceof InlineAction) { + $method = new \ReflectionMethod($this, $action->actionMethod); + } else { + $method = new \ReflectionMethod($action, 'run'); + } + + $missing = array(); + foreach ($method->getParameters() as $i => $param) { + $name = $param->getName(); + if (!isset($args[$i])) { + if ($param->isDefaultValueAvailable()) { + $args[$i] = $param->getDefaultValue(); + } else { + $missing[] = $name; + } + } + } + + if ($missing !== array()) { + throw new Exception(Yii::t('yii', 'Missing required arguments: {params}', array( + '{params}' => implode(', ', $missing), ))); } + + return $args; } /** diff --git a/framework/console/controllers/HelpController.php b/framework/console/controllers/HelpController.php index d2e50b8..3cd6a07 100644 --- a/framework/console/controllers/HelpController.php +++ b/framework/console/controllers/HelpController.php @@ -47,27 +47,28 @@ class HelpController extends Controller * yiic help message # display help info about "message" * ~~~ * - * @param array $args The name of the command to show help about. + * @param string $command The name of the command to show help about. * If not provided, all available commands will be displayed. * @return integer the exit status * @throws Exception if the command for help is unknown */ - public function actionIndex($args = array()) + public function actionIndex($command) { - if (isset($args[0])) { - $result = Yii::$application->createController($args[0]); + if ($command !== null) { + $result = Yii::$application->createController($command); if ($result === false) { throw new Exception(Yii::t('yii', 'No help for unknown command "{command}".', array( - '{command}' => $args[0], + '{command}' => $command, ))); } list($controller, $actionID) = $result; - if ($actionID === '') { - $this->getControllerHelp($controller); - } else { + $actions = $this->getActions($controller); + if ($actionID !== '' || count($actions) === 1 && $actions[0] === $controller->defaultAction) { $this->getActionHelp($controller, $actionID); + } else { + $this->getControllerHelp($controller); } } else { $this->getHelp(); @@ -144,13 +145,12 @@ class HelpController extends Controller { $commands = $this->getCommands(); if ($commands !== array()) { - echo "Usage: yiic [...options...] [...arguments...]\n\n"; echo "The following commands are available:\n\n"; foreach ($commands as $command) { echo " * $command\n"; } echo "\nTo see the help of each command, enter:\n"; - echo "\n yiic help \n"; + echo "\n yiic help \n\n"; } else { echo "\nNo commands are found.\n"; } @@ -178,7 +178,11 @@ class HelpController extends Controller echo "\nSUB-COMMANDS\n\n"; $prefix = $controller->getUniqueId(); foreach ($actions as $action) { - echo "* $prefix/$action\n"; + if ($action === $controller->defaultAction) { + echo "* $prefix/$action (default)\n"; + } else { + echo "* $prefix/$action\n"; + } } echo "\n\nTo see the help of each sub-command, enter:\n"; echo "\n yiic help \n\n"; @@ -206,9 +210,11 @@ class HelpController extends Controller } $tags = $this->parseComment($method->getDocComment()); - $options = $this->getOptions($method, isset($tags['param']) ? $tags['param'] : array()); - $globalOptions = $this->getGlobalOptions($controller); - $options = array_merge($options, $globalOptions); + + if ($tags['description'] !== '') { + echo "\nDESCRIPTION"; + echo "\n\n" . $tags['description'] . "\n\n"; + } echo "\nUSAGE\n\n"; if ($action->id === $controller->defaultAction) { @@ -216,59 +222,34 @@ class HelpController extends Controller } else { echo "yiic " . $action->getUniqueId(); } - if (isset($options[Request::ANONYMOUS_PARAMS])) { - if (count($options) > 1) { - echo ' [...options...]'; - } - echo " [...arguments...]"; - } elseif (count($options)) { - echo " [...options...]"; + list ($required, $optional) = $this->getArgHelps($method, isset($tags['param']) ? $tags['param'] : array()); + if (!empty($required)) { + echo ' <' . implode('> <', array_keys($required)) . '>'; } - echo "\n\n"; - - if ($tags['description'] !== '') { - echo "\nDESCRIPTION"; - echo "\n\n" . $tags['description'] . "\n\n"; + if (!empty($optional)) { + echo ' [' . implode('] [', array_keys($optional)) . ']'; } + echo "\n\n"; - if (isset($options[Request::ANONYMOUS_PARAMS])) { + if (!empty($required) || !empty($optional)) { echo "\nARGUMENTS\n\n"; - echo $options[Request::ANONYMOUS_PARAMS] . "\n\n"; - unset($options[Request::ANONYMOUS_PARAMS]); + echo implode("\n\n", array_merge($required, $optional)) . "\n\n"; } + $options = $this->getOptionHelps($controller); if ($options !== array()) { echo "\nOPTIONS\n\n"; echo implode("\n\n", $options) . "\n\n"; } } - function parseComment($comment) - { - $tags = array(); - $comment = "@description \n" . strtr(trim(preg_replace('/^\s*\**( |\t)?/m', '', trim($comment, '/'))), "\r", ''); - $parts = preg_split('/^\s*@/m', $comment, -1, PREG_SPLIT_NO_EMPTY); - foreach ($parts as $part) { - if (preg_match('/^(\w+)(.*)/ms', trim($part), $matches)) { - $name = $matches[1]; - if (!isset($tags[$name])) { - $tags[$name] = trim($matches[2]); - } elseif (is_array($tags[$name])) { - $tags[$name][] = trim($matches[2]); - } else { - $tags[$name] = array($tags[$name], trim($matches[2])); - } - } - } - return $tags; - } - /** + * Returns the help information about arguments. * @param \ReflectionMethod $method - * @param string $meta - * @return array + * @param string $tags the parsed comment block related with arguments + * @return array the required and optional argument help information */ - protected function getOptions($method, $tags) + protected function getArgHelps($method, $tags) { if (is_string($tags)) { $tags = array($tags); @@ -286,51 +267,21 @@ class HelpController extends Controller $comment = $tag; } if ($param->isDefaultValueAvailable()) { - $optional[$name] = $this->formatOptionHelp($name, false, $type, $param->getDefaultValue(), $comment); + $optional[$name] = $this->formatOptionHelp('* ' . $name, false, $type, $param->getDefaultValue(), $comment); } else { - $required[$name] = $this->formatOptionHelp($name, true, $type, null, $comment); + $required[$name] = $this->formatOptionHelp('* ' . $name, true, $type, null, $comment); } } - ksort($required); - ksort($optional); - - return array_merge($required, $optional); - } - - protected function formatOptionHelp($name, $required, $type, $defaultValue, $comment) - { - $doc = ''; - $comment = trim($comment); - - if ($name === Request::ANONYMOUS_PARAMS) { - return $comment; - } - - if ($defaultValue !== null && !is_array($defaultValue)) { - if ($type === null) { - $type = gettype($defaultValue); - } - $doc = "$type (defaults to " . var_export($defaultValue, true) . ")"; - } elseif (trim($type) !== '') { - $doc = $type; - } - - if ($doc === '') { - $doc = $comment; - } elseif ($comment !== '') { - $doc .= "\n" . preg_replace("/^/m", " ", $comment); - } - - $name = $required ? "--$name (required)" : "--$name"; - return $doc === '' ? $name : "$name: $doc"; + return array($required, $optional); } /** - * @param Controller $controller - * @return array + * Returns the help information about the options available for a console controller. + * @param Controller $controller the console controller + * @return array the help information about the options */ - protected function getGlobalOptions($controller) + protected function getOptionHelps($controller) { $optionNames = $controller->globalOptions(); if (empty($optionNames)) { @@ -358,12 +309,70 @@ class HelpController extends Controller $type = null; $comment = $doc; } - $options[$name] = $this->formatOptionHelp($name, false, $type, $defaultValue, $comment); + $options[$name] = $this->formatOptionHelp('--' . $name, false, $type, $defaultValue, $comment); } else { - $options[$name] = $this->formatOptionHelp($name, false, null, $defaultValue, ''); + $options[$name] = $this->formatOptionHelp('--' . $name, false, null, $defaultValue, ''); } } ksort($options); return $options; } + + /** + * Parses the comment block into tags. + * @param string $comment the comment block + * @return array the parsed tags + */ + protected function parseComment($comment) + { + $tags = array(); + $comment = "@description \n" . strtr(trim(preg_replace('/^\s*\**( |\t)?/m', '', trim($comment, '/'))), "\r", ''); + $parts = preg_split('/^\s*@/m', $comment, -1, PREG_SPLIT_NO_EMPTY); + foreach ($parts as $part) { + if (preg_match('/^(\w+)(.*)/ms', trim($part), $matches)) { + $name = $matches[1]; + if (!isset($tags[$name])) { + $tags[$name] = trim($matches[2]); + } elseif (is_array($tags[$name])) { + $tags[$name][] = trim($matches[2]); + } else { + $tags[$name] = array($tags[$name], trim($matches[2])); + } + } + } + return $tags; + } + + /** + * Generates a well-formed string for an argument or option. + * @param string $name the name of the argument or option + * @param boolean $required whether the argument is required + * @param string $type the type of the option or argument + * @param mixed $defaultValue the default value of the option or argument + * @param string $comment comment about the option or argument + * @return string the formatted string for the argument or option + */ + protected function formatOptionHelp($name, $required, $type, $defaultValue, $comment) + { + $doc = ''; + $comment = trim($comment); + + if ($defaultValue !== null && !is_array($defaultValue)) { + if ($type === null) { + $type = gettype($defaultValue); + } + $doc = "$type (defaults to " . var_export($defaultValue, true) . ")"; + } elseif (trim($type) !== '') { + $doc = $type; + } + + if ($doc === '') { + $doc = $comment; + } elseif ($comment !== '') { + $doc .= "\n" . preg_replace("/^/m", " ", $comment); + } + + $name = $required ? "$name (required)" : $name; + return $doc === '' ? $name : "$name: $doc"; + } } \ No newline at end of file From af4455eb03a79c4c24a92771a6f842641ec2a8fe Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Sun, 3 Feb 2013 19:37:29 -0500 Subject: [PATCH 006/117] Finished console support. Finished migrate command. --- framework/base/Controller.php | 5 + framework/console/Request.php | 2 +- framework/console/controllers/HelpController.php | 51 ++- .../console/controllers/MigrateController.php | 438 ++++++++++++--------- framework/views/migration.php | 23 ++ 5 files changed, 331 insertions(+), 188 deletions(-) create mode 100644 framework/views/migration.php diff --git a/framework/base/Controller.php b/framework/base/Controller.php index b9d2916..f9cf399 100644 --- a/framework/base/Controller.php +++ b/framework/base/Controller.php @@ -317,6 +317,11 @@ class Controller extends Component return $this->createView()->renderPartial($view, $params); } + public function renderFile($file, $params = array()) + { + return $this->createView()->renderFile($file, $params); + } + public function createView() { return new View($this); diff --git a/framework/console/Request.php b/framework/console/Request.php index 4976fd2..4c801ca 100644 --- a/framework/console/Request.php +++ b/framework/console/Request.php @@ -15,7 +15,7 @@ namespace yii\console; */ class Request extends \yii\base\Request { - const ANONYMOUS_PARAMS = 'args'; + const ANONYMOUS_PARAMS = '-args'; /** * @var string the controller route specified by this request. If this is an empty string, diff --git a/framework/console/controllers/HelpController.php b/framework/console/controllers/HelpController.php index 3cd6a07..3e9b4e1 100644 --- a/framework/console/controllers/HelpController.php +++ b/framework/console/controllers/HelpController.php @@ -179,17 +179,59 @@ class HelpController extends Controller $prefix = $controller->getUniqueId(); foreach ($actions as $action) { if ($action === $controller->defaultAction) { - echo "* $prefix/$action (default)\n"; + echo "* $prefix/$action (default)"; } else { - echo "* $prefix/$action\n"; + echo "* $prefix/$action"; } + $summary = $this->getActionSummary($controller, $action); + if ($summary !== '') { + echo ': ' . $summary; + } + echo "\n"; } - echo "\n\nTo see the help of each sub-command, enter:\n"; + echo "\n\nTo see the detailed information about individual sub-commands, enter:\n"; echo "\n yiic help \n\n"; } } /** + * Returns the short summary of the action. + * @param Controller $controller the controller instance + * @param string $actionID action ID + * @return string the summary about the action + */ + protected function getActionSummary($controller, $actionID) + { + $action = $controller->createAction($actionID); + if ($action === null) { + return ''; + } + if ($action instanceof InlineAction) { + $reflection = new \ReflectionMethod($controller, $action->actionMethod); + } else { + $reflection = new \ReflectionClass($action); + } + $tags = $this->parseComment($reflection->getDocComment()); + if ($tags['description'] !== '') { + $limit = 73 - strlen($action->getUniqueId()); + if ($actionID === $controller->defaultAction) { + $limit -= 10; + } + if ($limit < 0) { + $limit = 50; + } + $description = $tags['description']; + if (($pos = strpos($tags['description'], "\n")) !== false) { + $description = substr($description, 0, $pos); + } + $text = substr($description, 0, $limit); + return strlen($description) > $limit ? $text . '...' : $text; + } else { + return ''; + } + } + + /** * Displays the detailed information of a command action. * @param Controller $controller the controller instance * @param string $actionID action ID @@ -200,7 +242,7 @@ class HelpController extends Controller $action = $controller->createAction($actionID); if ($action === null) { throw new Exception(Yii::t('yii', 'No help for unknown sub-command "{command}".', array( - '{command}' => $action->getUniqueId(), + '{command}' => rtrim($controller->getUniqueId() . '/' . $actionID, '/'), ))); } if ($action instanceof InlineAction) { @@ -232,7 +274,6 @@ class HelpController extends Controller echo "\n\n"; if (!empty($required) || !empty($optional)) { - echo "\nARGUMENTS\n\n"; echo implode("\n\n", array_merge($required, $optional)) . "\n\n"; } diff --git a/framework/console/controllers/MigrateController.php b/framework/console/controllers/MigrateController.php index b8076fc..62e7d40 100644 --- a/framework/console/controllers/MigrateController.php +++ b/framework/console/controllers/MigrateController.php @@ -14,45 +14,43 @@ use Yii; use yii\console\Exception; use yii\console\Controller; use yii\db\Connection; +use yii\db\Query; +use yii\util\ArrayHelper; /** - * This command provides support for database migrations. + * This command manages application migrations. * - * EXAMPLES + * 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. * - * - yiic migrate - * Applies ALL new migrations. This is equivalent to 'yiic migrate up'. + * This command provides support for tracking the migration history, upgrading + * or downloading with migrations, and creating new migration skeletons. * - * - yiic migrate create create_user_table - * Creates a new migration named 'create_user_table'. + * 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: * - * - yiic migrate up 3 - * Applies the next 3 new migrations. + * ~~~ + * CREATE TABLE tbl_migration ( + * version varchar(255) PRIMARY KEY, + * apply_time integer + * ) + * ~~~ * - * - yiic migrate down - * Reverts the last applied migration. + * Below are some common usages of this command: * - * - yiic migrate down 3 - * Reverts the last 3 applied migrations. + * ~~~ + * # creates a new migration named 'create_user_table' + * yiic migrate/create create_user_table * - * - yiic migrate to 101129_185401 - * Migrates up or down to version 101129_185401. + * # applies ALL new migrations + * yiic migrate * - * - yiic migrate mark 101129_185401 - * Modifies the migration history up or down to version 101129_185401. - * No actual migration will be performed. - * - * - yiic migrate history - * Shows all previously applied migration information. - * - * - yiic migrate history 10 - * Shows the last 10 applied migrations. - * - * - yiic migrate new - * Shows all new migrations. - * - * - yiic migrate new 10 - * Shows the next 10 migrations that have not been applied. + * # reverts the last applied migration + * yiic migrate/down + * ~~~ * * @author Qiang Xue * @since 2.0 @@ -65,21 +63,16 @@ class MigrateController extends Controller const BASE_MIGRATION = 'm000000_000000_base'; /** - * @var string the directory that stores the migrations. This can be either a path alias - * or a directory. Defaults to '@application/migrations'. + * @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 = '@application/migrations'; /** * @var string the name of the table for keeping applied migration information. - * This table will be automatically created if not exists. - * The table structure is as follows: - * - * ~~~ - * CREATE TABLE tbl_migration ( - * version varchar(255) PRIMARY KEY, - * apply_time integer - * ) - * ~~~ */ public $migrationTable = 'tbl_migration'; /** @@ -88,24 +81,30 @@ class MigrateController extends Controller */ public $connectionID = 'db'; /** - * @var string the path of the template file for generating new migrations. + * @var string the template file for generating new migrations. * This can be either a path alias (e.g. "@application/migrations/template.php") - * or a file path. If not set, an internal template will be used. + * or a file path. */ - public $templateFile; - /** - * @var string the default command action. - */ - public $defaultAction = 'up'; + 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]]. @@ -121,8 +120,14 @@ class MigrateController extends Controller throw new Exception("The migration directory \"{$this->migrationPath}\" does not exist."); } $this->migrationPath = $path; + + $this->db = Yii::$application->getComponent($this->connectionID); + if (!$this->db instanceof Connection) { + throw new Exception("Invalid DB connection \"{$this->connectionID}\"."); + } + $version = Yii::getVersion(); - echo "\nYii Migration Tool (based on Yii v{$version})\n\n"; + echo "Yii Migration Tool (based on Yii v{$version})\n\n"; return true; } else { return false; @@ -130,17 +135,18 @@ class MigrateController extends Controller } /** - * Upgrades the application by applying new migrations. For example, + * 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 array $args the number of new migrations to be applied. If not provided, - * all new migrations will be applied. + * @param integer $limit the number of new migrations to be applied. If 0, it means + * applying all available new migrations. */ - public function actionUp($args) + public function actionUp($limit = 0) { if (($migrations = $this->getNewMigrations()) === array()) { echo "No new migration found. Your system is up-to-date.\n"; @@ -148,9 +154,9 @@ class MigrateController extends Controller } $total = count($migrations); - $step = isset($args[0]) ? (int)$args[0] : 0; - if ($step > 0) { - $migrations = array_slice($migrations, 0, $step); + $limit = (int)$limit; + if ($limit > 0) { + $migrations = array_slice($migrations, 0, $limit); } $n = count($migrations); @@ -167,8 +173,8 @@ class MigrateController extends Controller if ($this->confirm('Apply the above ' . ($n === 1 ? 'migration' : 'migrations') . "?")) { foreach ($migrations as $migration) { - if ($this->migrateUp($migration) === false) { - echo "\nMigration failed. The rest of the new migrations are canceled.\n"; + if (!$this->migrateUp($migration)) { + echo "\nMigration failed. The rest of the migrations are canceled.\n"; return; } } @@ -177,25 +183,26 @@ class MigrateController extends Controller } /** - * Downgrades the application by reverting old migrations. For example, + * 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 array $args the number of migrations to be reverted. If not provided, - * the last applied migration will be reverted. - * @throws Exception if the number of the steps is less than 1. + * @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($args) + public function actionDown($limit = 1) { - $step = isset($args[0]) ? (int)$args[0] : 1; - if ($step < 1) { - throw new Exception("The step parameter must be greater than 0."); + $limit = (int)$limit; + if ($limit < 1) { + throw new Exception("The step argument must be greater than 0."); } - if (($migrations = $this->getMigrationHistory($step)) === array()) { + if (($migrations = $this->getMigrationHistory($limit)) === array()) { echo "No migration has been done before.\n"; return; } @@ -210,8 +217,8 @@ class MigrateController extends Controller if ($this->confirm('Revert the above ' . ($n === 1 ? 'migration' : 'migrations') . "?")) { foreach ($migrations as $migration) { - if ($this->migrateDown($migration) === false) { - echo "\nMigration failed. All later migrations are canceled.\n"; + if (!$this->migrateDown($migration)) { + echo "\nMigration failed. The rest of the migrations are canceled.\n"; return; } } @@ -219,14 +226,29 @@ class MigrateController extends Controller } } - public function actionRedo($args) + /** + * 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) { - $step = isset($args[0]) ? (int)$args[0] : 1; - if ($step < 1) { - die("Error: The step parameter must be greater than 0.\n"); + $limit = (int)$limit; + if ($limit < 1) { + throw new Exception("The step argument must be greater than 0."); } - if (($migrations = $this->getMigrationHistory($step)) === array()) { + if (($migrations = $this->getMigrationHistory($limit)) === array()) { echo "No migration has been done before.\n"; return; } @@ -241,14 +263,14 @@ class MigrateController extends Controller if ($this->confirm('Redo the above ' . ($n === 1 ? 'migration' : 'migrations') . "?")) { foreach ($migrations as $migration) { - if ($this->migrateDown($migration) === false) { - echo "\nMigration failed. All later migrations are canceled.\n"; + 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) === false) { - echo "\nMigration failed. All later migrations are canceled.\n"; + if (!$this->migrateUp($migration)) { + echo "\nMigration failed. The rest of the migrations migrations are canceled.\n"; return; } } @@ -256,26 +278,35 @@ class MigrateController extends Controller } } - public function actionTo($args) + /** + * 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) { - if (isset($args[0])) { - $version = $args[0]; - } else { - throw new Exception("Please specify which version to migrate to."); - } - $originalVersion = $version; if (preg_match('/^m?(\d{6}_\d{6})(_.*?)?$/', $version, $matches)) { $version = 'm' . $matches[1]; } else { - throw new Exception("The version option must be either a timestamp (e.g. 101129_185401)\nor the full name of a migration (e.g. m101129_185401_create_user_table)."); + 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(array($i + 1)); + $this->actionUp($i + 1); return; } } @@ -287,7 +318,7 @@ class MigrateController extends Controller if ($i === 0) { echo "Already at '$originalVersion'. Nothing needs to be done.\n"; } else { - $this->actionDown(array($i)); + $this->actionDown($i); } return; } @@ -296,33 +327,40 @@ class MigrateController extends Controller throw new Exception("Unable to find the version '$originalVersion'."); } - public function actionMark($args) + /** + * 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) { - if (isset($args[0])) { - $version = $args[0]; - } else { - throw new Exception('Please specify which version to mark to.'); - } $originalVersion = $version; if (preg_match('/^m?(\d{6}_\d{6})(_.*?)?$/', $version, $matches)) { $version = 'm' . $matches[1]; } else { - throw new Exception("Error: The version option must be either a timestamp (e.g. 101129_185401)\nor the full name of a migration (e.g. m101129_185401_create_user_table)."); + 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)."); } - $db = $this->getDb(); - // 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 = $db->createCommand(); + $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"; } @@ -338,9 +376,11 @@ class MigrateController extends Controller echo "Already at '$originalVersion'. Nothing needs to be done.\n"; } else { if ($this->confirm("Set migration history at $originalVersion?")) { - $command = $db->createCommand(); + $command = $this->db->createCommand(); for ($j = 0; $j < $i; ++$j) { - $command->delete($this->migrationTable, $db->quoteColumnName('version') . '=:version', array(':version' => $migrations[$j])); + $command->delete($this->migrationTable, array( + 'version' => $migrations[$j], + ))->execute(); } echo "The migration history is set at $originalVersion.\nNo actual migration was performed.\n"; } @@ -349,12 +389,27 @@ class MigrateController extends Controller } } - die("Error: Unable to find the version '$originalVersion'.\n"); + throw new Exception("Unable to find the version '$originalVersion'."); } - public function actionHistory($args) + /** + * 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 = isset($args[0]) ? (int)$args[0] : -1; + $limit = (int)$limit; $migrations = $this->getMigrationHistory($limit); if ($migrations === array()) { echo "No migration has been done before.\n"; @@ -371,9 +426,24 @@ class MigrateController extends Controller } } - public function actionNew($args) + /** + * 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 = isset($args[0]) ? (int)$args[0] : -1; + $limit = (int)$limit; $migrations = $this->getNewMigrations(); if ($migrations === array()) { echo "No new migrations found. Your system is up-to-date.\n"; @@ -394,47 +464,59 @@ class MigrateController extends Controller /** * Creates a new migration. - * @param array $args the name of the new migration. - * @throws Exception if the name of the new migration is not provided + * + * 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($args) + public function actionCreate($name) { - if (isset($args[0])) { - $name = $args[0]; - } else { - throw new Exception('Please provide the name of the new migration.'); - } - if (!preg_match('/^\w+$/', $name)) { - die("Error: The name of the migration must contain letters, digits and/or underscore characters only.\n"); + throw new Exception("The migration name should contain letters, digits and/or underscore characters only."); } $name = 'm' . gmdate('ymd_His') . '_' . $name; - $content = strtr($this->getTemplate(), array('{ClassName}' => $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; + return true; } echo "*** applying $class\n"; $start = microtime(true); - $migration = $this->instantiateMigration($class); + $migration = $this->createMigration($class); if ($migration->up() !== false) { - $this->getDb()->createCommand()->insert($this->migrationTable, array( + $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"; @@ -442,20 +524,27 @@ class MigrateController extends Controller } } + /** + * 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; + return true; } echo "*** reverting $class\n"; $start = microtime(true); - $migration = $this->instantiateMigration($class); + $migration = $this->createMigration($class); if ($migration->down() !== false) { - $db = $this->getDb(); - $db->createCommand()->delete($this->migrationTable, $db->quoteColumnName('version') . '=:version', array(':version' => $class)); + $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"; @@ -463,62 +552,82 @@ class MigrateController extends Controller } } - protected function instantiateMigration($class) + /** + * 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); - $migration = new $class; - $migration->db = $this->getDb(); - return $migration; + return new $class(array( + 'db' => $this->db, + )); } + /** - * @var CDbConnection + * @return Connection the database connection that is used to store the migration history. + * @throws Exception if the database connection ID is invalid. */ - private $_db; - protected function getDb() { - if ($this->_db !== null) { - return $this->_db; + if ($this->db !== null) { + return $this->db; } else { - if (($this->_db = Yii::$application->getComponent($this->connectionID)) instanceof CDbConnection) { - return $this->_db; + $this->db = Yii::$application->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) { - $db = $this->getDb(); - if ($db->schema->getTable($this->migrationTable) === null) { + if ($this->db->schema->getTableSchema($this->migrationTable) === null) { $this->createMigrationHistoryTable(); } - return CHtml::listData($db->createCommand() - ->select('version, apply_time') + $query = new Query; + $rows = $query->select(array('version', 'apply_time')) ->from($this->migrationTable) - ->order('version DESC') + ->orderBy('version DESC') ->limit($limit) - ->queryAll(), 'version', 'apply_time'); + ->createCommand() + ->queryAll(); + $history = ArrayHelper::map($rows, 'version', 'apply_time'); + unset($history[self::BASE_MIGRATION]); + return $history; } + /** + * Creates the migration history table. + */ protected function createMigrationHistoryTable() { - $db = $this->getDb(); echo 'Creating migration history table "' . $this->migrationTable . '"...'; - $db->createCommand()->createTable($this->migrationTable, array( - 'version' => 'string NOT NULL PRIMARY KEY', + $this->db->createCommand()->createTable($this->migrationTable, array( + 'version' => 'varchar(255) NOT NULL PRIMARY KEY', 'apply_time' => 'integer', - )); - $db->createCommand()->insert($this->migrationTable, array( + ))->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(); @@ -541,39 +650,4 @@ class MigrateController extends Controller sort($migrations); return $migrations; } - - protected function getTemplate() - { - if ($this->templateFile !== null) { - return file_get_contents(Yii::getAlias($this->templateFile)); - } else { - return << + +class extends \yii\db\Migration +{ + public function up() + { + + } + + public function down() + { + echo " cannot be reverted.\n"; + return false; + } +} From 398ac25456449caf40134522bfae20b7de198de1 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Sun, 3 Feb 2013 19:45:44 -0500 Subject: [PATCH 007/117] added UserException. --- framework/base/Application.php | 2 +- framework/base/ErrorHandler.php | 4 ++-- framework/base/Exception.php | 5 ----- framework/base/HttpException.php | 6 +----- framework/base/InvalidRequestException.php | 7 +------ framework/base/InvalidRouteException.php | 7 +------ framework/base/UserException.php | 21 +++++++++++++++++++++ framework/console/Application.php | 3 +-- framework/console/Exception.php | 7 +------ framework/console/controllers/HelpController.php | 6 +++--- 10 files changed, 32 insertions(+), 36 deletions(-) create mode 100644 framework/base/UserException.php diff --git a/framework/base/Application.php b/framework/base/Application.php index 40e8437..cd77f1f 100644 --- a/framework/base/Application.php +++ b/framework/base/Application.php @@ -464,7 +464,7 @@ class Application extends Module */ public function renderException($exception) { - if ($exception instanceof Exception && ($exception->causedByUser || !YII_DEBUG)) { + if ($exception instanceof Exception && ($exception instanceof UserException || !YII_DEBUG)) { $message = $exception->getName() . ': ' . $exception->getMessage(); } else { $message = YII_DEBUG ? (string)$exception : 'Error: ' . $exception->getMessage(); diff --git a/framework/base/ErrorHandler.php b/framework/base/ErrorHandler.php index 0b6bf97..211bad6 100644 --- a/framework/base/ErrorHandler.php +++ b/framework/base/ErrorHandler.php @@ -81,7 +81,7 @@ class ErrorHandler extends Component \Yii::$application->renderException($exception); } else { $view = new View($this); - if (!YII_DEBUG || $exception instanceof Exception && $exception->causedByUser) { + if (!YII_DEBUG || $exception instanceof UserException) { $viewName = $this->errorView; } else { $viewName = $this->exceptionView; @@ -256,7 +256,7 @@ class ErrorHandler extends Component public function renderAsHtml($exception) { $view = new View($this); - if (!YII_DEBUG || $exception instanceof Exception && $exception->causedByUser) { + if (!YII_DEBUG || $exception instanceof UserException) { $viewName = $this->errorView; } else { $viewName = $this->exceptionView; diff --git a/framework/base/Exception.php b/framework/base/Exception.php index ab681e2..db77028 100644 --- a/framework/base/Exception.php +++ b/framework/base/Exception.php @@ -18,11 +18,6 @@ namespace yii\base; class Exception extends \Exception { /** - * @var boolean whether this exception is caused by end user's mistake (e.g. wrong URL) - */ - public $causedByUser = false; - - /** * @return string the user-friendly name of this exception */ public function getName() diff --git a/framework/base/HttpException.php b/framework/base/HttpException.php index ca753b9..378b56a 100644 --- a/framework/base/HttpException.php +++ b/framework/base/HttpException.php @@ -19,16 +19,12 @@ namespace yii\base; * @author Qiang Xue * @since 2.0 */ -class HttpException extends Exception +class HttpException extends UserException { /** * @var integer HTTP status code, such as 403, 404, 500, etc. */ public $statusCode; - /** - * @var boolean whether this exception is caused by end user's mistake (e.g. wrong URL) - */ - public $causedByUser = true; /** * Constructor. diff --git a/framework/base/InvalidRequestException.php b/framework/base/InvalidRequestException.php index fd468a1..dddb35e 100644 --- a/framework/base/InvalidRequestException.php +++ b/framework/base/InvalidRequestException.php @@ -15,14 +15,9 @@ namespace yii\base; * @author Qiang Xue * @since 2.0 */ -class InvalidRequestException extends \Exception +class InvalidRequestException extends UserException { /** - * @var boolean whether this exception is caused by end user's mistake (e.g. wrong URL) - */ - public $causedByUser = true; - - /** * @return string the user-friendly name of this exception */ public function getName() diff --git a/framework/base/InvalidRouteException.php b/framework/base/InvalidRouteException.php index e20b2b7..0d5a9b5 100644 --- a/framework/base/InvalidRouteException.php +++ b/framework/base/InvalidRouteException.php @@ -15,14 +15,9 @@ namespace yii\base; * @author Qiang Xue * @since 2.0 */ -class InvalidRouteException extends \Exception +class InvalidRouteException extends UserException { /** - * @var boolean whether this exception is caused by end user's mistake (e.g. wrong URL) - */ - public $causedByUser = true; - - /** * @return string the user-friendly name of this exception */ public function getName() diff --git a/framework/base/UserException.php b/framework/base/UserException.php new file mode 100644 index 0000000..f915882 --- /dev/null +++ b/framework/base/UserException.php @@ -0,0 +1,21 @@ + + * @since 2.0 + */ +class UserException extends Exception +{ +} diff --git a/framework/console/Application.php b/framework/console/Application.php index 5f86cac..2f91d5b 100644 --- a/framework/console/Application.php +++ b/framework/console/Application.php @@ -126,8 +126,7 @@ class Application extends \yii\base\Application 'message' => 'yii\console\controllers\MessageController', 'help' => 'yii\console\controllers\HelpController', 'migrate' => 'yii\console\controllers\MigrateController', - 'shell' => 'yii\console\controllers\ShellController', - 'create' => 'yii\console\controllers\CreateController', + 'app' => 'yii\console\controllers\CreateController', ); } diff --git a/framework/console/Exception.php b/framework/console/Exception.php index 0ed5a42..63daba3 100644 --- a/framework/console/Exception.php +++ b/framework/console/Exception.php @@ -15,14 +15,9 @@ namespace yii\console; * @author Qiang Xue * @since 2.0 */ -class Exception extends \yii\base\Exception +class Exception extends \yii\base\UserException { /** - * @var boolean whether this exception is caused by end user's mistake (e.g. wrong URL) - */ - public $causedByUser = true; - - /** * @return string the user-friendly name of this exception */ public function getName() diff --git a/framework/console/controllers/HelpController.php b/framework/console/controllers/HelpController.php index 3e9b4e1..147fde0 100644 --- a/framework/console/controllers/HelpController.php +++ b/framework/console/controllers/HelpController.php @@ -52,7 +52,7 @@ class HelpController extends Controller * @return integer the exit status * @throws Exception if the command for help is unknown */ - public function actionIndex($command) + public function actionIndex($command = null) { if ($command !== null) { $result = Yii::$application->createController($command); @@ -147,10 +147,10 @@ class HelpController extends Controller if ($commands !== array()) { echo "The following commands are available:\n\n"; foreach ($commands as $command) { - echo " * $command\n"; + echo "* $command\n"; } echo "\nTo see the help of each command, enter:\n"; - echo "\n yiic help \n\n"; + echo "\n yiic help \n\n"; } else { echo "\nNo commands are found.\n"; } From 54ee8c44a0c9208dc472950a3db0df9a3ae2bb7c Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Mon, 4 Feb 2013 16:39:19 -0500 Subject: [PATCH 008/117] refactored exceptions. I18N WIP. --- framework/YiiBase.php | 57 +------- framework/base/Application.php | 81 +++-------- framework/base/Controller.php | 2 +- framework/base/Exception.php | 2 +- framework/base/HttpException.php | 2 +- framework/base/InvalidCallException.php | 4 +- framework/base/InvalidConfigException.php | 4 +- framework/base/InvalidRequestException.php | 2 +- framework/base/InvalidRouteException.php | 2 +- framework/base/NotSupportedException.php | 4 +- framework/base/SecurityManager.php | 8 +- framework/base/UnknownMethodException.php | 4 +- framework/base/UnknownPropertyException.php | 4 +- framework/base/UrlManager.php | 6 +- framework/console/Application.php | 4 +- framework/console/Controller.php | 4 +- framework/console/Exception.php | 6 +- framework/console/controllers/HelpController.php | 4 +- framework/db/Exception.php | 2 +- framework/i18n/I18N.php | 117 +++++++++++++++ framework/i18n/MessageSource.php | 158 ++++++++++++++++++++ framework/i18n/PhpMessageSource.php | 160 +++++++++++++++++++++ framework/logging/EmailTarget.php | 2 +- framework/logging/ProfileTarget.php | 6 +- framework/validators/BooleanValidator.php | 4 +- framework/validators/CaptchaValidator.php | 4 +- framework/validators/CompareValidator.php | 24 ++-- framework/validators/DateValidator.php | 2 +- framework/validators/EmailValidator.php | 4 +- framework/validators/ExistValidator.php | 2 +- framework/validators/FileValidator.php | 18 +-- framework/validators/NumberValidator.php | 16 +-- framework/validators/RangeValidator.php | 6 +- .../validators/RegularExpressionValidator.php | 4 +- framework/validators/RequiredValidator.php | 8 +- framework/validators/StringValidator.php | 14 +- framework/validators/UniqueValidator.php | 2 +- framework/validators/UrlValidator.php | 4 +- framework/web/AssetManager.php | 4 +- framework/web/Controller.php | 2 +- framework/web/Request.php | 2 +- framework/web/Session.php | 8 +- 42 files changed, 561 insertions(+), 212 deletions(-) create mode 100644 framework/i18n/I18N.php create mode 100644 framework/i18n/MessageSource.php create mode 100644 framework/i18n/PhpMessageSource.php diff --git a/framework/YiiBase.php b/framework/YiiBase.php index d01648c..be59c16 100644 --- a/framework/YiiBase.php +++ b/framework/YiiBase.php @@ -507,9 +507,6 @@ class YiiBase * 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 $category message category. Please use only word letters. Note, category 'yii' is - * reserved for Yii framework core code use. See {@link CPhpMessageSource} for - * more interpretation about message category. * @param string $message the original message * @param array $params parameters to be applied to the message using strtr. * The first parameter can be a number without key. @@ -517,62 +514,12 @@ class YiiBase * an appropriate message translation. * You can pass parameter for {@link CChoiceFormat::format} * or plural forms format without wrapping it with array. - * @param string $source which message source application component to use. - * Defaults to null, meaning using 'coreMessages' for messages belonging to - * the 'yii' category and using 'messages' for the rest messages. * @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 */ - public static function t($category, $message, $params = array(), $source = null, $language = null) + public static function t($message, $params = array(), $language = null) { - // todo; - return $params !== array() ? strtr($message, $params) : $message; - if (self::$application !== null) - { - if ($source === null) - { - $source = $category === 'yii' ? 'coreMessages' : 'messages'; - } - if (($source = self::$application->getComponent($source)) !== null) - { - $message = $source->translate($category, $message, $language); - } - } - if ($params === array()) - { - return $message; - } - if (!is_array($params)) - { - $params = array($params); - } - if (isset($params[0])) // number choice - { - if (strpos($message, '|') !== false) - { - if (strpos($message, '#') === false) - { - $chunks = explode('|', $message); - $expressions = self::$application->getLocale($language)->getPluralRules(); - if ($n = min(count($chunks), count($expressions))) - { - for ($i = 0; $i < $n; $i++) - { - $chunks[$i] = $expressions[$i] . '#' . $chunks[$i]; - } - - $message = implode('|', $chunks); - } - } - $message = CChoiceFormat::format($message, $params[0]); - } - if (!isset($params['{n}'])) - { - $params['{n}'] = $params[0]; - } - unset($params[0]); - } - return $params !== array() ? strtr($message, $params) : $message; + Yii::$application->getI18N()->translate($message, $params, $language); } } diff --git a/framework/base/Application.php b/framework/base/Application.php index cd77f1f..e4c1333 100644 --- a/framework/base/Application.php +++ b/framework/base/Application.php @@ -30,10 +30,6 @@ use yii\util\FileHelper; * persistence method. This application component is dynamically loaded when needed. *
  • {@link getCache cache}: provides caching feature. This application component is * disabled by default.
  • - *
  • {@link getMessages messages}: provides the message source for translating - * application messages. This application component is dynamically loaded when needed.
  • - *
  • {@link getCoreMessages coreMessages}: provides the message source for translating - * Yii framework messages. This application component is dynamically loaded when needed.
  • * * * Application will undergo the following life cycles when processing a user request: @@ -57,23 +53,28 @@ class Application extends Module const EVENT_BEFORE_REQUEST = 'beforeRequest'; const EVENT_AFTER_REQUEST = 'afterRequest'; /** - * @var string the application name. Defaults to 'My Application'. + * @var string the application name. */ public $name = 'My Application'; /** - * @var string the version of this application. Defaults to '1.0'. + * @var string the version of this application. */ public $version = '1.0'; /** - * @var string the charset currently used for the application. Defaults to 'UTF-8'. + * @var string the charset currently used for the application. */ public $charset = 'UTF-8'; /** + * @var string the language that is meant to be used for end users. + * @see sourceLanguage + */ + public $language = 'en_US'; + /** * @var string the language that the application is written in. This mainly refers to - * the language that the messages and view files are in. Defaults to 'en_us' (US English). + * the language that the messages and view files are written in. * @see language */ - public $sourceLanguage = 'en_us'; + public $sourceLanguage = 'en_US'; /** * @var array IDs of the components that need to be loaded when the application starts. */ @@ -213,29 +214,6 @@ class Application extends Module } /** - * Returns the language that the end user is using. - * @return string the language that the user is using (e.g. 'en_US', 'zh_CN'). - * Defaults to the value of [[sourceLanguage]]. - */ - public function getLanguage() - { - return $this->_language === null ? $this->sourceLanguage : $this->_language; - } - - /** - * Specifies which language the end user is using. - * This is the language that the application should use to display to end users. - * By default, [[language]] and [[sourceLanguage]] are the same. - * Do not set this property unless your application needs to support multiple languages. - * @param string $language the user language (e.g. 'en_US', 'zh_CN'). - * If it is null, the [[sourceLanguage]] will be used. - */ - public function setLanguage($language) - { - $this->_language = $language; - } - - /** * Returns the time zone used by this application. * This is a simple wrapper of PHP function date_default_timezone_get(). * @return string the time zone used by this application. @@ -295,23 +273,6 @@ class Application extends Module // return $this->getLocale()->getDateFormatter(); // } // - // /** - // * Returns the core message translations component. - // * @return \yii\i18n\MessageSource the core message translations - // */ - // public function getCoreMessages() - // { - // return $this->getComponent('coreMessages'); - // } - // - // /** - // * Returns the application message translations component. - // * @return \yii\i18n\MessageSource the application message translations - // */ - // public function getMessages() - // { - // return $this->getComponent('messages'); - // } /** * Returns the database connection component. @@ -368,6 +329,15 @@ class Application extends Module } /** + * Returns the internationalization (i18n) component + * @return \yii\i18n\I18N the internationalization component + */ + public function getI18N() + { + return $this->getComponent('i18n'); + } + + /** * Sets default path aliases. */ public function registerDefaultAliases() @@ -387,19 +357,14 @@ class Application extends Module 'errorHandler' => array( 'class' => 'yii\base\ErrorHandler', ), - 'coreMessages' => array( - 'class' => 'yii\i18n\PhpMessageSource', - 'language' => 'en_us', - 'basePath' => '@yii/messages', - ), - 'messages' => array( - 'class' => 'yii\i18n\PhpMessageSource', + 'i18n' => array( + 'class' => 'yii\i18n\I18N', ), 'securityManager' => array( 'class' => 'yii\base\SecurityManager', ), - 'urlManager' => array( - 'class' => 'yii\web\UrlManager', + 'translator' => array( + 'class' => 'yii\i18n\Translator', ), )); } diff --git a/framework/base/Controller.php b/framework/base/Controller.php index f9cf399..15681e8 100644 --- a/framework/base/Controller.php +++ b/framework/base/Controller.php @@ -177,7 +177,7 @@ class Controller extends Component } if ($missing !== array()) { - throw new InvalidRequestException(Yii::t('yii', 'Missing required parameters: {params}', array( + throw new InvalidRequestException(Yii::t('yii:Missing required parameters: {params}', array( '{params}' => implode(', ', $missing), ))); } diff --git a/framework/base/Exception.php b/framework/base/Exception.php index db77028..d9cb77a 100644 --- a/framework/base/Exception.php +++ b/framework/base/Exception.php @@ -22,7 +22,7 @@ class Exception extends \Exception */ public function getName() { - return \Yii::t('yii', 'Exception'); + return \Yii::t('yii:Exception'); } } diff --git a/framework/base/HttpException.php b/framework/base/HttpException.php index 378b56a..e23ff09 100644 --- a/framework/base/HttpException.php +++ b/framework/base/HttpException.php @@ -104,6 +104,6 @@ class HttpException extends UserException if(isset($httpCodes[$this->statusCode])) return $httpCodes[$this->statusCode]; else - return \Yii::t('yii', 'Error'); + return \Yii::t('yii:Error'); } } diff --git a/framework/base/InvalidCallException.php b/framework/base/InvalidCallException.php index a1df021..ab9de67 100644 --- a/framework/base/InvalidCallException.php +++ b/framework/base/InvalidCallException.php @@ -15,14 +15,14 @@ namespace yii\base; * @author Qiang Xue * @since 2.0 */ -class InvalidCallException extends \Exception +class InvalidCallException extends Exception { /** * @return string the user-friendly name of this exception */ public function getName() { - return \Yii::t('yii', 'Invalid Call'); + return \Yii::t('yii:Invalid Call'); } } diff --git a/framework/base/InvalidConfigException.php b/framework/base/InvalidConfigException.php index 3c100d1..97cdc8e 100644 --- a/framework/base/InvalidConfigException.php +++ b/framework/base/InvalidConfigException.php @@ -15,14 +15,14 @@ namespace yii\base; * @author Qiang Xue * @since 2.0 */ -class InvalidConfigException extends \Exception +class InvalidConfigException extends Exception { /** * @return string the user-friendly name of this exception */ public function getName() { - return \Yii::t('yii', 'Invalid Configuration'); + return \Yii::t('yii:Invalid Configuration'); } } diff --git a/framework/base/InvalidRequestException.php b/framework/base/InvalidRequestException.php index dddb35e..428d008 100644 --- a/framework/base/InvalidRequestException.php +++ b/framework/base/InvalidRequestException.php @@ -22,7 +22,7 @@ class InvalidRequestException extends UserException */ public function getName() { - return \Yii::t('yii', 'Invalid Request'); + return \Yii::t('yii:Invalid Request'); } } diff --git a/framework/base/InvalidRouteException.php b/framework/base/InvalidRouteException.php index 0d5a9b5..929221c 100644 --- a/framework/base/InvalidRouteException.php +++ b/framework/base/InvalidRouteException.php @@ -22,7 +22,7 @@ class InvalidRouteException extends UserException */ public function getName() { - return \Yii::t('yii', 'Invalid Route'); + return \Yii::t('yii:Invalid Route'); } } diff --git a/framework/base/NotSupportedException.php b/framework/base/NotSupportedException.php index 56e7e36..b7efed4 100644 --- a/framework/base/NotSupportedException.php +++ b/framework/base/NotSupportedException.php @@ -15,14 +15,14 @@ namespace yii\base; * @author Qiang Xue * @since 2.0 */ -class NotSupportedException extends \Exception +class NotSupportedException extends Exception { /** * @return string the user-friendly name of this exception */ public function getName() { - return \Yii::t('yii', 'Not Supported'); + return \Yii::t('yii:Not Supported'); } } diff --git a/framework/base/SecurityManager.php b/framework/base/SecurityManager.php index 441b908..04be15c 100644 --- a/framework/base/SecurityManager.php +++ b/framework/base/SecurityManager.php @@ -79,7 +79,7 @@ class SecurityManager extends Component if (!empty($value)) { $this->_validationKey = $value; } else { - throw new CException(Yii::t('yii', 'SecurityManager.validationKey cannot be empty.')); + throw new CException(Yii::t('yii:SecurityManager.validationKey cannot be empty.')); } } @@ -112,7 +112,7 @@ class SecurityManager extends Component if (!empty($value)) { $this->_encryptionKey = $value; } else { - throw new CException(Yii::t('yii', 'SecurityManager.encryptionKey cannot be empty.')); + throw new CException(Yii::t('yii:SecurityManager.encryptionKey cannot be empty.')); } } @@ -191,12 +191,12 @@ class SecurityManager extends Component } if ($module === false) { - throw new CException(Yii::t('yii', 'Failed to initialize the mcrypt module.')); + throw new CException(Yii::t('yii:Failed to initialize the mcrypt module.')); } return $module; } else { - throw new CException(Yii::t('yii', 'SecurityManager requires PHP mcrypt extension to be loaded in order to use data encryption feature.')); + throw new CException(Yii::t('yii:SecurityManager requires PHP mcrypt extension to be loaded in order to use data encryption feature.')); } } diff --git a/framework/base/UnknownMethodException.php b/framework/base/UnknownMethodException.php index 459f791..0cad676 100644 --- a/framework/base/UnknownMethodException.php +++ b/framework/base/UnknownMethodException.php @@ -15,14 +15,14 @@ namespace yii\base; * @author Qiang Xue * @since 2.0 */ -class UnknownMethodException extends \Exception +class UnknownMethodException extends Exception { /** * @return string the user-friendly name of this exception */ public function getName() { - return \Yii::t('yii', 'Unknown Method'); + return \Yii::t('yii:Unknown Method'); } } diff --git a/framework/base/UnknownPropertyException.php b/framework/base/UnknownPropertyException.php index de8de1c..a226f84 100644 --- a/framework/base/UnknownPropertyException.php +++ b/framework/base/UnknownPropertyException.php @@ -15,14 +15,14 @@ namespace yii\base; * @author Qiang Xue * @since 2.0 */ -class UnknownPropertyException extends \Exception +class UnknownPropertyException extends Exception { /** * @return string the user-friendly name of this exception */ public function getName() { - return \Yii::t('yii', 'Unknown Property'); + return \Yii::t('yii:Unknown Property'); } } diff --git a/framework/base/UrlManager.php b/framework/base/UrlManager.php index 3de8807..e90f08f 100644 --- a/framework/base/UrlManager.php +++ b/framework/base/UrlManager.php @@ -366,7 +366,7 @@ class UrlManager extends Component return isset($_GET[$this->routeVar]) ? $_GET[$this->routeVar] : $r; } if($this->useStrictParsing) - throw new HttpException(404,Yii::t('yii','Unable to resolve the request "{route}".', + throw new HttpException(404,Yii::t('yii:Unable to resolve the request "{route}".', array('{route}'=>$pathInfo))); else return $pathInfo; @@ -502,7 +502,7 @@ class UrlManager extends Component if($value===self::PATH_FORMAT || $value===self::GET_FORMAT) $this->_urlFormat=$value; else - throw new CException(Yii::t('yii','CUrlManager.UrlFormat must be either "path" or "get".')); + throw new CException(Yii::t('yii:CUrlManager.UrlFormat must be either "path" or "get".')); } } @@ -685,7 +685,7 @@ class CUrlRule extends CBaseUrlRule $this->routePattern='/^'.strtr($this->route,$tr2).'$/u'; if(YII_DEBUG && @preg_match($this->pattern,'test')===false) - throw new CException(Yii::t('yii','The URL pattern "{pattern}" for route "{route}" is not a valid regular expression.', + throw new CException(Yii::t('yii:The URL pattern "{pattern}" for route "{route}" is not a valid regular expression.', array('{route}'=>$route,'{pattern}'=>$pattern))); } diff --git a/framework/console/Application.php b/framework/console/Application.php index 2f91d5b..7a8de43 100644 --- a/framework/console/Application.php +++ b/framework/console/Application.php @@ -93,7 +93,7 @@ class Application extends \yii\base\Application if ($request->getIsConsoleRequest()) { return $this->runAction($request->route, $request->params); } else { - throw new Exception(\Yii::t('yii', 'this script must be run from the command line.')); + throw new Exception(\Yii::t('yii:this script must be run from the command line.')); } } @@ -112,7 +112,7 @@ class Application extends \yii\base\Application try { return parent::runAction($route, $params); } catch (InvalidRouteException $e) { - throw new Exception(\Yii::t('yii', 'Unknown command "{command}".', array('{command}' => $route))); + throw new Exception(\Yii::t('yii:Unknown command "{command}".', array('{command}' => $route))); } } diff --git a/framework/console/Controller.php b/framework/console/Controller.php index f51588f..b3e9ff7 100644 --- a/framework/console/Controller.php +++ b/framework/console/Controller.php @@ -85,7 +85,7 @@ class Controller extends \yii\base\Controller $args = isset($params[Request::ANONYMOUS_PARAMS]) ? $params[Request::ANONYMOUS_PARAMS] : array(); unset($params[Request::ANONYMOUS_PARAMS]); if ($params !== array()) { - throw new Exception(Yii::t('yii', 'Unknown options: {params}', array( + throw new Exception(Yii::t('yii:Unknown options: {params}', array( '{params}' => implode(', ', array_keys($params)), ))); } @@ -109,7 +109,7 @@ class Controller extends \yii\base\Controller } if ($missing !== array()) { - throw new Exception(Yii::t('yii', 'Missing required arguments: {params}', array( + throw new Exception(Yii::t('yii:Missing required arguments: {params}', array( '{params}' => implode(', ', $missing), ))); } diff --git a/framework/console/Exception.php b/framework/console/Exception.php index 63daba3..052030b 100644 --- a/framework/console/Exception.php +++ b/framework/console/Exception.php @@ -9,20 +9,22 @@ namespace yii\console; +use yii\base\UserException; + /** * Exception represents an exception caused by incorrect usage of a console command. * * @author Qiang Xue * @since 2.0 */ -class Exception extends \yii\base\UserException +class Exception extends UserException { /** * @return string the user-friendly name of this exception */ public function getName() { - return \Yii::t('yii', 'Error'); + return \Yii::t('yii:Error'); } } diff --git a/framework/console/controllers/HelpController.php b/framework/console/controllers/HelpController.php index 147fde0..7e2eab9 100644 --- a/framework/console/controllers/HelpController.php +++ b/framework/console/controllers/HelpController.php @@ -57,7 +57,7 @@ class HelpController extends Controller if ($command !== null) { $result = Yii::$application->createController($command); if ($result === false) { - throw new Exception(Yii::t('yii', 'No help for unknown command "{command}".', array( + throw new Exception(Yii::t('yii:No help for unknown command "{command}".', array( '{command}' => $command, ))); } @@ -241,7 +241,7 @@ class HelpController extends Controller { $action = $controller->createAction($actionID); if ($action === null) { - throw new Exception(Yii::t('yii', 'No help for unknown sub-command "{command}".', array( + throw new Exception(Yii::t('yii:No help for unknown sub-command "{command}".', array( '{command}' => rtrim($controller->getUniqueId() . '/' . $actionID, '/'), ))); } diff --git a/framework/db/Exception.php b/framework/db/Exception.php index 209dc40..1c4196f 100644 --- a/framework/db/Exception.php +++ b/framework/db/Exception.php @@ -40,6 +40,6 @@ class Exception extends \yii\base\Exception */ public function getName() { - return \Yii::t('yii', 'Database Exception'); + return \Yii::t('yii:Database Exception'); } } \ No newline at end of file diff --git a/framework/i18n/I18N.php b/framework/i18n/I18N.php new file mode 100644 index 0000000..99a4e69 --- /dev/null +++ b/framework/i18n/I18N.php @@ -0,0 +1,117 @@ +language; + } + + if (preg_match('/^([\w\-]+):(.*)/', $message, $matches)) { + $category = $matches[1]; + $message = $matches[2]; + } else { + $category = 'app'; + } + + $message = $this->getSource($category)->translate($category, $message, $language); + + if (!is_array($params)) { + $params = array($params); + } + + if (isset($params[0])) { + $message = $this->getPluralFormat($message, $params[0], $language); + if (!isset($params['{n}'])) { + $params['{n}'] = $params[0]; + } + unset($params[0]); + } + + return $params === array() ? $message : strtr($message, $params); + } + + public function getLocale($language) + { + + } + + public function getSource($category) + { + return $category === 'yii' ? $this->getMessages() : $this->getCoreMessages(); + } + + private $_coreMessages; + private $_messages; + + public function getCoreMessages() + { + if (is_object($this->_coreMessages)) { + return $this->_coreMessages; + } elseif ($this->_coreMessages === null) { + return $this->_coreMessages = new PhpMessageSource(array( + 'sourceLanguage' => 'en_US', + 'basePath' => '@yii/messages', + )); + } else { + return $this->_coreMessages = Yii::createObject($this->_coreMessages); + } + } + + public function setCoreMessages($config) + { + $this->_coreMessages = $config; + } + + public function getMessages() + { + if (is_object($this->_messages)) { + return $this->_messages; + } elseif ($this->_messages === null) { + return $this->_messages = new PhpMessageSource(array( + 'sourceLanguage' => 'en_US', + 'basePath' => '@app/messages', + )); + } else { + return $this->_messages = Yii::createObject($this->_messages); + } + } + + public function setMessages($config) + { + $this->_messages = $config; + } + + protected function getPluralFormat($message, $number, $language) + { + if (strpos($message, '|') === false) { + return $message; + } + $chunks = explode('|', $message); + $rules = $this->getLocale($language)->getPluralRules(); + foreach ($rules as $i => $rule) { + if (isset($chunks[$i]) && self::evaluate($rule, $number)) { + return $chunks[$i]; + } + } + $n = count($rules); + return isset($chunks[$n]) ? $chunks[$n] : $chunks[0]; + } + + /** + * Evaluates a PHP expression with the given number value. + * @param string $expression the PHP expression + * @param mixed $n the number value + * @return boolean the expression result + */ + protected static function evaluate($expression, $n) + { + return @eval("return $expression;"); + } +} diff --git a/framework/i18n/MessageSource.php b/framework/i18n/MessageSource.php new file mode 100644 index 0000000..17a5b00 --- /dev/null +++ b/framework/i18n/MessageSource.php @@ -0,0 +1,158 @@ + + * @since 2.0 + */ +abstract class MessageSource extends Component +{ + /** + * @var boolean whether to force message translation when the source and target languages are the same. + * Defaults to false, meaning translation is only performed when source and target languages are different. + */ + public $forceTranslation = false; + public $sourceLanguage; + + private $_messages = array(); + + public function init() + { + parent::init(); + if ($this->sourceLanguage === null) { + $this->sourceLanguage = Yii::$application->sourceLanguage; + } + } + + /** + * Loads the message translation for the specified language and category. + * @param string $category the message category + * @param string $language the target language + * @return array the loaded messages + */ + abstract protected function loadMessages($category, $language); + + /** + * Translates a message to the specified language. + * + * Note, if the specified language is the same as + * the {@link getLanguage source message language}, messages will NOT be translated. + * + * If the message is not found in the translations, an {@link onMissingTranslation} + * event will be raised. Handlers can mark this message or do some + * default handling. The {@link CMissingTranslationEvent::message} + * property of the event parameter will be returned. + * + * @param string $category the message category + * @param string $message the message to be translated + * @param string $language the target language. If null (default), the {@link CApplication::getLanguage application language} will be used. + * @return string the translated message (or the original message if translation is not needed) + */ + public function translate($category, $message, $language) + { + if ($this->forceTranslation || $language !== $this->sourceLanguage) { + return $this->translateMessage($category, $message, $language); + } else { + return $message; + } + } + + /** + * Translates the specified message. + * If the message is not found, an {@link onMissingTranslation} + * event will be raised. + * @param string $category the category that the message belongs to + * @param string $message the message to be translated + * @param string $language the target language + * @return string the translated message + */ + protected function translateMessage($category, $message, $language) + { + $key = $language . '/' . $category; + if (!isset($this->_messages[$key])) { + $this->_messages[$key] = $this->loadMessages($category, $language); + } + if (isset($this->_messages[$key][$message]) && $this->_messages[$key][$message] !== '') { + return $this->_messages[$key][$message]; + } elseif ($this->hasEventHandler('onMissingTranslation')) { + $event = new CMissingTranslationEvent($this, $category, $message, $language); + $this->onMissingTranslation($event); + return $event->message; + } else { + return $message; + } + } + + /** + * Raised when a message cannot be translated. + * Handlers may log this message or do some default handling. + * The {@link CMissingTranslationEvent::message} property + * will be returned by {@link translateMessage}. + * @param CMissingTranslationEvent $event the event parameter + */ + public function onMissingTranslation($event) + { + $this->raiseEvent('onMissingTranslation', $event); + } +} + + +/** + * CMissingTranslationEvent represents the parameter for the {@link MessageSource::onMissingTranslation onMissingTranslation} event. + * + * @author Qiang Xue + * @package system.i18n + * @since 1.0 + */ +class CMissingTranslationEvent extends CEvent +{ + /** + * @var string the message to be translated + */ + public $message; + /** + * @var string the category that the message belongs to + */ + public $category; + /** + * @var string the ID of the language that the message is to be translated to + */ + public $language; + + /** + * Constructor. + * @param mixed $sender sender of this event + * @param string $category the category that the message belongs to + * @param string $message the message to be translated + * @param string $language the ID of the language that the message is to be translated to + */ + public function __construct($sender, $category, $message, $language) + { + parent::__construct($sender); + $this->message = $message; + $this->category = $category; + $this->language = $language; + } +} diff --git a/framework/i18n/PhpMessageSource.php b/framework/i18n/PhpMessageSource.php new file mode 100644 index 0000000..4f8511f --- /dev/null +++ b/framework/i18n/PhpMessageSource.php @@ -0,0 +1,160 @@ + + *
  • All translations are saved under the {@link basePath} directory.
  • + *
  • Translations in one language are kept as PHP files under an individual subdirectory + * whose name is the same as the language ID. Each PHP file contains messages + * belonging to the same category, and the file name is the same as the category name.
  • + *
  • Within a PHP file, an array of (source, translation) pairs is returned. + * For example: + *
    + * return array(
    + *     'original message 1' => 'translated message 1',
    + *     'original message 2' => 'translated message 2',
    + * );
    + * 
    + *
  • + * + * When {@link cachingDuration} is set as a positive number, message translations will be cached. + * + * Messages for an extension class (e.g. a widget, a module) can be specially managed and used. + * In particular, if a message belongs to an extension whose class name is Xyz, then the message category + * can be specified in the format of 'Xyz.categoryName'. And the corresponding message file + * is assumed to be 'BasePath/messages/LanguageID/categoryName.php', where 'BasePath' refers to + * the directory that contains the extension class file. When using Yii::t() to translate an extension message, + * the category name should be set as 'Xyz.categoryName'. + * + * @author Qiang Xue + * @since 2.0 + */ +class PhpMessageSource extends MessageSource +{ + const CACHE_KEY_PREFIX='Yii.CPhpMessageSource.'; + + /** + * @var integer the time in seconds that the messages can remain valid in cache. + * Defaults to 0, meaning the caching is disabled. + */ + public $cachingDuration=0; + /** + * @var string the ID of the cache application component that is used to cache the messages. + * Defaults to 'cache' which refers to the primary cache application component. + * Set this property to false if you want to disable caching the messages. + */ + public $cacheID='cache'; + /** + * @var string the base path for all translated messages. Defaults to null, meaning + * the "messages" subdirectory of the application directory (e.g. "protected/messages"). + */ + public $basePath; + /** + * @var array the message paths for extensions that do not have a base class to use as category prefix. + * The format of the array should be: + *
    +	 * array(
    +	 *     'ExtensionName' => 'ext.ExtensionName.messages',
    +	 * )
    +	 * 
    + * Where the key is the name of the extension and the value is the alias to the path + * of the "messages" subdirectory of the extension. + * When using Yii::t() to translate an extension message, the category name should be + * set as 'ExtensionName.categoryName'. + * Defaults to an empty array, meaning no extensions registered. + * @since 1.1.13 + */ + public $extensionPaths=array(); + + private $_files=array(); + + /** + * Initializes the application component. + * This method overrides the parent implementation by preprocessing + * the user request data. + */ + public function init() + { + parent::init(); + if($this->basePath===null) + $this->basePath=Yii::getPathOfAlias('application.messages'); + } + + /** + * Determines the message file name based on the given category and language. + * If the category name contains a dot, it will be split into the module class name and the category name. + * In this case, the message file will be assumed to be located within the 'messages' subdirectory of + * the directory containing the module class file. + * Otherwise, the message file is assumed to be under the {@link basePath}. + * @param string $category category name + * @param string $language language ID + * @return string the message file path + */ + protected function getMessageFile($category,$language) + { + if(!isset($this->_files[$category][$language])) + { + if(($pos=strpos($category,'.'))!==false) + { + $extensionClass=substr($category,0,$pos); + $extensionCategory=substr($category,$pos+1); + // First check if there's an extension registered for this class. + if(isset($this->extensionPaths[$extensionClass])) + $this->_files[$category][$language]=Yii::getPathOfAlias($this->extensionPaths[$extensionClass]).DIRECTORY_SEPARATOR.$language.DIRECTORY_SEPARATOR.$extensionCategory.'.php'; + else + { + // No extension registered, need to find it. + $class=new ReflectionClass($extensionClass); + $this->_files[$category][$language]=dirname($class->getFileName()).DIRECTORY_SEPARATOR.'messages'.DIRECTORY_SEPARATOR.$language.DIRECTORY_SEPARATOR.$extensionCategory.'.php'; + } + } + else + $this->_files[$category][$language]=$this->basePath.DIRECTORY_SEPARATOR.$language.DIRECTORY_SEPARATOR.$category.'.php'; + } + return $this->_files[$category][$language]; + } + + /** + * Loads the message translation for the specified language and category. + * @param string $category the message category + * @param string $language the target language + * @return array the loaded messages + */ + protected function loadMessages($category,$language) + { + $messageFile=$this->getMessageFile($category,$language); + + if($this->cachingDuration>0 && $this->cacheID!==false && ($cache=Yii::app()->getComponent($this->cacheID))!==null) + { + $key=self::CACHE_KEY_PREFIX . $messageFile; + if(($data=$cache->get($key))!==false) + return unserialize($data); + } + + if(is_file($messageFile)) + { + $messages=include($messageFile); + if(!is_array($messages)) + $messages=array(); + if(isset($cache)) + { + $dependency=new CFileCacheDependency($messageFile); + $cache->set($key,serialize($messages),$this->cachingDuration,$dependency); + } + return $messages; + } + else + return array(); + } +} \ No newline at end of file diff --git a/framework/logging/EmailTarget.php b/framework/logging/EmailTarget.php index e02e4da..205cd06 100644 --- a/framework/logging/EmailTarget.php +++ b/framework/logging/EmailTarget.php @@ -50,7 +50,7 @@ class EmailTarget extends Target $body .= $this->formatMessage($message); } $body = wordwrap($body, 70); - $subject = $this->subject === null ? \Yii::t('yii', 'Application Log') : $this->subject; + $subject = $this->subject === null ? \Yii::t('yii:Application Log') : $this->subject; foreach ($this->emails as $email) { $this->sendEmail($subject, $body, $email, $this->sentFrom, $this->headers); } diff --git a/framework/logging/ProfileTarget.php b/framework/logging/ProfileTarget.php index 716e6b7..30a207c 100644 --- a/framework/logging/ProfileTarget.php +++ b/framework/logging/ProfileTarget.php @@ -61,7 +61,7 @@ class CProfileLogRoute extends CWebLogRoute if ($value === 'summary' || $value === 'callstack') $this->_report = $value; else - throw new CException(Yii::t('yii', 'CProfileLogRoute.report "{report}" is invalid. Valid values include "summary" and "callstack".', + throw new CException(Yii::t('yii:CProfileLogRoute.report "{report}" is invalid. Valid values include "summary" and "callstack".', array('{report}' => $value))); } @@ -108,7 +108,7 @@ class CProfileLogRoute extends CWebLogRoute $results[$last[4]] = array($token, $delta, count($stack)); } else { - throw new CException(Yii::t('yii', 'CProfileLogRoute found a mismatching code block "{token}". Make sure the calls to Yii::beginProfile() and Yii::endProfile() be properly nested.', + throw new CException(Yii::t('yii:CProfileLogRoute found a mismatching code block "{token}". Make sure the calls to Yii::beginProfile() and Yii::endProfile() be properly nested.', array('{token}' => $token))); } } @@ -151,7 +151,7 @@ class CProfileLogRoute extends CWebLogRoute else $results[$token] = array($token, 1, $delta, $delta, $delta); } else - throw new CException(Yii::t('yii', 'CProfileLogRoute found a mismatching code block "{token}". Make sure the calls to Yii::beginProfile() and Yii::endProfile() be properly nested.', + throw new CException(Yii::t('yii:CProfileLogRoute found a mismatching code block "{token}". Make sure the calls to Yii::beginProfile() and Yii::endProfile() be properly nested.', array('{token}' => $token))); } } diff --git a/framework/validators/BooleanValidator.php b/framework/validators/BooleanValidator.php index 90e7939..a002ba5 100644 --- a/framework/validators/BooleanValidator.php +++ b/framework/validators/BooleanValidator.php @@ -54,7 +54,7 @@ class BooleanValidator extends Validator } if (!$this->strict && $value != $this->trueValue && $value != $this->falseValue || $this->strict && $value !== $this->trueValue && $value !== $this->falseValue) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii', '{attribute} must be either {true} or {false}.'); + $message = ($this->message !== null) ? $this->message : \Yii::t('yii:{attribute} must be either {true} or {false}.'); $this->addError($object, $attribute, $message, array( '{true}' => $this->trueValue, '{false}' => $this->falseValue, @@ -70,7 +70,7 @@ class BooleanValidator extends Validator */ public function clientValidateAttribute($object, $attribute) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii', '{attribute} must be either {true} or {false}.'); + $message = ($this->message !== null) ? $this->message : \Yii::t('yii:{attribute} must be either {true} or {false}.'); $message = strtr($message, array( '{attribute}' => $object->getAttributeLabel($attribute), '{value}' => $object->$attribute, diff --git a/framework/validators/CaptchaValidator.php b/framework/validators/CaptchaValidator.php index 3da8ed6..e936f63 100644 --- a/framework/validators/CaptchaValidator.php +++ b/framework/validators/CaptchaValidator.php @@ -49,7 +49,7 @@ class CaptchaValidator extends Validator } $captcha = $this->getCaptchaAction(); if (!$captcha->validate($value, $this->caseSensitive)) { - $message = $this->message !== null ? $this->message : \Yii::t('yii', 'The verification code is incorrect.'); + $message = $this->message !== null ? $this->message : \Yii::t('yii:The verification code is incorrect.'); $this->addError($object, $attribute, $message); } } @@ -85,7 +85,7 @@ class CaptchaValidator extends Validator public function clientValidateAttribute($object, $attribute) { $captcha = $this->getCaptchaAction(); - $message = $this->message !== null ? $this->message : \Yii::t('yii', 'The verification code is incorrect.'); + $message = $this->message !== null ? $this->message : \Yii::t('yii:The verification code is incorrect.'); $message = strtr($message, array( '{attribute}' => $object->getAttributeLabel($attribute), '{value}' => $object->$attribute, diff --git a/framework/validators/CompareValidator.php b/framework/validators/CompareValidator.php index 9345b73..141cb16 100644 --- a/framework/validators/CompareValidator.php +++ b/framework/validators/CompareValidator.php @@ -96,37 +96,37 @@ class CompareValidator extends Validator case '=': case '==': if (($this->strict && $value !== $compareValue) || (!$this->strict && $value != $compareValue)) { - $message = ($this->message !== null) ? $this->message : Yii::t('yii', '{attribute} must be repeated exactly.'); + $message = ($this->message !== null) ? $this->message : Yii::t('yii:{attribute} must be repeated exactly.'); $this->addError($object, $attribute, $message, array('{compareAttribute}' => $compareLabel)); } break; case '!=': if (($this->strict && $value === $compareValue) || (!$this->strict && $value == $compareValue)) { - $message = ($this->message !== null) ? $this->message : Yii::t('yii', '{attribute} must not be equal to "{compareValue}".'); + $message = ($this->message !== null) ? $this->message : Yii::t('yii:{attribute} must not be equal to "{compareValue}".'); $this->addError($object, $attribute, $message, array('{compareAttribute}' => $compareLabel, '{compareValue}' => $compareValue)); } break; case '>': if ($value <= $compareValue) { - $message = ($this->message !== null) ? $this->message : Yii::t('yii', '{attribute} must be greater than "{compareValue}".'); + $message = ($this->message !== null) ? $this->message : Yii::t('yii:{attribute} must be greater than "{compareValue}".'); $this->addError($object, $attribute, $message, array('{compareAttribute}' => $compareLabel, '{compareValue}' => $compareValue)); } break; case '>=': if ($value < $compareValue) { - $message = ($this->message !== null) ? $this->message : Yii::t('yii', '{attribute} must be greater than or equal to "{compareValue}".'); + $message = ($this->message !== null) ? $this->message : Yii::t('yii:{attribute} must be greater than or equal to "{compareValue}".'); $this->addError($object, $attribute, $message, array('{compareAttribute}' => $compareLabel, '{compareValue}' => $compareValue)); } break; case '<': if ($value >= $compareValue) { - $message = ($this->message !== null) ? $this->message : Yii::t('yii', '{attribute} must be less than "{compareValue}".'); + $message = ($this->message !== null) ? $this->message : Yii::t('yii:{attribute} must be less than "{compareValue}".'); $this->addError($object, $attribute, $message, array('{compareAttribute}' => $compareLabel, '{compareValue}' => $compareValue)); } break; case '<=': if ($value > $compareValue) { - $message = ($this->message !== null) ? $this->message : Yii::t('yii', '{attribute} must be less than or equal to "{compareValue}".'); + $message = ($this->message !== null) ? $this->message : Yii::t('yii:{attribute} must be less than or equal to "{compareValue}".'); $this->addError($object, $attribute, $message, array('{compareAttribute}' => $compareLabel, '{compareValue}' => $compareValue)); } break; @@ -158,37 +158,37 @@ class CompareValidator extends Validator case '=': case '==': if ($message === null) { - $message = Yii::t('yii', '{attribute} must be repeated exactly.'); + $message = Yii::t('yii:{attribute} must be repeated exactly.'); } $condition = 'value!=' . $compareValue; break; case '!=': if ($message === null) { - $message = Yii::t('yii', '{attribute} must not be equal to "{compareValue}".'); + $message = Yii::t('yii:{attribute} must not be equal to "{compareValue}".'); } $condition = 'value==' . $compareValue; break; case '>': if ($message === null) { - $message = Yii::t('yii', '{attribute} must be greater than "{compareValue}".'); + $message = Yii::t('yii:{attribute} must be greater than "{compareValue}".'); } $condition = 'value<=' . $compareValue; break; case '>=': if ($message === null) { - $message = Yii::t('yii', '{attribute} must be greater than or equal to "{compareValue}".'); + $message = Yii::t('yii:{attribute} must be greater than or equal to "{compareValue}".'); } $condition = 'value<' . $compareValue; break; case '<': if ($message === null) { - $message = Yii::t('yii', '{attribute} must be less than "{compareValue}".'); + $message = Yii::t('yii:{attribute} must be less than "{compareValue}".'); } $condition = 'value>=' . $compareValue; break; case '<=': if ($message === null) { - $message = Yii::t('yii', '{attribute} must be less than or equal to "{compareValue}".'); + $message = Yii::t('yii:{attribute} must be less than or equal to "{compareValue}".'); } $condition = 'value>' . $compareValue; break; diff --git a/framework/validators/DateValidator.php b/framework/validators/DateValidator.php index f4fa866..af97a71 100644 --- a/framework/validators/DateValidator.php +++ b/framework/validators/DateValidator.php @@ -66,7 +66,7 @@ class DateValidator extends Validator } if (!$valid) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii', 'The format of {attribute} is invalid.'); + $message = ($this->message !== null) ? $this->message : \Yii::t('yii:The format of {attribute} is invalid.'); $this->addError($object, $attribute, $message); } } diff --git a/framework/validators/EmailValidator.php b/framework/validators/EmailValidator.php index 8fd8120..b78b2da 100644 --- a/framework/validators/EmailValidator.php +++ b/framework/validators/EmailValidator.php @@ -63,7 +63,7 @@ class EmailValidator extends Validator return; } if (!$this->validateValue($value)) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii', '{attribute} is not a valid email address.'); + $message = ($this->message !== null) ? $this->message : \Yii::t('yii:{attribute} is not a valid email address.'); $this->addError($object, $attribute, $message); } } @@ -100,7 +100,7 @@ class EmailValidator extends Validator */ public function clientValidateAttribute($object, $attribute) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii', '{attribute} is not a valid email address.'); + $message = ($this->message !== null) ? $this->message : \Yii::t('yii:{attribute} is not a valid email address.'); $message = strtr($message, array( '{attribute}' => $object->getAttributeLabel($attribute), '{value}' => $object->$attribute, diff --git a/framework/validators/ExistValidator.php b/framework/validators/ExistValidator.php index be710bd..9d253c9 100644 --- a/framework/validators/ExistValidator.php +++ b/framework/validators/ExistValidator.php @@ -68,7 +68,7 @@ class ExistValidator extends Validator $query = $className::find(); $query->where(array($column->name => $value)); if (!$query->exists()) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii', '{attribute} "{value}" is invalid.'); + $message = ($this->message !== null) ? $this->message : \Yii::t('yii:{attribute} "{value}" is invalid.'); $this->addError($object, $attribute, $message); } } diff --git a/framework/validators/FileValidator.php b/framework/validators/FileValidator.php index 106b9a6..c558b76 100644 --- a/framework/validators/FileValidator.php +++ b/framework/validators/FileValidator.php @@ -115,7 +115,7 @@ class CFileValidator extends Validator return $this->emptyAttribute($object, $attribute); if (count($files) > $this->maxFiles) { - $message = $this->tooMany !== null ? $this->tooMany : \Yii::t('yii', '{attribute} cannot accept more than {limit} files.'); + $message = $this->tooMany !== null ? $this->tooMany : \Yii::t('yii:{attribute} cannot accept more than {limit} files.'); $this->addError($object, $attribute, $message, array('{attribute}' => $attribute, '{limit}' => $this->maxFiles)); } else foreach ($files as $file) @@ -145,20 +145,20 @@ class CFileValidator extends Validator return $this->emptyAttribute($object, $attribute); elseif ($error == UPLOAD_ERR_INI_SIZE || $error == UPLOAD_ERR_FORM_SIZE || $this->maxSize !== null && $file->getSize() > $this->maxSize) { - $message = $this->tooLarge !== null ? $this->tooLarge : \Yii::t('yii', 'The file "{file}" is too large. Its size cannot exceed {limit} bytes.'); + $message = $this->tooLarge !== null ? $this->tooLarge : \Yii::t('yii:The file "{file}" is too large. Its size cannot exceed {limit} bytes.'); $this->addError($object, $attribute, $message, array('{file}' => $file->getName(), '{limit}' => $this->getSizeLimit())); } elseif ($error == UPLOAD_ERR_PARTIAL) - throw new CException(\Yii::t('yii', 'The file "{file}" was only partially uploaded.', array('{file}' => $file->getName()))); + throw new CException(\Yii::t('yii:The file "{file}" was only partially uploaded.', array('{file}' => $file->getName()))); elseif ($error == UPLOAD_ERR_NO_TMP_DIR) - throw new CException(\Yii::t('yii', 'Missing the temporary folder to store the uploaded file "{file}".', array('{file}' => $file->getName()))); + throw new CException(\Yii::t('yii:Missing the temporary folder to store the uploaded file "{file}".', array('{file}' => $file->getName()))); elseif ($error == UPLOAD_ERR_CANT_WRITE) - throw new CException(\Yii::t('yii', 'Failed to write the uploaded file "{file}" to disk.', array('{file}' => $file->getName()))); + throw new CException(\Yii::t('yii:Failed to write the uploaded file "{file}" to disk.', array('{file}' => $file->getName()))); elseif (defined('UPLOAD_ERR_EXTENSION') && $error == UPLOAD_ERR_EXTENSION) // available for PHP 5.2.0 or above - throw new CException(\Yii::t('yii', 'File upload was stopped by extension.')); + throw new CException(\Yii::t('yii:File upload was stopped by extension.')); if ($this->minSize !== null && $file->getSize() < $this->minSize) { - $message = $this->tooSmall !== null ? $this->tooSmall : \Yii::t('yii', 'The file "{file}" is too small. Its size cannot be smaller than {limit} bytes.'); + $message = $this->tooSmall !== null ? $this->tooSmall : \Yii::t('yii:The file "{file}" is too small. Its size cannot be smaller than {limit} bytes.'); $this->addError($object, $attribute, $message, array('{file}' => $file->getName(), '{limit}' => $this->minSize)); } @@ -170,7 +170,7 @@ class CFileValidator extends Validator $types = $this->types; if (!in_array(strtolower($file->getExtensionName()), $types)) { - $message = $this->wrongType !== null ? $this->wrongType : \Yii::t('yii', 'The file "{file}" cannot be uploaded. Only files with these extensions are allowed: {extensions}.'); + $message = $this->wrongType !== null ? $this->wrongType : \Yii::t('yii:The file "{file}" cannot be uploaded. Only files with these extensions are allowed: {extensions}.'); $this->addError($object, $attribute, $message, array('{file}' => $file->getName(), '{extensions}' => implode(', ', $types))); } } @@ -185,7 +185,7 @@ class CFileValidator extends Validator { if (!$this->allowEmpty) { - $message = $this->message !== null ? $this->message : \Yii::t('yii', '{attribute} cannot be blank.'); + $message = $this->message !== null ? $this->message : \Yii::t('yii:{attribute} cannot be blank.'); $this->addError($object, $attribute, $message); } } diff --git a/framework/validators/NumberValidator.php b/framework/validators/NumberValidator.php index 4596fc1..d459fb8 100644 --- a/framework/validators/NumberValidator.php +++ b/framework/validators/NumberValidator.php @@ -73,21 +73,21 @@ class NumberValidator extends Validator } if ($this->integerOnly) { if (!preg_match($this->integerPattern, "$value")) { - $message = $this->message !== null ? $this->message : Yii::t('yii', '{attribute} must be an integer.'); + $message = $this->message !== null ? $this->message : Yii::t('yii:{attribute} must be an integer.'); $this->addError($object, $attribute, $message); } } else { if (!preg_match($this->numberPattern, "$value")) { - $message = $this->message !== null ? $this->message : Yii::t('yii', '{attribute} must be a number.'); + $message = $this->message !== null ? $this->message : Yii::t('yii:{attribute} must be a number.'); $this->addError($object, $attribute, $message); } } if ($this->min !== null && $value < $this->min) { - $message = $this->tooSmall !== null ? $this->tooSmall : Yii::t('yii', '{attribute} is too small (minimum is {min}).'); + $message = $this->tooSmall !== null ? $this->tooSmall : Yii::t('yii:{attribute} is too small (minimum is {min}).'); $this->addError($object, $attribute, $message, array('{min}' => $this->min)); } if ($this->max !== null && $value > $this->max) { - $message = $this->tooBig !== null ? $this->tooBig : Yii::t('yii', '{attribute} is too big (maximum is {max}).'); + $message = $this->tooBig !== null ? $this->tooBig : Yii::t('yii:{attribute} is too big (maximum is {max}).'); $this->addError($object, $attribute, $message, array('{max}' => $this->max)); } } @@ -103,8 +103,8 @@ class NumberValidator extends Validator $label = $object->getAttributeLabel($attribute); if (($message = $this->message) === null) { - $message = $this->integerOnly ? Yii::t('yii', '{attribute} must be an integer.') - : Yii::t('yii', '{attribute} must be a number.'); + $message = $this->integerOnly ? Yii::t('yii:{attribute} must be an integer.') + : Yii::t('yii:{attribute} must be a number.'); } $message = strtr($message, array( '{attribute}' => $label, @@ -118,7 +118,7 @@ if(!value.match($pattern)) { "; if ($this->min !== null) { if (($tooSmall = $this->tooSmall) === null) { - $tooSmall = Yii::t('yii', '{attribute} is too small (minimum is {min}).'); + $tooSmall = Yii::t('yii:{attribute} is too small (minimum is {min}).'); } $tooSmall = strtr($tooSmall, array( '{attribute}' => $label, @@ -133,7 +133,7 @@ if(value<{$this->min}) { } if ($this->max !== null) { if (($tooBig = $this->tooBig) === null) { - $tooBig = Yii::t('yii', '{attribute} is too big (maximum is {max}).'); + $tooBig = Yii::t('yii:{attribute} is too big (maximum is {max}).'); } $tooBig = strtr($tooBig, array( '{attribute}' => $label, diff --git a/framework/validators/RangeValidator.php b/framework/validators/RangeValidator.php index b2ff773..6abf81f 100644 --- a/framework/validators/RangeValidator.php +++ b/framework/validators/RangeValidator.php @@ -58,10 +58,10 @@ class RangeValidator extends Validator throw new InvalidConfigException('The "range" property must be specified as an array.'); } if (!$this->not && !in_array($value, $this->range, $this->strict)) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii', '{attribute} should be in the list.'); + $message = ($this->message !== null) ? $this->message : \Yii::t('yii:{attribute} should be in the list.'); $this->addError($object, $attribute, $message); } elseif ($this->not && in_array($value, $this->range, $this->strict)) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii', '{attribute} should NOT be in the list.'); + $message = ($this->message !== null) ? $this->message : \Yii::t('yii:{attribute} should NOT be in the list.'); $this->addError($object, $attribute, $message); } } @@ -80,7 +80,7 @@ class RangeValidator extends Validator } if (($message = $this->message) === null) { - $message = $this->not ? \Yii::t('yii', '{attribute} should NOT be in the list.') : \Yii::t('yii', '{attribute} should be in the list.'); + $message = $this->not ? \Yii::t('yii:{attribute} should NOT be in the list.') : \Yii::t('yii:{attribute} should be in the list.'); } $message = strtr($message, array( '{attribute}' => $object->getAttributeLabel($attribute), diff --git a/framework/validators/RegularExpressionValidator.php b/framework/validators/RegularExpressionValidator.php index fbdb062..1e034f7 100644 --- a/framework/validators/RegularExpressionValidator.php +++ b/framework/validators/RegularExpressionValidator.php @@ -51,7 +51,7 @@ class RegularExpressionValidator extends Validator throw new \yii\base\Exception('The "pattern" property must be specified with a valid regular expression.'); } if ((!$this->not && !preg_match($this->pattern, $value)) || ($this->not && preg_match($this->pattern, $value))) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii', '{attribute} is invalid.'); + $message = ($this->message !== null) ? $this->message : \Yii::t('yii:{attribute} is invalid.'); $this->addError($object, $attribute, $message); } } @@ -69,7 +69,7 @@ class RegularExpressionValidator extends Validator throw new \yii\base\Exception('The "pattern" property must be specified with a valid regular expression.'); } - $message = ($this->message !== null) ? $this->message : \Yii::t('yii', '{attribute} is invalid.'); + $message = ($this->message !== null) ? $this->message : \Yii::t('yii:{attribute} is invalid.'); $message = strtr($message, array( '{attribute}' => $object->getAttributeLabel($attribute), '{value}' => $object->$attribute, diff --git a/framework/validators/RequiredValidator.php b/framework/validators/RequiredValidator.php index f0f4bfd..af03221 100644 --- a/framework/validators/RequiredValidator.php +++ b/framework/validators/RequiredValidator.php @@ -47,12 +47,12 @@ class RequiredValidator extends Validator $value = $object->$attribute; if ($this->requiredValue === null) { if ($this->strict && $value === null || !$this->strict && $this->isEmpty($value, true)) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii', '{attribute} cannot be blank.'); + $message = ($this->message !== null) ? $this->message : \Yii::t('yii:{attribute} cannot be blank.'); $this->addError($object, $attribute, $message); } } else { if (!$this->strict && $value != $this->requiredValue || $this->strict && $value !== $this->requiredValue) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii', '{attribute} must be "{requiredValue}".'); + $message = ($this->message !== null) ? $this->message : \Yii::t('yii:{attribute} must be "{requiredValue}".'); $this->addError($object, $attribute, $message, array( '{requiredValue}' => $this->requiredValue, )); @@ -71,7 +71,7 @@ class RequiredValidator extends Validator $message = $this->message; if ($this->requiredValue !== null) { if ($message === null) { - $message = \Yii::t('yii', '{attribute} must be "{requiredValue}".'); + $message = \Yii::t('yii:{attribute} must be "{requiredValue}".'); } $message = strtr($message, array( '{attribute}' => $object->getAttributeLabel($attribute), @@ -85,7 +85,7 @@ if (value != " . json_encode($this->requiredValue) . ") { "; } else { if ($message === null) { - $message = \Yii::t('yii', '{attribute} cannot be blank.'); + $message = \Yii::t('yii:{attribute} cannot be blank.'); } $message = strtr($message, array( '{attribute}' => $object->getAttributeLabel($attribute), diff --git a/framework/validators/StringValidator.php b/framework/validators/StringValidator.php index fe69a4d..4db29d3 100644 --- a/framework/validators/StringValidator.php +++ b/framework/validators/StringValidator.php @@ -76,7 +76,7 @@ class StringValidator extends Validator } if (!is_string($value)) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii', '{attribute} must be a string.'); + $message = ($this->message !== null) ? $this->message : \Yii::t('yii:{attribute} must be a string.'); $this->addError($object, $attribute, $message); return; } @@ -88,15 +88,15 @@ class StringValidator extends Validator } if ($this->min !== null && $length < $this->min) { - $message = ($this->tooShort !== null) ? $this->tooShort : \Yii::t('yii', '{attribute} is too short (minimum is {min} characters).'); + $message = ($this->tooShort !== null) ? $this->tooShort : \Yii::t('yii:{attribute} is too short (minimum is {min} characters).'); $this->addError($object, $attribute, $message, array('{min}' => $this->min)); } if ($this->max !== null && $length > $this->max) { - $message = ($this->tooLong !== null) ? $this->tooLong : \Yii::t('yii', '{attribute} is too long (maximum is {max} characters).'); + $message = ($this->tooLong !== null) ? $this->tooLong : \Yii::t('yii:{attribute} is too long (maximum is {max} characters).'); $this->addError($object, $attribute, $message, array('{max}' => $this->max)); } if ($this->is !== null && $length !== $this->is) { - $message = ($this->notEqual !== null) ? $this->notEqual : \Yii::t('yii', '{attribute} is of the wrong length (should be {length} characters).'); + $message = ($this->notEqual !== null) ? $this->notEqual : \Yii::t('yii:{attribute} is of the wrong length (should be {length} characters).'); $this->addError($object, $attribute, $message, array('{length}' => $this->is)); } } @@ -113,7 +113,7 @@ class StringValidator extends Validator $value = $object->$attribute; if (($notEqual = $this->notEqual) === null) { - $notEqual = \Yii::t('yii', '{attribute} is of the wrong length (should be {length} characters).'); + $notEqual = \Yii::t('yii:{attribute} is of the wrong length (should be {length} characters).'); } $notEqual = strtr($notEqual, array( '{attribute}' => $label, @@ -122,7 +122,7 @@ class StringValidator extends Validator )); if (($tooShort = $this->tooShort) === null) { - $tooShort = \Yii::t('yii', '{attribute} is too short (minimum is {min} characters).'); + $tooShort = \Yii::t('yii:{attribute} is too short (minimum is {min} characters).'); } $tooShort = strtr($tooShort, array( '{attribute}' => $label, @@ -131,7 +131,7 @@ class StringValidator extends Validator )); if (($tooLong = $this->tooLong) === null) { - $tooLong = \Yii::t('yii', '{attribute} is too long (maximum is {max} characters).'); + $tooLong = \Yii::t('yii:{attribute} is too long (maximum is {max} characters).'); } $tooLong = strtr($tooLong, array( '{attribute}' => $label, diff --git a/framework/validators/UniqueValidator.php b/framework/validators/UniqueValidator.php index 5d5e603..758d44f 100644 --- a/framework/validators/UniqueValidator.php +++ b/framework/validators/UniqueValidator.php @@ -86,7 +86,7 @@ class UniqueValidator extends Validator } if ($exists) { - $message = $this->message !== null ? $this->message : \Yii::t('yii', '{attribute} "{value}" has already been taken.'); + $message = $this->message !== null ? $this->message : \Yii::t('yii:{attribute} "{value}" has already been taken.'); $this->addError($object, $attribute, $message); } } diff --git a/framework/validators/UrlValidator.php b/framework/validators/UrlValidator.php index c6242a2..768e8d4 100644 --- a/framework/validators/UrlValidator.php +++ b/framework/validators/UrlValidator.php @@ -55,7 +55,7 @@ class UrlValidator extends Validator if (($value = $this->validateValue($value)) !== false) { $object->$attribute = $value; } else { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii', '{attribute} is not a valid URL.'); + $message = ($this->message !== null) ? $this->message : \Yii::t('yii:{attribute} is not a valid URL.'); $this->addError($object, $attribute, $message); } } @@ -97,7 +97,7 @@ class UrlValidator extends Validator */ public function clientValidateAttribute($object, $attribute) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii', '{attribute} is not a valid URL.'); + $message = ($this->message !== null) ? $this->message : \Yii::t('yii:{attribute} is not a valid URL.'); $message = strtr($message, array( '{attribute}' => $object->getAttributeLabel($attribute), '{value}' => $object->$attribute, diff --git a/framework/web/AssetManager.php b/framework/web/AssetManager.php index 2028a1c..bf27e6a 100644 --- a/framework/web/AssetManager.php +++ b/framework/web/AssetManager.php @@ -113,7 +113,7 @@ class CAssetManager extends CApplicationComponent if(($basePath=realpath($value))!==false && is_dir($basePath) && is_writable($basePath)) $this->_basePath=$basePath; else - throw new CException(Yii::t('yii','CAssetManager.basePath "{path}" is invalid. Please make sure the directory exists and is writable by the Web server process.', + throw new CException(Yii::t('yii:CAssetManager.basePath "{path}" is invalid. Please make sure the directory exists and is writable by the Web server process.', array('{path}'=>$value))); } @@ -236,7 +236,7 @@ class CAssetManager extends CApplicationComponent return $this->_published[$path]=$this->getBaseUrl().'/'.$dir; } } - throw new CException(Yii::t('yii','The asset "{asset}" to be published does not exist.', + throw new CException(Yii::t('yii:The asset "{asset}" to be published does not exist.', array('{asset}'=>$path))); } diff --git a/framework/web/Controller.php b/framework/web/Controller.php index 92722b5..e4c36c9 100644 --- a/framework/web/Controller.php +++ b/framework/web/Controller.php @@ -45,7 +45,7 @@ class Controller extends \yii\base\Controller */ public function invalidActionParams($action, $exception) { - throw new HttpException(400, \Yii::t('yii', 'Your request is invalid.')); + throw new HttpException(400, \Yii::t('yii:Your request is invalid.')); } /** diff --git a/framework/web/Request.php b/framework/web/Request.php index 09e4864..9566974 100644 --- a/framework/web/Request.php +++ b/framework/web/Request.php @@ -823,7 +823,7 @@ class Request extends \yii\base\Request $valid = false; } if (!$valid) { - throw new CHttpException(400, Yii::t('yii', 'The CSRF token could not be verified.')); + throw new CHttpException(400, Yii::t('yii:The CSRF token could not be verified.')); } } } diff --git a/framework/web/Session.php b/framework/web/Session.php index 4544fc0..2b23e62 100644 --- a/framework/web/Session.php +++ b/framework/web/Session.php @@ -115,7 +115,7 @@ class CHttpSession extends CApplicationComponent implements IteratorAggregate,Ar @session_start(); if(YII_DEBUG && session_id()=='') { - $message=Yii::t('yii','Failed to start session.'); + $message=Yii::t('yii:Failed to start session.'); if(function_exists('error_get_last')) { $error=error_get_last(); @@ -215,7 +215,7 @@ class CHttpSession extends CApplicationComponent implements IteratorAggregate,Ar if(is_dir($value)) session_save_path($value); else - throw new CException(Yii::t('yii','CHttpSession.savePath "{path}" is not a valid directory.', + throw new CException(Yii::t('yii:CHttpSession.savePath "{path}" is not a valid directory.', array('{path}'=>$value))); } @@ -280,7 +280,7 @@ class CHttpSession extends CApplicationComponent implements IteratorAggregate,Ar ini_set('session.use_only_cookies','1'); } else - throw new CException(Yii::t('yii','CHttpSession.cookieMode can only be "none", "allow" or "only".')); + throw new CException(Yii::t('yii:CHttpSession.cookieMode can only be "none", "allow" or "only".')); } /** @@ -304,7 +304,7 @@ class CHttpSession extends CApplicationComponent implements IteratorAggregate,Ar ini_set('session.gc_divisor','100'); } else - throw new CException(Yii::t('yii','CHttpSession.gcProbability "{value}" is invalid. It must be an integer between 0 and 100.', + throw new CException(Yii::t('yii:CHttpSession.gcProbability "{value}" is invalid. It must be an integer between 0 and 100.', array('{value}'=>$value))); } From 3c1452e84b769d2fecddc07cafdcdae869dab1ee Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Tue, 5 Feb 2013 11:48:35 -0500 Subject: [PATCH 009/117] Changed the separator of message category. --- framework/base/Controller.php | 2 +- framework/base/Exception.php | 2 +- framework/base/HttpException.php | 2 +- framework/base/InvalidCallException.php | 2 +- framework/base/InvalidConfigException.php | 2 +- framework/base/InvalidRequestException.php | 2 +- framework/base/InvalidRouteException.php | 2 +- framework/base/NotSupportedException.php | 2 +- framework/base/SecurityManager.php | 8 ++++---- framework/base/UnknownMethodException.php | 2 +- framework/base/UnknownPropertyException.php | 2 +- framework/base/UrlManager.php | 6 +++--- framework/console/Application.php | 4 ++-- framework/console/Controller.php | 4 ++-- framework/console/Exception.php | 2 +- framework/console/controllers/HelpController.php | 4 ++-- framework/db/Exception.php | 2 +- framework/i18n/I18N.php | 2 +- framework/logging/EmailTarget.php | 2 +- framework/logging/ProfileTarget.php | 6 +++--- framework/validators/BooleanValidator.php | 4 ++-- framework/validators/CaptchaValidator.php | 4 ++-- framework/validators/CompareValidator.php | 24 +++++++++++----------- framework/validators/DateValidator.php | 2 +- framework/validators/EmailValidator.php | 4 ++-- framework/validators/ExistValidator.php | 2 +- framework/validators/FileValidator.php | 18 ++++++++-------- framework/validators/NumberValidator.php | 16 +++++++-------- framework/validators/RangeValidator.php | 6 +++--- .../validators/RegularExpressionValidator.php | 4 ++-- framework/validators/RequiredValidator.php | 8 ++++---- framework/validators/StringValidator.php | 14 ++++++------- framework/validators/UniqueValidator.php | 2 +- framework/validators/UrlValidator.php | 4 ++-- framework/web/AssetManager.php | 4 ++-- framework/web/Controller.php | 2 +- framework/web/Request.php | 2 +- framework/web/Session.php | 8 ++++---- 38 files changed, 94 insertions(+), 94 deletions(-) diff --git a/framework/base/Controller.php b/framework/base/Controller.php index 15681e8..58deda2 100644 --- a/framework/base/Controller.php +++ b/framework/base/Controller.php @@ -177,7 +177,7 @@ class Controller extends Component } if ($missing !== array()) { - throw new InvalidRequestException(Yii::t('yii:Missing required parameters: {params}', array( + throw new InvalidRequestException(Yii::t('yii|Missing required parameters: {params}', array( '{params}' => implode(', ', $missing), ))); } diff --git a/framework/base/Exception.php b/framework/base/Exception.php index d9cb77a..0088a55 100644 --- a/framework/base/Exception.php +++ b/framework/base/Exception.php @@ -22,7 +22,7 @@ class Exception extends \Exception */ public function getName() { - return \Yii::t('yii:Exception'); + return \Yii::t('yii|Exception'); } } diff --git a/framework/base/HttpException.php b/framework/base/HttpException.php index e23ff09..a839b06 100644 --- a/framework/base/HttpException.php +++ b/framework/base/HttpException.php @@ -104,6 +104,6 @@ class HttpException extends UserException if(isset($httpCodes[$this->statusCode])) return $httpCodes[$this->statusCode]; else - return \Yii::t('yii:Error'); + return \Yii::t('yii|Error'); } } diff --git a/framework/base/InvalidCallException.php b/framework/base/InvalidCallException.php index ab9de67..cdd5c6b 100644 --- a/framework/base/InvalidCallException.php +++ b/framework/base/InvalidCallException.php @@ -22,7 +22,7 @@ class InvalidCallException extends Exception */ public function getName() { - return \Yii::t('yii:Invalid Call'); + return \Yii::t('yii|Invalid Call'); } } diff --git a/framework/base/InvalidConfigException.php b/framework/base/InvalidConfigException.php index 97cdc8e..683b2f4 100644 --- a/framework/base/InvalidConfigException.php +++ b/framework/base/InvalidConfigException.php @@ -22,7 +22,7 @@ class InvalidConfigException extends Exception */ public function getName() { - return \Yii::t('yii:Invalid Configuration'); + return \Yii::t('yii|Invalid Configuration'); } } diff --git a/framework/base/InvalidRequestException.php b/framework/base/InvalidRequestException.php index 428d008..377abbd 100644 --- a/framework/base/InvalidRequestException.php +++ b/framework/base/InvalidRequestException.php @@ -22,7 +22,7 @@ class InvalidRequestException extends UserException */ public function getName() { - return \Yii::t('yii:Invalid Request'); + return \Yii::t('yii|Invalid Request'); } } diff --git a/framework/base/InvalidRouteException.php b/framework/base/InvalidRouteException.php index 929221c..74216c5 100644 --- a/framework/base/InvalidRouteException.php +++ b/framework/base/InvalidRouteException.php @@ -22,7 +22,7 @@ class InvalidRouteException extends UserException */ public function getName() { - return \Yii::t('yii:Invalid Route'); + return \Yii::t('yii|Invalid Route'); } } diff --git a/framework/base/NotSupportedException.php b/framework/base/NotSupportedException.php index b7efed4..f487069 100644 --- a/framework/base/NotSupportedException.php +++ b/framework/base/NotSupportedException.php @@ -22,7 +22,7 @@ class NotSupportedException extends Exception */ public function getName() { - return \Yii::t('yii:Not Supported'); + return \Yii::t('yii|Not Supported'); } } diff --git a/framework/base/SecurityManager.php b/framework/base/SecurityManager.php index 04be15c..a3fb654 100644 --- a/framework/base/SecurityManager.php +++ b/framework/base/SecurityManager.php @@ -79,7 +79,7 @@ class SecurityManager extends Component if (!empty($value)) { $this->_validationKey = $value; } else { - throw new CException(Yii::t('yii:SecurityManager.validationKey cannot be empty.')); + throw new CException(Yii::t('yii|SecurityManager.validationKey cannot be empty.')); } } @@ -112,7 +112,7 @@ class SecurityManager extends Component if (!empty($value)) { $this->_encryptionKey = $value; } else { - throw new CException(Yii::t('yii:SecurityManager.encryptionKey cannot be empty.')); + throw new CException(Yii::t('yii|SecurityManager.encryptionKey cannot be empty.')); } } @@ -191,12 +191,12 @@ class SecurityManager extends Component } if ($module === false) { - throw new CException(Yii::t('yii:Failed to initialize the mcrypt module.')); + throw new CException(Yii::t('yii|Failed to initialize the mcrypt module.')); } return $module; } else { - throw new CException(Yii::t('yii:SecurityManager requires PHP mcrypt extension to be loaded in order to use data encryption feature.')); + throw new CException(Yii::t('yii|SecurityManager requires PHP mcrypt extension to be loaded in order to use data encryption feature.')); } } diff --git a/framework/base/UnknownMethodException.php b/framework/base/UnknownMethodException.php index 0cad676..b46903f 100644 --- a/framework/base/UnknownMethodException.php +++ b/framework/base/UnknownMethodException.php @@ -22,7 +22,7 @@ class UnknownMethodException extends Exception */ public function getName() { - return \Yii::t('yii:Unknown Method'); + return \Yii::t('yii|Unknown Method'); } } diff --git a/framework/base/UnknownPropertyException.php b/framework/base/UnknownPropertyException.php index a226f84..4cb768d 100644 --- a/framework/base/UnknownPropertyException.php +++ b/framework/base/UnknownPropertyException.php @@ -22,7 +22,7 @@ class UnknownPropertyException extends Exception */ public function getName() { - return \Yii::t('yii:Unknown Property'); + return \Yii::t('yii|Unknown Property'); } } diff --git a/framework/base/UrlManager.php b/framework/base/UrlManager.php index e90f08f..db6b598 100644 --- a/framework/base/UrlManager.php +++ b/framework/base/UrlManager.php @@ -366,7 +366,7 @@ class UrlManager extends Component return isset($_GET[$this->routeVar]) ? $_GET[$this->routeVar] : $r; } if($this->useStrictParsing) - throw new HttpException(404,Yii::t('yii:Unable to resolve the request "{route}".', + throw new HttpException(404,Yii::t('yii|Unable to resolve the request "{route}".', array('{route}'=>$pathInfo))); else return $pathInfo; @@ -502,7 +502,7 @@ class UrlManager extends Component if($value===self::PATH_FORMAT || $value===self::GET_FORMAT) $this->_urlFormat=$value; else - throw new CException(Yii::t('yii:CUrlManager.UrlFormat must be either "path" or "get".')); + throw new CException(Yii::t('yii|CUrlManager.UrlFormat must be either "path" or "get".')); } } @@ -685,7 +685,7 @@ class CUrlRule extends CBaseUrlRule $this->routePattern='/^'.strtr($this->route,$tr2).'$/u'; if(YII_DEBUG && @preg_match($this->pattern,'test')===false) - throw new CException(Yii::t('yii:The URL pattern "{pattern}" for route "{route}" is not a valid regular expression.', + throw new CException(Yii::t('yii|The URL pattern "{pattern}" for route "{route}" is not a valid regular expression.', array('{route}'=>$route,'{pattern}'=>$pattern))); } diff --git a/framework/console/Application.php b/framework/console/Application.php index 7a8de43..5b7d190 100644 --- a/framework/console/Application.php +++ b/framework/console/Application.php @@ -93,7 +93,7 @@ class Application extends \yii\base\Application if ($request->getIsConsoleRequest()) { return $this->runAction($request->route, $request->params); } else { - throw new Exception(\Yii::t('yii:this script must be run from the command line.')); + throw new Exception(\Yii::t('yii|this script must be run from the command line.')); } } @@ -112,7 +112,7 @@ class Application extends \yii\base\Application try { return parent::runAction($route, $params); } catch (InvalidRouteException $e) { - throw new Exception(\Yii::t('yii:Unknown command "{command}".', array('{command}' => $route))); + throw new Exception(\Yii::t('yii|Unknown command "{command}".', array('{command}' => $route))); } } diff --git a/framework/console/Controller.php b/framework/console/Controller.php index b3e9ff7..ff84a45 100644 --- a/framework/console/Controller.php +++ b/framework/console/Controller.php @@ -85,7 +85,7 @@ class Controller extends \yii\base\Controller $args = isset($params[Request::ANONYMOUS_PARAMS]) ? $params[Request::ANONYMOUS_PARAMS] : array(); unset($params[Request::ANONYMOUS_PARAMS]); if ($params !== array()) { - throw new Exception(Yii::t('yii:Unknown options: {params}', array( + throw new Exception(Yii::t('yii|Unknown options: {params}', array( '{params}' => implode(', ', array_keys($params)), ))); } @@ -109,7 +109,7 @@ class Controller extends \yii\base\Controller } if ($missing !== array()) { - throw new Exception(Yii::t('yii:Missing required arguments: {params}', array( + throw new Exception(Yii::t('yii|Missing required arguments: {params}', array( '{params}' => implode(', ', $missing), ))); } diff --git a/framework/console/Exception.php b/framework/console/Exception.php index 052030b..33a06b7 100644 --- a/framework/console/Exception.php +++ b/framework/console/Exception.php @@ -24,7 +24,7 @@ class Exception extends UserException */ public function getName() { - return \Yii::t('yii:Error'); + return \Yii::t('yii|Error'); } } diff --git a/framework/console/controllers/HelpController.php b/framework/console/controllers/HelpController.php index 7e2eab9..4a37f3d 100644 --- a/framework/console/controllers/HelpController.php +++ b/framework/console/controllers/HelpController.php @@ -57,7 +57,7 @@ class HelpController extends Controller if ($command !== null) { $result = Yii::$application->createController($command); if ($result === false) { - throw new Exception(Yii::t('yii:No help for unknown command "{command}".', array( + throw new Exception(Yii::t('yii|No help for unknown command "{command}".', array( '{command}' => $command, ))); } @@ -241,7 +241,7 @@ class HelpController extends Controller { $action = $controller->createAction($actionID); if ($action === null) { - throw new Exception(Yii::t('yii:No help for unknown sub-command "{command}".', array( + throw new Exception(Yii::t('yii|No help for unknown sub-command "{command}".', array( '{command}' => rtrim($controller->getUniqueId() . '/' . $actionID, '/'), ))); } diff --git a/framework/db/Exception.php b/framework/db/Exception.php index 1c4196f..808dc8f 100644 --- a/framework/db/Exception.php +++ b/framework/db/Exception.php @@ -40,6 +40,6 @@ class Exception extends \yii\base\Exception */ public function getName() { - return \Yii::t('yii:Database Exception'); + return \Yii::t('yii|Database Exception'); } } \ No newline at end of file diff --git a/framework/i18n/I18N.php b/framework/i18n/I18N.php index 99a4e69..2cf118f 100644 --- a/framework/i18n/I18N.php +++ b/framework/i18n/I18N.php @@ -13,7 +13,7 @@ class I18N extends Component $language = Yii::$application->language; } - if (preg_match('/^([\w\-]+):(.*)/', $message, $matches)) { + if (preg_match('/^([\w\-\.]+)\|(.*)/', $message, $matches)) { $category = $matches[1]; $message = $matches[2]; } else { diff --git a/framework/logging/EmailTarget.php b/framework/logging/EmailTarget.php index 205cd06..4167b8b 100644 --- a/framework/logging/EmailTarget.php +++ b/framework/logging/EmailTarget.php @@ -50,7 +50,7 @@ class EmailTarget extends Target $body .= $this->formatMessage($message); } $body = wordwrap($body, 70); - $subject = $this->subject === null ? \Yii::t('yii:Application Log') : $this->subject; + $subject = $this->subject === null ? \Yii::t('yii|Application Log') : $this->subject; foreach ($this->emails as $email) { $this->sendEmail($subject, $body, $email, $this->sentFrom, $this->headers); } diff --git a/framework/logging/ProfileTarget.php b/framework/logging/ProfileTarget.php index 30a207c..6030669 100644 --- a/framework/logging/ProfileTarget.php +++ b/framework/logging/ProfileTarget.php @@ -61,7 +61,7 @@ class CProfileLogRoute extends CWebLogRoute if ($value === 'summary' || $value === 'callstack') $this->_report = $value; else - throw new CException(Yii::t('yii:CProfileLogRoute.report "{report}" is invalid. Valid values include "summary" and "callstack".', + throw new CException(Yii::t('yii|CProfileLogRoute.report "{report}" is invalid. Valid values include "summary" and "callstack".', array('{report}' => $value))); } @@ -108,7 +108,7 @@ class CProfileLogRoute extends CWebLogRoute $results[$last[4]] = array($token, $delta, count($stack)); } else { - throw new CException(Yii::t('yii:CProfileLogRoute found a mismatching code block "{token}". Make sure the calls to Yii::beginProfile() and Yii::endProfile() be properly nested.', + throw new CException(Yii::t('yii|CProfileLogRoute found a mismatching code block "{token}". Make sure the calls to Yii::beginProfile() and Yii::endProfile() be properly nested.', array('{token}' => $token))); } } @@ -151,7 +151,7 @@ class CProfileLogRoute extends CWebLogRoute else $results[$token] = array($token, 1, $delta, $delta, $delta); } else - throw new CException(Yii::t('yii:CProfileLogRoute found a mismatching code block "{token}". Make sure the calls to Yii::beginProfile() and Yii::endProfile() be properly nested.', + throw new CException(Yii::t('yii|CProfileLogRoute found a mismatching code block "{token}". Make sure the calls to Yii::beginProfile() and Yii::endProfile() be properly nested.', array('{token}' => $token))); } } diff --git a/framework/validators/BooleanValidator.php b/framework/validators/BooleanValidator.php index a002ba5..acf79be 100644 --- a/framework/validators/BooleanValidator.php +++ b/framework/validators/BooleanValidator.php @@ -54,7 +54,7 @@ class BooleanValidator extends Validator } if (!$this->strict && $value != $this->trueValue && $value != $this->falseValue || $this->strict && $value !== $this->trueValue && $value !== $this->falseValue) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii:{attribute} must be either {true} or {false}.'); + $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} must be either {true} or {false}.'); $this->addError($object, $attribute, $message, array( '{true}' => $this->trueValue, '{false}' => $this->falseValue, @@ -70,7 +70,7 @@ class BooleanValidator extends Validator */ public function clientValidateAttribute($object, $attribute) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii:{attribute} must be either {true} or {false}.'); + $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} must be either {true} or {false}.'); $message = strtr($message, array( '{attribute}' => $object->getAttributeLabel($attribute), '{value}' => $object->$attribute, diff --git a/framework/validators/CaptchaValidator.php b/framework/validators/CaptchaValidator.php index e936f63..1b6c303 100644 --- a/framework/validators/CaptchaValidator.php +++ b/framework/validators/CaptchaValidator.php @@ -49,7 +49,7 @@ class CaptchaValidator extends Validator } $captcha = $this->getCaptchaAction(); if (!$captcha->validate($value, $this->caseSensitive)) { - $message = $this->message !== null ? $this->message : \Yii::t('yii:The verification code is incorrect.'); + $message = $this->message !== null ? $this->message : \Yii::t('yii|The verification code is incorrect.'); $this->addError($object, $attribute, $message); } } @@ -85,7 +85,7 @@ class CaptchaValidator extends Validator public function clientValidateAttribute($object, $attribute) { $captcha = $this->getCaptchaAction(); - $message = $this->message !== null ? $this->message : \Yii::t('yii:The verification code is incorrect.'); + $message = $this->message !== null ? $this->message : \Yii::t('yii|The verification code is incorrect.'); $message = strtr($message, array( '{attribute}' => $object->getAttributeLabel($attribute), '{value}' => $object->$attribute, diff --git a/framework/validators/CompareValidator.php b/framework/validators/CompareValidator.php index 141cb16..027ee98 100644 --- a/framework/validators/CompareValidator.php +++ b/framework/validators/CompareValidator.php @@ -96,37 +96,37 @@ class CompareValidator extends Validator case '=': case '==': if (($this->strict && $value !== $compareValue) || (!$this->strict && $value != $compareValue)) { - $message = ($this->message !== null) ? $this->message : Yii::t('yii:{attribute} must be repeated exactly.'); + $message = ($this->message !== null) ? $this->message : Yii::t('yii|{attribute} must be repeated exactly.'); $this->addError($object, $attribute, $message, array('{compareAttribute}' => $compareLabel)); } break; case '!=': if (($this->strict && $value === $compareValue) || (!$this->strict && $value == $compareValue)) { - $message = ($this->message !== null) ? $this->message : Yii::t('yii:{attribute} must not be equal to "{compareValue}".'); + $message = ($this->message !== null) ? $this->message : Yii::t('yii|{attribute} must not be equal to "{compareValue}".'); $this->addError($object, $attribute, $message, array('{compareAttribute}' => $compareLabel, '{compareValue}' => $compareValue)); } break; case '>': if ($value <= $compareValue) { - $message = ($this->message !== null) ? $this->message : Yii::t('yii:{attribute} must be greater than "{compareValue}".'); + $message = ($this->message !== null) ? $this->message : Yii::t('yii|{attribute} must be greater than "{compareValue}".'); $this->addError($object, $attribute, $message, array('{compareAttribute}' => $compareLabel, '{compareValue}' => $compareValue)); } break; case '>=': if ($value < $compareValue) { - $message = ($this->message !== null) ? $this->message : Yii::t('yii:{attribute} must be greater than or equal to "{compareValue}".'); + $message = ($this->message !== null) ? $this->message : Yii::t('yii|{attribute} must be greater than or equal to "{compareValue}".'); $this->addError($object, $attribute, $message, array('{compareAttribute}' => $compareLabel, '{compareValue}' => $compareValue)); } break; case '<': if ($value >= $compareValue) { - $message = ($this->message !== null) ? $this->message : Yii::t('yii:{attribute} must be less than "{compareValue}".'); + $message = ($this->message !== null) ? $this->message : Yii::t('yii|{attribute} must be less than "{compareValue}".'); $this->addError($object, $attribute, $message, array('{compareAttribute}' => $compareLabel, '{compareValue}' => $compareValue)); } break; case '<=': if ($value > $compareValue) { - $message = ($this->message !== null) ? $this->message : Yii::t('yii:{attribute} must be less than or equal to "{compareValue}".'); + $message = ($this->message !== null) ? $this->message : Yii::t('yii|{attribute} must be less than or equal to "{compareValue}".'); $this->addError($object, $attribute, $message, array('{compareAttribute}' => $compareLabel, '{compareValue}' => $compareValue)); } break; @@ -158,37 +158,37 @@ class CompareValidator extends Validator case '=': case '==': if ($message === null) { - $message = Yii::t('yii:{attribute} must be repeated exactly.'); + $message = Yii::t('yii|{attribute} must be repeated exactly.'); } $condition = 'value!=' . $compareValue; break; case '!=': if ($message === null) { - $message = Yii::t('yii:{attribute} must not be equal to "{compareValue}".'); + $message = Yii::t('yii|{attribute} must not be equal to "{compareValue}".'); } $condition = 'value==' . $compareValue; break; case '>': if ($message === null) { - $message = Yii::t('yii:{attribute} must be greater than "{compareValue}".'); + $message = Yii::t('yii|{attribute} must be greater than "{compareValue}".'); } $condition = 'value<=' . $compareValue; break; case '>=': if ($message === null) { - $message = Yii::t('yii:{attribute} must be greater than or equal to "{compareValue}".'); + $message = Yii::t('yii|{attribute} must be greater than or equal to "{compareValue}".'); } $condition = 'value<' . $compareValue; break; case '<': if ($message === null) { - $message = Yii::t('yii:{attribute} must be less than "{compareValue}".'); + $message = Yii::t('yii|{attribute} must be less than "{compareValue}".'); } $condition = 'value>=' . $compareValue; break; case '<=': if ($message === null) { - $message = Yii::t('yii:{attribute} must be less than or equal to "{compareValue}".'); + $message = Yii::t('yii|{attribute} must be less than or equal to "{compareValue}".'); } $condition = 'value>' . $compareValue; break; diff --git a/framework/validators/DateValidator.php b/framework/validators/DateValidator.php index af97a71..020d0ed 100644 --- a/framework/validators/DateValidator.php +++ b/framework/validators/DateValidator.php @@ -66,7 +66,7 @@ class DateValidator extends Validator } if (!$valid) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii:The format of {attribute} is invalid.'); + $message = ($this->message !== null) ? $this->message : \Yii::t('yii|The format of {attribute} is invalid.'); $this->addError($object, $attribute, $message); } } diff --git a/framework/validators/EmailValidator.php b/framework/validators/EmailValidator.php index b78b2da..7bb6bb2 100644 --- a/framework/validators/EmailValidator.php +++ b/framework/validators/EmailValidator.php @@ -63,7 +63,7 @@ class EmailValidator extends Validator return; } if (!$this->validateValue($value)) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii:{attribute} is not a valid email address.'); + $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} is not a valid email address.'); $this->addError($object, $attribute, $message); } } @@ -100,7 +100,7 @@ class EmailValidator extends Validator */ public function clientValidateAttribute($object, $attribute) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii:{attribute} is not a valid email address.'); + $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} is not a valid email address.'); $message = strtr($message, array( '{attribute}' => $object->getAttributeLabel($attribute), '{value}' => $object->$attribute, diff --git a/framework/validators/ExistValidator.php b/framework/validators/ExistValidator.php index 9d253c9..5f590ba 100644 --- a/framework/validators/ExistValidator.php +++ b/framework/validators/ExistValidator.php @@ -68,7 +68,7 @@ class ExistValidator extends Validator $query = $className::find(); $query->where(array($column->name => $value)); if (!$query->exists()) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii:{attribute} "{value}" is invalid.'); + $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} "{value}" is invalid.'); $this->addError($object, $attribute, $message); } } diff --git a/framework/validators/FileValidator.php b/framework/validators/FileValidator.php index c558b76..bde3a6f 100644 --- a/framework/validators/FileValidator.php +++ b/framework/validators/FileValidator.php @@ -115,7 +115,7 @@ class CFileValidator extends Validator return $this->emptyAttribute($object, $attribute); if (count($files) > $this->maxFiles) { - $message = $this->tooMany !== null ? $this->tooMany : \Yii::t('yii:{attribute} cannot accept more than {limit} files.'); + $message = $this->tooMany !== null ? $this->tooMany : \Yii::t('yii|{attribute} cannot accept more than {limit} files.'); $this->addError($object, $attribute, $message, array('{attribute}' => $attribute, '{limit}' => $this->maxFiles)); } else foreach ($files as $file) @@ -145,20 +145,20 @@ class CFileValidator extends Validator return $this->emptyAttribute($object, $attribute); elseif ($error == UPLOAD_ERR_INI_SIZE || $error == UPLOAD_ERR_FORM_SIZE || $this->maxSize !== null && $file->getSize() > $this->maxSize) { - $message = $this->tooLarge !== null ? $this->tooLarge : \Yii::t('yii:The file "{file}" is too large. Its size cannot exceed {limit} bytes.'); + $message = $this->tooLarge !== null ? $this->tooLarge : \Yii::t('yii|The file "{file}" is too large. Its size cannot exceed {limit} bytes.'); $this->addError($object, $attribute, $message, array('{file}' => $file->getName(), '{limit}' => $this->getSizeLimit())); } elseif ($error == UPLOAD_ERR_PARTIAL) - throw new CException(\Yii::t('yii:The file "{file}" was only partially uploaded.', array('{file}' => $file->getName()))); + throw new CException(\Yii::t('yii|The file "{file}" was only partially uploaded.', array('{file}' => $file->getName()))); elseif ($error == UPLOAD_ERR_NO_TMP_DIR) - throw new CException(\Yii::t('yii:Missing the temporary folder to store the uploaded file "{file}".', array('{file}' => $file->getName()))); + throw new CException(\Yii::t('yii|Missing the temporary folder to store the uploaded file "{file}".', array('{file}' => $file->getName()))); elseif ($error == UPLOAD_ERR_CANT_WRITE) - throw new CException(\Yii::t('yii:Failed to write the uploaded file "{file}" to disk.', array('{file}' => $file->getName()))); + throw new CException(\Yii::t('yii|Failed to write the uploaded file "{file}" to disk.', array('{file}' => $file->getName()))); elseif (defined('UPLOAD_ERR_EXTENSION') && $error == UPLOAD_ERR_EXTENSION) // available for PHP 5.2.0 or above - throw new CException(\Yii::t('yii:File upload was stopped by extension.')); + throw new CException(\Yii::t('yii|File upload was stopped by extension.')); if ($this->minSize !== null && $file->getSize() < $this->minSize) { - $message = $this->tooSmall !== null ? $this->tooSmall : \Yii::t('yii:The file "{file}" is too small. Its size cannot be smaller than {limit} bytes.'); + $message = $this->tooSmall !== null ? $this->tooSmall : \Yii::t('yii|The file "{file}" is too small. Its size cannot be smaller than {limit} bytes.'); $this->addError($object, $attribute, $message, array('{file}' => $file->getName(), '{limit}' => $this->minSize)); } @@ -170,7 +170,7 @@ class CFileValidator extends Validator $types = $this->types; if (!in_array(strtolower($file->getExtensionName()), $types)) { - $message = $this->wrongType !== null ? $this->wrongType : \Yii::t('yii:The file "{file}" cannot be uploaded. Only files with these extensions are allowed: {extensions}.'); + $message = $this->wrongType !== null ? $this->wrongType : \Yii::t('yii|The file "{file}" cannot be uploaded. Only files with these extensions are allowed: {extensions}.'); $this->addError($object, $attribute, $message, array('{file}' => $file->getName(), '{extensions}' => implode(', ', $types))); } } @@ -185,7 +185,7 @@ class CFileValidator extends Validator { if (!$this->allowEmpty) { - $message = $this->message !== null ? $this->message : \Yii::t('yii:{attribute} cannot be blank.'); + $message = $this->message !== null ? $this->message : \Yii::t('yii|{attribute} cannot be blank.'); $this->addError($object, $attribute, $message); } } diff --git a/framework/validators/NumberValidator.php b/framework/validators/NumberValidator.php index d459fb8..5ca40c6 100644 --- a/framework/validators/NumberValidator.php +++ b/framework/validators/NumberValidator.php @@ -73,21 +73,21 @@ class NumberValidator extends Validator } if ($this->integerOnly) { if (!preg_match($this->integerPattern, "$value")) { - $message = $this->message !== null ? $this->message : Yii::t('yii:{attribute} must be an integer.'); + $message = $this->message !== null ? $this->message : Yii::t('yii|{attribute} must be an integer.'); $this->addError($object, $attribute, $message); } } else { if (!preg_match($this->numberPattern, "$value")) { - $message = $this->message !== null ? $this->message : Yii::t('yii:{attribute} must be a number.'); + $message = $this->message !== null ? $this->message : Yii::t('yii|{attribute} must be a number.'); $this->addError($object, $attribute, $message); } } if ($this->min !== null && $value < $this->min) { - $message = $this->tooSmall !== null ? $this->tooSmall : Yii::t('yii:{attribute} is too small (minimum is {min}).'); + $message = $this->tooSmall !== null ? $this->tooSmall : Yii::t('yii|{attribute} is too small (minimum is {min}).'); $this->addError($object, $attribute, $message, array('{min}' => $this->min)); } if ($this->max !== null && $value > $this->max) { - $message = $this->tooBig !== null ? $this->tooBig : Yii::t('yii:{attribute} is too big (maximum is {max}).'); + $message = $this->tooBig !== null ? $this->tooBig : Yii::t('yii|{attribute} is too big (maximum is {max}).'); $this->addError($object, $attribute, $message, array('{max}' => $this->max)); } } @@ -103,8 +103,8 @@ class NumberValidator extends Validator $label = $object->getAttributeLabel($attribute); if (($message = $this->message) === null) { - $message = $this->integerOnly ? Yii::t('yii:{attribute} must be an integer.') - : Yii::t('yii:{attribute} must be a number.'); + $message = $this->integerOnly ? Yii::t('yii|{attribute} must be an integer.') + : Yii::t('yii|{attribute} must be a number.'); } $message = strtr($message, array( '{attribute}' => $label, @@ -118,7 +118,7 @@ if(!value.match($pattern)) { "; if ($this->min !== null) { if (($tooSmall = $this->tooSmall) === null) { - $tooSmall = Yii::t('yii:{attribute} is too small (minimum is {min}).'); + $tooSmall = Yii::t('yii|{attribute} is too small (minimum is {min}).'); } $tooSmall = strtr($tooSmall, array( '{attribute}' => $label, @@ -133,7 +133,7 @@ if(value<{$this->min}) { } if ($this->max !== null) { if (($tooBig = $this->tooBig) === null) { - $tooBig = Yii::t('yii:{attribute} is too big (maximum is {max}).'); + $tooBig = Yii::t('yii|{attribute} is too big (maximum is {max}).'); } $tooBig = strtr($tooBig, array( '{attribute}' => $label, diff --git a/framework/validators/RangeValidator.php b/framework/validators/RangeValidator.php index 6abf81f..9119eef 100644 --- a/framework/validators/RangeValidator.php +++ b/framework/validators/RangeValidator.php @@ -58,10 +58,10 @@ class RangeValidator extends Validator throw new InvalidConfigException('The "range" property must be specified as an array.'); } if (!$this->not && !in_array($value, $this->range, $this->strict)) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii:{attribute} should be in the list.'); + $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} should be in the list.'); $this->addError($object, $attribute, $message); } elseif ($this->not && in_array($value, $this->range, $this->strict)) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii:{attribute} should NOT be in the list.'); + $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} should NOT be in the list.'); $this->addError($object, $attribute, $message); } } @@ -80,7 +80,7 @@ class RangeValidator extends Validator } if (($message = $this->message) === null) { - $message = $this->not ? \Yii::t('yii:{attribute} should NOT be in the list.') : \Yii::t('yii:{attribute} should be in the list.'); + $message = $this->not ? \Yii::t('yii|{attribute} should NOT be in the list.') : \Yii::t('yii|{attribute} should be in the list.'); } $message = strtr($message, array( '{attribute}' => $object->getAttributeLabel($attribute), diff --git a/framework/validators/RegularExpressionValidator.php b/framework/validators/RegularExpressionValidator.php index 1e034f7..456b01e 100644 --- a/framework/validators/RegularExpressionValidator.php +++ b/framework/validators/RegularExpressionValidator.php @@ -51,7 +51,7 @@ class RegularExpressionValidator extends Validator throw new \yii\base\Exception('The "pattern" property must be specified with a valid regular expression.'); } if ((!$this->not && !preg_match($this->pattern, $value)) || ($this->not && preg_match($this->pattern, $value))) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii:{attribute} is invalid.'); + $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} is invalid.'); $this->addError($object, $attribute, $message); } } @@ -69,7 +69,7 @@ class RegularExpressionValidator extends Validator throw new \yii\base\Exception('The "pattern" property must be specified with a valid regular expression.'); } - $message = ($this->message !== null) ? $this->message : \Yii::t('yii:{attribute} is invalid.'); + $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} is invalid.'); $message = strtr($message, array( '{attribute}' => $object->getAttributeLabel($attribute), '{value}' => $object->$attribute, diff --git a/framework/validators/RequiredValidator.php b/framework/validators/RequiredValidator.php index af03221..a80733d 100644 --- a/framework/validators/RequiredValidator.php +++ b/framework/validators/RequiredValidator.php @@ -47,12 +47,12 @@ class RequiredValidator extends Validator $value = $object->$attribute; if ($this->requiredValue === null) { if ($this->strict && $value === null || !$this->strict && $this->isEmpty($value, true)) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii:{attribute} cannot be blank.'); + $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} cannot be blank.'); $this->addError($object, $attribute, $message); } } else { if (!$this->strict && $value != $this->requiredValue || $this->strict && $value !== $this->requiredValue) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii:{attribute} must be "{requiredValue}".'); + $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} must be "{requiredValue}".'); $this->addError($object, $attribute, $message, array( '{requiredValue}' => $this->requiredValue, )); @@ -71,7 +71,7 @@ class RequiredValidator extends Validator $message = $this->message; if ($this->requiredValue !== null) { if ($message === null) { - $message = \Yii::t('yii:{attribute} must be "{requiredValue}".'); + $message = \Yii::t('yii|{attribute} must be "{requiredValue}".'); } $message = strtr($message, array( '{attribute}' => $object->getAttributeLabel($attribute), @@ -85,7 +85,7 @@ if (value != " . json_encode($this->requiredValue) . ") { "; } else { if ($message === null) { - $message = \Yii::t('yii:{attribute} cannot be blank.'); + $message = \Yii::t('yii|{attribute} cannot be blank.'); } $message = strtr($message, array( '{attribute}' => $object->getAttributeLabel($attribute), diff --git a/framework/validators/StringValidator.php b/framework/validators/StringValidator.php index 4db29d3..c57e183 100644 --- a/framework/validators/StringValidator.php +++ b/framework/validators/StringValidator.php @@ -76,7 +76,7 @@ class StringValidator extends Validator } if (!is_string($value)) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii:{attribute} must be a string.'); + $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} must be a string.'); $this->addError($object, $attribute, $message); return; } @@ -88,15 +88,15 @@ class StringValidator extends Validator } if ($this->min !== null && $length < $this->min) { - $message = ($this->tooShort !== null) ? $this->tooShort : \Yii::t('yii:{attribute} is too short (minimum is {min} characters).'); + $message = ($this->tooShort !== null) ? $this->tooShort : \Yii::t('yii|{attribute} is too short (minimum is {min} characters).'); $this->addError($object, $attribute, $message, array('{min}' => $this->min)); } if ($this->max !== null && $length > $this->max) { - $message = ($this->tooLong !== null) ? $this->tooLong : \Yii::t('yii:{attribute} is too long (maximum is {max} characters).'); + $message = ($this->tooLong !== null) ? $this->tooLong : \Yii::t('yii|{attribute} is too long (maximum is {max} characters).'); $this->addError($object, $attribute, $message, array('{max}' => $this->max)); } if ($this->is !== null && $length !== $this->is) { - $message = ($this->notEqual !== null) ? $this->notEqual : \Yii::t('yii:{attribute} is of the wrong length (should be {length} characters).'); + $message = ($this->notEqual !== null) ? $this->notEqual : \Yii::t('yii|{attribute} is of the wrong length (should be {length} characters).'); $this->addError($object, $attribute, $message, array('{length}' => $this->is)); } } @@ -113,7 +113,7 @@ class StringValidator extends Validator $value = $object->$attribute; if (($notEqual = $this->notEqual) === null) { - $notEqual = \Yii::t('yii:{attribute} is of the wrong length (should be {length} characters).'); + $notEqual = \Yii::t('yii|{attribute} is of the wrong length (should be {length} characters).'); } $notEqual = strtr($notEqual, array( '{attribute}' => $label, @@ -122,7 +122,7 @@ class StringValidator extends Validator )); if (($tooShort = $this->tooShort) === null) { - $tooShort = \Yii::t('yii:{attribute} is too short (minimum is {min} characters).'); + $tooShort = \Yii::t('yii|{attribute} is too short (minimum is {min} characters).'); } $tooShort = strtr($tooShort, array( '{attribute}' => $label, @@ -131,7 +131,7 @@ class StringValidator extends Validator )); if (($tooLong = $this->tooLong) === null) { - $tooLong = \Yii::t('yii:{attribute} is too long (maximum is {max} characters).'); + $tooLong = \Yii::t('yii|{attribute} is too long (maximum is {max} characters).'); } $tooLong = strtr($tooLong, array( '{attribute}' => $label, diff --git a/framework/validators/UniqueValidator.php b/framework/validators/UniqueValidator.php index 758d44f..5c2ab86 100644 --- a/framework/validators/UniqueValidator.php +++ b/framework/validators/UniqueValidator.php @@ -86,7 +86,7 @@ class UniqueValidator extends Validator } if ($exists) { - $message = $this->message !== null ? $this->message : \Yii::t('yii:{attribute} "{value}" has already been taken.'); + $message = $this->message !== null ? $this->message : \Yii::t('yii|{attribute} "{value}" has already been taken.'); $this->addError($object, $attribute, $message); } } diff --git a/framework/validators/UrlValidator.php b/framework/validators/UrlValidator.php index 768e8d4..55d8433 100644 --- a/framework/validators/UrlValidator.php +++ b/framework/validators/UrlValidator.php @@ -55,7 +55,7 @@ class UrlValidator extends Validator if (($value = $this->validateValue($value)) !== false) { $object->$attribute = $value; } else { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii:{attribute} is not a valid URL.'); + $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} is not a valid URL.'); $this->addError($object, $attribute, $message); } } @@ -97,7 +97,7 @@ class UrlValidator extends Validator */ public function clientValidateAttribute($object, $attribute) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii:{attribute} is not a valid URL.'); + $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} is not a valid URL.'); $message = strtr($message, array( '{attribute}' => $object->getAttributeLabel($attribute), '{value}' => $object->$attribute, diff --git a/framework/web/AssetManager.php b/framework/web/AssetManager.php index bf27e6a..1c5965a 100644 --- a/framework/web/AssetManager.php +++ b/framework/web/AssetManager.php @@ -113,7 +113,7 @@ class CAssetManager extends CApplicationComponent if(($basePath=realpath($value))!==false && is_dir($basePath) && is_writable($basePath)) $this->_basePath=$basePath; else - throw new CException(Yii::t('yii:CAssetManager.basePath "{path}" is invalid. Please make sure the directory exists and is writable by the Web server process.', + throw new CException(Yii::t('yii|CAssetManager.basePath "{path}" is invalid. Please make sure the directory exists and is writable by the Web server process.', array('{path}'=>$value))); } @@ -236,7 +236,7 @@ class CAssetManager extends CApplicationComponent return $this->_published[$path]=$this->getBaseUrl().'/'.$dir; } } - throw new CException(Yii::t('yii:The asset "{asset}" to be published does not exist.', + throw new CException(Yii::t('yii|The asset "{asset}" to be published does not exist.', array('{asset}'=>$path))); } diff --git a/framework/web/Controller.php b/framework/web/Controller.php index e4c36c9..6c0bc28 100644 --- a/framework/web/Controller.php +++ b/framework/web/Controller.php @@ -45,7 +45,7 @@ class Controller extends \yii\base\Controller */ public function invalidActionParams($action, $exception) { - throw new HttpException(400, \Yii::t('yii:Your request is invalid.')); + throw new HttpException(400, \Yii::t('yii|Your request is invalid.')); } /** diff --git a/framework/web/Request.php b/framework/web/Request.php index 9566974..68d6044 100644 --- a/framework/web/Request.php +++ b/framework/web/Request.php @@ -823,7 +823,7 @@ class Request extends \yii\base\Request $valid = false; } if (!$valid) { - throw new CHttpException(400, Yii::t('yii:The CSRF token could not be verified.')); + throw new CHttpException(400, Yii::t('yii|The CSRF token could not be verified.')); } } } diff --git a/framework/web/Session.php b/framework/web/Session.php index 2b23e62..83c7ed9 100644 --- a/framework/web/Session.php +++ b/framework/web/Session.php @@ -115,7 +115,7 @@ class CHttpSession extends CApplicationComponent implements IteratorAggregate,Ar @session_start(); if(YII_DEBUG && session_id()=='') { - $message=Yii::t('yii:Failed to start session.'); + $message=Yii::t('yii|Failed to start session.'); if(function_exists('error_get_last')) { $error=error_get_last(); @@ -215,7 +215,7 @@ class CHttpSession extends CApplicationComponent implements IteratorAggregate,Ar if(is_dir($value)) session_save_path($value); else - throw new CException(Yii::t('yii:CHttpSession.savePath "{path}" is not a valid directory.', + throw new CException(Yii::t('yii|CHttpSession.savePath "{path}" is not a valid directory.', array('{path}'=>$value))); } @@ -280,7 +280,7 @@ class CHttpSession extends CApplicationComponent implements IteratorAggregate,Ar ini_set('session.use_only_cookies','1'); } else - throw new CException(Yii::t('yii:CHttpSession.cookieMode can only be "none", "allow" or "only".')); + throw new CException(Yii::t('yii|CHttpSession.cookieMode can only be "none", "allow" or "only".')); } /** @@ -304,7 +304,7 @@ class CHttpSession extends CApplicationComponent implements IteratorAggregate,Ar ini_set('session.gc_divisor','100'); } else - throw new CException(Yii::t('yii:CHttpSession.gcProbability "{value}" is invalid. It must be an integer between 0 and 100.', + throw new CException(Yii::t('yii|CHttpSession.gcProbability "{value}" is invalid. It must be an integer between 0 and 100.', array('{value}'=>$value))); } From fcf3f2c8936c9caff4a7c61565b057553fb91fb7 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Tue, 5 Feb 2013 15:21:12 -0500 Subject: [PATCH 010/117] i18n WIP --- framework/i18n/I18N.php | 4 +- framework/i18n/MessageSource.php | 111 ++++++++--------------- framework/i18n/MissingTranslationEvent.php | 35 ++++++++ framework/i18n/PhpMessageSource.php | 140 +++++------------------------ 4 files changed, 96 insertions(+), 194 deletions(-) create mode 100644 framework/i18n/MissingTranslationEvent.php diff --git a/framework/i18n/I18N.php b/framework/i18n/I18N.php index 2cf118f..fe09a84 100644 --- a/framework/i18n/I18N.php +++ b/framework/i18n/I18N.php @@ -20,7 +20,7 @@ class I18N extends Component $category = 'app'; } - $message = $this->getSource($category)->translate($category, $message, $language); + $message = $this->getMessageSource($category)->translate($category, $message, $language); if (!is_array($params)) { $params = array($params); @@ -42,7 +42,7 @@ class I18N extends Component } - public function getSource($category) + public function getMessageSource($category) { return $category === 'yii' ? $this->getMessages() : $this->getCoreMessages(); } diff --git a/framework/i18n/MessageSource.php b/framework/i18n/MessageSource.php index 17a5b00..4f6bf94 100644 --- a/framework/i18n/MessageSource.php +++ b/framework/i18n/MessageSource.php @@ -15,29 +15,36 @@ use yii\base\Component; /** * MessageSource is the base class for message translation repository classes. * - * A message source is an application component that provides message internationalization (i18n). - * It stores messages translated in different languages and provides - * these translated versions when requested. + * A message source stores message translations in some persistent storage. * - * A concrete class must implement {@link loadMessages} or override {@link translateMessage}. - * - * @property string $language The language that the source messages are written in. - * Defaults to {@link CApplication::language application language}. + * Child classes should override [[loadMessages()]] to provide translated messages. * * @author Qiang Xue * @since 2.0 */ -abstract class MessageSource extends Component +class MessageSource extends Component { /** + * @event MissingTranslationEvent an event that is triggered when a message translation is not found. + */ + const EVENT_MISSING_TRANSLATION = 'missingTranslation'; + + /** * @var boolean whether to force message translation when the source and target languages are the same. * Defaults to false, meaning translation is only performed when source and target languages are different. */ public $forceTranslation = false; + /** + * @var string the language that the original messages are in. If not set, it will use the value of + * [[\yii\base\Application::sourceLanguage]]. + */ public $sourceLanguage; private $_messages = array(); + /** + * Initializes this component. + */ public function init() { parent::init(); @@ -48,26 +55,30 @@ abstract class MessageSource extends Component /** * Loads the message translation for the specified language and category. + * Child classes should override this method to return the message translations of + * the specified language and category. * @param string $category the message category * @param string $language the target language - * @return array the loaded messages + * @return array the loaded messages. The keys are original messages, and the values + * are translated messages. */ - abstract protected function loadMessages($category, $language); + protected function loadMessages($category, $language) + { + return array(); + } /** * Translates a message to the specified language. * - * Note, if the specified language is the same as - * the {@link getLanguage source message language}, messages will NOT be translated. + * Note that unless [[forceTranslation]] is true, if the target language + * is the same as the [[sourceLanguage|source language]], the message + * will NOT be translated. * - * If the message is not found in the translations, an {@link onMissingTranslation} - * event will be raised. Handlers can mark this message or do some - * default handling. The {@link CMissingTranslationEvent::message} - * property of the event parameter will be returned. + * If a translation is not found, a [[missingTranslation]] event will be triggered. * * @param string $category the message category * @param string $message the message to be translated - * @param string $language the target language. If null (default), the {@link CApplication::getLanguage application language} will be used. + * @param string $language the target language * @return string the translated message (or the original message if translation is not needed) */ public function translate($category, $message, $language) @@ -81,8 +92,8 @@ abstract class MessageSource extends Component /** * Translates the specified message. - * If the message is not found, an {@link onMissingTranslation} - * event will be raised. + * If the message is not found, a [[missingTranslation]] event will be triggered + * and the original message will be returned. * @param string $category the category that the message belongs to * @param string $message the message to be translated * @param string $language the target language @@ -96,63 +107,17 @@ abstract class MessageSource extends Component } if (isset($this->_messages[$key][$message]) && $this->_messages[$key][$message] !== '') { return $this->_messages[$key][$message]; - } elseif ($this->hasEventHandler('onMissingTranslation')) { - $event = new CMissingTranslationEvent($this, $category, $message, $language); - $this->onMissingTranslation($event); - return $event->message; + } elseif ($this->hasEventHandlers('missingTranslation')) { + $event = new MissingTranslationEvent(array( + 'category' => $category, + 'message' => $message, + 'language' => $language, + )); + $this->trigger(self::EVENT_MISSING_TRANSLATION, $event); + return $this->_messages[$key] = $event->message; } else { return $message; } } - - /** - * Raised when a message cannot be translated. - * Handlers may log this message or do some default handling. - * The {@link CMissingTranslationEvent::message} property - * will be returned by {@link translateMessage}. - * @param CMissingTranslationEvent $event the event parameter - */ - public function onMissingTranslation($event) - { - $this->raiseEvent('onMissingTranslation', $event); - } } - -/** - * CMissingTranslationEvent represents the parameter for the {@link MessageSource::onMissingTranslation onMissingTranslation} event. - * - * @author Qiang Xue - * @package system.i18n - * @since 1.0 - */ -class CMissingTranslationEvent extends CEvent -{ - /** - * @var string the message to be translated - */ - public $message; - /** - * @var string the category that the message belongs to - */ - public $category; - /** - * @var string the ID of the language that the message is to be translated to - */ - public $language; - - /** - * Constructor. - * @param mixed $sender sender of this event - * @param string $category the category that the message belongs to - * @param string $message the message to be translated - * @param string $language the ID of the language that the message is to be translated to - */ - public function __construct($sender, $category, $message, $language) - { - parent::__construct($sender); - $this->message = $message; - $this->category = $category; - $this->language = $language; - } -} diff --git a/framework/i18n/MissingTranslationEvent.php b/framework/i18n/MissingTranslationEvent.php new file mode 100644 index 0000000..723b68c --- /dev/null +++ b/framework/i18n/MissingTranslationEvent.php @@ -0,0 +1,35 @@ + + * @since 2.0 + */ +class MissingTranslationEvent extends Event +{ + /** + * @var string the message to be translated. An event handler may overwrite this property + * with a translated version if possible. + */ + public $message; + /** + * @var string the category that the message belongs to + */ + public $category; + /** + * @var string the language ID (e.g. en_US) that the message is to be translated to + */ + public $language; +} diff --git a/framework/i18n/PhpMessageSource.php b/framework/i18n/PhpMessageSource.php index 4f8511f..2fea3b7 100644 --- a/framework/i18n/PhpMessageSource.php +++ b/framework/i18n/PhpMessageSource.php @@ -9,121 +9,35 @@ namespace yii\i18n; +use Yii; + /** * PhpMessageSource represents a message source that stores translated messages in PHP scripts. * - * PhpMessageSource uses PHP files and arrays to keep message translations. - *
      - *
    • All translations are saved under the {@link basePath} directory.
    • - *
    • Translations in one language are kept as PHP files under an individual subdirectory - * whose name is the same as the language ID. Each PHP file contains messages - * belonging to the same category, and the file name is the same as the category name.
    • - *
    • Within a PHP file, an array of (source, translation) pairs is returned. - * For example: - *
      + * PhpMessageSource uses PHP arrays to keep message translations.
      + *
      + * - Each PHP script contains one array which stores the message translations in one particular
      + *   language and for a single message category;
      + * - Each PHP script is saved as a file named as `[[basePath]]/LanguageID/CategoryName.php`;
      + * - Within each PHP script, the message translations are returned as an array like the following:
      + *
      + * ~~~
        * return array(
        *     'original message 1' => 'translated message 1',
        *     'original message 2' => 'translated message 2',
        * );
      - * 
      - *
    • - *
    - * When {@link cachingDuration} is set as a positive number, message translations will be cached. - * - * Messages for an extension class (e.g. a widget, a module) can be specially managed and used. - * In particular, if a message belongs to an extension whose class name is Xyz, then the message category - * can be specified in the format of 'Xyz.categoryName'. And the corresponding message file - * is assumed to be 'BasePath/messages/LanguageID/categoryName.php', where 'BasePath' refers to - * the directory that contains the extension class file. When using Yii::t() to translate an extension message, - * the category name should be set as 'Xyz.categoryName'. + * ~~~ * * @author Qiang Xue * @since 2.0 */ class PhpMessageSource extends MessageSource { - const CACHE_KEY_PREFIX='Yii.CPhpMessageSource.'; - - /** - * @var integer the time in seconds that the messages can remain valid in cache. - * Defaults to 0, meaning the caching is disabled. - */ - public $cachingDuration=0; - /** - * @var string the ID of the cache application component that is used to cache the messages. - * Defaults to 'cache' which refers to the primary cache application component. - * Set this property to false if you want to disable caching the messages. - */ - public $cacheID='cache'; /** * @var string the base path for all translated messages. Defaults to null, meaning * the "messages" subdirectory of the application directory (e.g. "protected/messages"). */ - public $basePath; - /** - * @var array the message paths for extensions that do not have a base class to use as category prefix. - * The format of the array should be: - *
    -	 * array(
    -	 *     'ExtensionName' => 'ext.ExtensionName.messages',
    -	 * )
    -	 * 
    - * Where the key is the name of the extension and the value is the alias to the path - * of the "messages" subdirectory of the extension. - * When using Yii::t() to translate an extension message, the category name should be - * set as 'ExtensionName.categoryName'. - * Defaults to an empty array, meaning no extensions registered. - * @since 1.1.13 - */ - public $extensionPaths=array(); - - private $_files=array(); - - /** - * Initializes the application component. - * This method overrides the parent implementation by preprocessing - * the user request data. - */ - public function init() - { - parent::init(); - if($this->basePath===null) - $this->basePath=Yii::getPathOfAlias('application.messages'); - } - - /** - * Determines the message file name based on the given category and language. - * If the category name contains a dot, it will be split into the module class name and the category name. - * In this case, the message file will be assumed to be located within the 'messages' subdirectory of - * the directory containing the module class file. - * Otherwise, the message file is assumed to be under the {@link basePath}. - * @param string $category category name - * @param string $language language ID - * @return string the message file path - */ - protected function getMessageFile($category,$language) - { - if(!isset($this->_files[$category][$language])) - { - if(($pos=strpos($category,'.'))!==false) - { - $extensionClass=substr($category,0,$pos); - $extensionCategory=substr($category,$pos+1); - // First check if there's an extension registered for this class. - if(isset($this->extensionPaths[$extensionClass])) - $this->_files[$category][$language]=Yii::getPathOfAlias($this->extensionPaths[$extensionClass]).DIRECTORY_SEPARATOR.$language.DIRECTORY_SEPARATOR.$extensionCategory.'.php'; - else - { - // No extension registered, need to find it. - $class=new ReflectionClass($extensionClass); - $this->_files[$category][$language]=dirname($class->getFileName()).DIRECTORY_SEPARATOR.'messages'.DIRECTORY_SEPARATOR.$language.DIRECTORY_SEPARATOR.$extensionCategory.'.php'; - } - } - else - $this->_files[$category][$language]=$this->basePath.DIRECTORY_SEPARATOR.$language.DIRECTORY_SEPARATOR.$category.'.php'; - } - return $this->_files[$category][$language]; - } + public $basePath = '@app/messages'; /** * Loads the message translation for the specified language and category. @@ -131,30 +45,18 @@ class PhpMessageSource extends MessageSource * @param string $language the target language * @return array the loaded messages */ - protected function loadMessages($category,$language) + protected function loadMessages($category, $language) { - $messageFile=$this->getMessageFile($category,$language); - - if($this->cachingDuration>0 && $this->cacheID!==false && ($cache=Yii::app()->getComponent($this->cacheID))!==null) - { - $key=self::CACHE_KEY_PREFIX . $messageFile; - if(($data=$cache->get($key))!==false) - return unserialize($data); - } - - if(is_file($messageFile)) - { - $messages=include($messageFile); - if(!is_array($messages)) - $messages=array(); - if(isset($cache)) - { - $dependency=new CFileCacheDependency($messageFile); - $cache->set($key,serialize($messages),$this->cachingDuration,$dependency); + $messageFile = Yii::getAlias($this->basePath) . "/$language/$category.php"; + if (is_file($messageFile)) { + $messages = include($messageFile); + if (!is_array($messages)) { + $messages = array(); } return $messages; - } - else + } else { + Yii::error("Message file not found: $messageFile", __CLASS__); return array(); + } } } \ No newline at end of file From 8b2b7915b44a5145437e78b4caa6721c4fb13e7d Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Tue, 5 Feb 2013 16:12:22 -0500 Subject: [PATCH 011/117] Renamed application to app. --- framework/YiiBase.php | 13 +++++----- framework/base/Application.php | 4 ++-- framework/base/Controller.php | 6 ++--- framework/base/ErrorHandler.php | 12 +++++----- framework/base/Module.php | 12 +++++----- framework/base/SecurityManager.php | 8 +++---- framework/base/Theme.php | 2 +- framework/base/UrlManager.php | 10 ++++---- framework/base/View.php | 28 +++++++++++----------- framework/base/Widget.php | 2 +- framework/caching/DbCache.php | 2 +- framework/caching/DbDependency.php | 2 +- framework/caching/DummyCache.php | 2 +- framework/caching/FileCache.php | 2 +- framework/console/controllers/HelpController.php | 4 ++-- .../console/controllers/MigrateController.php | 10 ++++---- framework/db/ActiveRecord.php | 2 +- framework/db/Command.php | 2 +- framework/db/Migration.php | 2 +- framework/db/Query.php | 2 +- framework/db/Schema.php | 4 ++-- framework/i18n/I18N.php | 4 ++-- framework/i18n/MessageSource.php | 2 +- framework/logging/DbTarget.php | 2 +- framework/logging/FileTarget.php | 2 +- framework/logging/ProfileTarget.php | 2 +- framework/logging/Router.php | 2 +- framework/logging/Target.php | 2 +- framework/logging/WebTarget.php | 2 +- framework/util/FileHelper.php | 4 ++-- framework/validators/CaptchaValidator.php | 4 ++-- framework/validators/StringValidator.php | 2 +- framework/web/AssetManager.php | 4 ++-- framework/web/Controller.php | 4 ++-- framework/web/Request.php | 2 +- framework/web/Sort.php | 2 +- framework/web/Theme.php | 2 +- 37 files changed, 87 insertions(+), 86 deletions(-) diff --git a/framework/YiiBase.php b/framework/YiiBase.php index be59c16..464bcea 100644 --- a/framework/YiiBase.php +++ b/framework/YiiBase.php @@ -65,7 +65,7 @@ class YiiBase /** * @var yii\base\Application the application instance */ - public static $application; + public static $app; /** * @var array registered path aliases * @see getAlias @@ -125,8 +125,8 @@ class YiiBase * * To import a class or a directory, one can use either path alias or class name (can be namespaced): * - * - `@application/components/GoogleMap`: importing the `GoogleMap` class with a path alias; - * - `@application/components/*`: importing the whole `components` directory with a path alias; + * - `@app/components/GoogleMap`: importing the `GoogleMap` class with a path alias; + * - `@app/components/*`: importing the whole `components` directory with a path alias; * - `GoogleMap`: importing the `GoogleMap` class with a class name. [[autoload()]] will be used * when this class is used for the first time. * @@ -262,6 +262,7 @@ class YiiBase * * @param string $className class name * @return boolean whether the class has been loaded successfully + * @throws Exception if the class file does not exist */ public static function autoload($className) { @@ -322,12 +323,12 @@ class YiiBase * the class. For example, * * - `\app\components\GoogleMap`: fully-qualified namespaced class. - * - `@application/components/GoogleMap`: an alias + * - `@app/components/GoogleMap`: an alias * * Below are some usage examples: * * ~~~ - * $object = \Yii::createObject('@application/components/GoogleMap'); + * $object = \Yii::createObject('@app/components/GoogleMap'); * $object = \Yii::createObject(array( * 'class' => '\app\components\GoogleMap', * 'apiKey' => 'xyz', @@ -520,6 +521,6 @@ class YiiBase */ public static function t($message, $params = array(), $language = null) { - Yii::$application->getI18N()->translate($message, $params, $language); + Yii::$app->getI18N()->translate($message, $params, $language); } } diff --git a/framework/base/Application.php b/framework/base/Application.php index e4c1333..9277e70 100644 --- a/framework/base/Application.php +++ b/framework/base/Application.php @@ -105,7 +105,7 @@ class Application extends Module */ public function __construct($id, $basePath, $config = array()) { - Yii::$application = $this; + Yii::$app = $this; $this->id = $id; $this->setBasePath($basePath); @@ -342,7 +342,7 @@ class Application extends Module */ public function registerDefaultAliases() { - Yii::$aliases['@application'] = $this->getBasePath(); + Yii::$aliases['@app'] = $this->getBasePath(); Yii::$aliases['@entry'] = dirname($_SERVER['SCRIPT_FILENAME']); Yii::$aliases['@www'] = ''; } diff --git a/framework/base/Controller.php b/framework/base/Controller.php index 58deda2..ca3b39b 100644 --- a/framework/base/Controller.php +++ b/framework/base/Controller.php @@ -72,9 +72,9 @@ class Controller extends Component * * ~~~ * return array( - * 'action1' => '@application/components/Action1', + * 'action1' => '@app/components/Action1', * 'action2' => array( - * 'class' => '@application/components/Action2', + * 'class' => '@app/components/Action2', * 'property1' => 'value1', * 'property2' => 'value2', * ), @@ -139,7 +139,7 @@ class Controller extends Component } elseif ($pos > 0) { return $this->module->runAction($route, $params); } else { - return \Yii::$application->runAction(ltrim($route, '/'), $params); + return \Yii::$app->runAction(ltrim($route, '/'), $params); } } diff --git a/framework/base/ErrorHandler.php b/framework/base/ErrorHandler.php index 211bad6..9464a92 100644 --- a/framework/base/ErrorHandler.php +++ b/framework/base/ErrorHandler.php @@ -36,7 +36,7 @@ class ErrorHandler extends Component public $discardExistingOutput = true; /** * @var string the route (eg 'site/error') to the controller action that will be used to display external errors. - * Inside the action, it can retrieve the error information by \Yii::$application->errorHandler->error. + * Inside the action, it can retrieve the error information by \Yii::$app->errorHandler->error. * This property defaults to null, meaning ErrorHandler will handle the error display. */ public $errorAction; @@ -71,14 +71,14 @@ class ErrorHandler extends Component protected function render($exception) { if ($this->errorAction !== null) { - \Yii::$application->runAction($this->errorAction); - } elseif (\Yii::$application instanceof \yii\web\Application) { + \Yii::$app->runAction($this->errorAction); + } elseif (\Yii::$app instanceof \yii\web\Application) { if (!headers_sent()) { $errorCode = $exception instanceof HttpException ? $exception->statusCode : 500; header("HTTP/1.0 $errorCode " . get_class($exception)); } if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest') { - \Yii::$application->renderException($exception); + \Yii::$app->renderException($exception); } else { $view = new View($this); if (!YII_DEBUG || $exception instanceof UserException) { @@ -91,7 +91,7 @@ class ErrorHandler extends Component )); } } else { - \Yii::$application->renderException($exception); + \Yii::$app->renderException($exception); } } @@ -239,7 +239,7 @@ class ErrorHandler extends Component public function htmlEncode($text) { - return htmlspecialchars($text, ENT_QUOTES, \Yii::$application->charset); + return htmlspecialchars($text, ENT_QUOTES, \Yii::$app->charset); } public function clearOutput() diff --git a/framework/base/Module.php b/framework/base/Module.php index dcb468c..3217471 100644 --- a/framework/base/Module.php +++ b/framework/base/Module.php @@ -73,9 +73,9 @@ abstract class Module extends Component * * ~~~ * array( - * 'account' => '@application/controllers/UserController', + * 'account' => '@app/controllers/UserController', * 'article' => array( - * 'class' => '@application/controllers/PostController', + * 'class' => '@app/controllers/PostController', * 'pageTitle' => 'something new', * ), * ) @@ -311,7 +311,7 @@ abstract class Module extends Component * * ~~~ * array( - * '@models' => '@application/models', // an existing alias + * '@models' => '@app/models', // an existing alias * '@backend' => __DIR__ . '/../backend', // a directory * ) * ~~~ @@ -563,10 +563,10 @@ abstract class Module extends Component if (is_array($result)) { /** @var $controller Controller */ list($controller, $actionID) = $result; - $oldController = Yii::$application->controller; - Yii::$application->controller = $controller; + $oldController = Yii::$app->controller; + Yii::$app->controller = $controller; $status = $controller->runAction($actionID, $params); - Yii::$application->controller = $oldController; + Yii::$app->controller = $oldController; return $status; } else { throw new InvalidRouteException('Unable to resolve the request: ' . trim($this->getUniqueId() . '/' . $route, '/')); diff --git a/framework/base/SecurityManager.php b/framework/base/SecurityManager.php index a3fb654..18ccb03 100644 --- a/framework/base/SecurityManager.php +++ b/framework/base/SecurityManager.php @@ -59,12 +59,12 @@ class SecurityManager extends Component if ($this->_validationKey !== null) { return $this->_validationKey; } else { - if (($key = \Yii::$application->getGlobalState(self::STATE_VALIDATION_KEY)) !== null) { + if (($key = \Yii::$app->getGlobalState(self::STATE_VALIDATION_KEY)) !== null) { $this->setValidationKey($key); } else { $key = $this->generateRandomKey(); $this->setValidationKey($key); - \Yii::$application->setGlobalState(self::STATE_VALIDATION_KEY, $key); + \Yii::$app->setGlobalState(self::STATE_VALIDATION_KEY, $key); } return $this->_validationKey; } @@ -92,12 +92,12 @@ class SecurityManager extends Component if ($this->_encryptionKey !== null) { return $this->_encryptionKey; } else { - if (($key = \Yii::$application->getGlobalState(self::STATE_ENCRYPTION_KEY)) !== null) { + if (($key = \Yii::$app->getGlobalState(self::STATE_ENCRYPTION_KEY)) !== null) { $this->setEncryptionKey($key); } else { $key = $this->generateRandomKey(); $this->setEncryptionKey($key); - \Yii::$application->setGlobalState(self::STATE_ENCRYPTION_KEY, $key); + \Yii::$app->setGlobalState(self::STATE_ENCRYPTION_KEY, $key); } return $this->_encryptionKey; } diff --git a/framework/base/Theme.php b/framework/base/Theme.php index 03f8f55..52d5245 100644 --- a/framework/base/Theme.php +++ b/framework/base/Theme.php @@ -58,7 +58,7 @@ class Theme extends Component if (empty($this->pathMap)) { if ($this->basePath !== null) { $this->basePath = FileHelper::ensureDirectory($this->basePath); - $this->pathMap = array(Yii::$application->getBasePath() => $this->basePath); + $this->pathMap = array(Yii::$app->getBasePath() => $this->basePath); } else { throw new InvalidConfigException("Theme::basePath must be set."); } diff --git a/framework/base/UrlManager.php b/framework/base/UrlManager.php index db6b598..e17685d 100644 --- a/framework/base/UrlManager.php +++ b/framework/base/UrlManager.php @@ -112,7 +112,7 @@ use \yii\base\Component; * * * UrlManager is a default application component that may be accessed via - * {@link \Yii::$application->urlManager}. + * {@link \Yii::$app->urlManager}. * * @property string $baseUrl The base URL of the application (the part after host name and before query string). * If {@link showScriptName} is true, it will include the script name part. @@ -214,7 +214,7 @@ class UrlManager extends Component { if(empty($this->rules) || $this->getUrlFormat()===self::GET_FORMAT) return; - if($this->cacheID!==false && ($cache=\Yii::$application->getComponent($this->cacheID))!==null) + if($this->cacheID!==false && ($cache=\Yii::$app->getComponent($this->cacheID))!==null) { $hash=md5(serialize($this->rules)); if(($data=$cache->get(self::CACHE_KEY))!==false && isset($data[1]) && $data[1]===$hash) @@ -464,9 +464,9 @@ class UrlManager extends Component else { if($this->showScriptName) - $this->_baseUrl=\Yii::$application->getRequest()->getScriptUrl(); + $this->_baseUrl=\Yii::$app->getRequest()->getScriptUrl(); else - $this->_baseUrl=\Yii::$application->getRequest()->getBaseUrl(); + $this->_baseUrl=\Yii::$app->getRequest()->getBaseUrl(); return $this->_baseUrl; } } @@ -755,7 +755,7 @@ class CUrlRule extends CBaseUrlRule if($this->hasHostInfo) { - $hostInfo=\Yii::$application->getRequest()->getHostInfo(); + $hostInfo=\Yii::$app->getRequest()->getHostInfo(); if(stripos($url,$hostInfo)===0) $url=substr($url,strlen($hostInfo)); } diff --git a/framework/base/View.php b/framework/base/View.php index 410e3c5..9caa4ef 100644 --- a/framework/base/View.php +++ b/framework/base/View.php @@ -148,7 +148,7 @@ class View extends Component */ public function renderFile($file, $params = array()) { - $renderer = Yii::$application->getViewRenderer(); + $renderer = Yii::$app->getViewRenderer(); if ($renderer !== null) { return $renderer->render($this, $file, $params); } else { @@ -344,7 +344,7 @@ class View extends Component * * A view name can be specified in one of the following formats: * - * - path alias (e.g. "@application/views/site/index"); + * - 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. @@ -374,7 +374,7 @@ class View extends Component $view .= '.php'; } if (strncmp($view, '@', 1) === 0) { - // e.g. "@application/views/common" + // e.g. "@app/views/common" if (($file = Yii::getAlias($view)) === false) { throw new InvalidConfigException("Invalid path alias: $view"); } @@ -386,18 +386,18 @@ class View extends Component $class = new \ReflectionClass($this->owner); $file = dirname($class->getFileName()) . DIRECTORY_SEPARATOR . 'views' . DIRECTORY_SEPARATOR . $view; } else { - $file = Yii::$application->getViewPath() . DIRECTORY_SEPARATOR . $view; + $file = Yii::$app->getViewPath() . DIRECTORY_SEPARATOR . $view; } - } elseif (strncmp($view, '//', 2) !== 0 && Yii::$application->controller !== null) { + } elseif (strncmp($view, '//', 2) !== 0 && Yii::$app->controller !== null) { // e.g. "/site/index" - $file = Yii::$application->controller->module->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); + $file = Yii::$app->controller->module->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); } else { // e.g. "//layouts/main" - $file = Yii::$application->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); + $file = Yii::$app->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); } if (is_file($file)) { - if ($this->enableTheme && ($theme = Yii::$application->getTheme()) !== null) { + if ($this->enableTheme && ($theme = Yii::$app->getTheme()) !== null) { $file = $theme->apply($file); } return $this->enableI18N ? FileHelper::localize($file, $this->language, $this->sourceLanguage) : $file; @@ -423,7 +423,7 @@ class View extends Component * * Like view names, a layout name can take several formats: * - * - path alias (e.g. "@application/views/layouts/main"); + * - path alias (e.g. "@app/views/layouts/main"); * - 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; * - relative path (e.g. "main"): the actual layout layout file will be looked for under the @@ -444,10 +444,10 @@ class View extends Component { /** @var $module Module */ if (is_string($this->layout)) { - if (Yii::$application->controller) { - $module = Yii::$application->controller->module; + if (Yii::$app->controller) { + $module = Yii::$app->controller->module; } else { - $module = Yii::$application; + $module = Yii::$app; } $view = $this->layout; } elseif ($this->owner instanceof Controller) { @@ -477,13 +477,13 @@ class View extends Component throw new InvalidConfigException("Invalid path alias: $view"); } } elseif (strncmp($view, '/', 1) === 0) { - $file = Yii::$application->getLayoutPath() . DIRECTORY_SEPARATOR . $view; + $file = Yii::$app->getLayoutPath() . DIRECTORY_SEPARATOR . $view; } else { $file = $module->getLayoutPath() . DIRECTORY_SEPARATOR . $view; } if (is_file($file)) { - if ($this->enableTheme && ($theme = Yii::$application->getTheme()) !== null) { + if ($this->enableTheme && ($theme = Yii::$app->getTheme()) !== null) { $file = $theme->apply($file); } return $this->enableI18N ? FileHelper::localize($file, $this->language, $this->sourceLanguage) : $file; diff --git a/framework/base/Widget.php b/framework/base/Widget.php index bdec634..b494387 100644 --- a/framework/base/Widget.php +++ b/framework/base/Widget.php @@ -80,7 +80,7 @@ class Widget extends Component * To determine which view file should be rendered, the method calls [[findViewFile()]] which * will search in the directories as specified by [[basePath]]. * - * View name can be a path alias representing an absolute file path (e.g. `@application/views/layout/index`), + * View name can be a path alias representing an absolute file path (e.g. `@app/views/layout/index`), * or a path relative to [[basePath]]. The file suffix is optional and defaults to `.php` if not given * in the view name. * diff --git a/framework/caching/DbCache.php b/framework/caching/DbCache.php index 4b84bfd..75f13b0 100644 --- a/framework/caching/DbCache.php +++ b/framework/caching/DbCache.php @@ -74,7 +74,7 @@ class DbCache extends Cache public function getDb() { if ($this->_db === null) { - $db = \Yii::$application->getComponent($this->connectionID); + $db = \Yii::$app->getComponent($this->connectionID); if ($db instanceof Connection) { $this->_db = $db; } else { diff --git a/framework/caching/DbDependency.php b/framework/caching/DbDependency.php index 7ffdb4e..e4cb547 100644 --- a/framework/caching/DbDependency.php +++ b/framework/caching/DbDependency.php @@ -91,7 +91,7 @@ class DbDependency extends Dependency public function getDb() { if ($this->_db === null) { - $db = \Yii::$application->getComponent($this->connectionID); + $db = \Yii::$app->getComponent($this->connectionID); if ($db instanceof Connection) { $this->_db = $db; } else { diff --git a/framework/caching/DummyCache.php b/framework/caching/DummyCache.php index f6e8a44..1f7a0c5 100644 --- a/framework/caching/DummyCache.php +++ b/framework/caching/DummyCache.php @@ -13,7 +13,7 @@ namespace yii\caching; * DummyCache is a placeholder cache component. * * DummyCache does not cache anything. It is provided so that one can always configure - * a 'cache' application component and save the check of existence of `\Yii::$application->cache`. + * a 'cache' application component and save the check of existence of `\Yii::$app->cache`. * By replacing DummyCache with some other cache component, one can quickly switch from * non-caching mode to caching mode. * diff --git a/framework/caching/FileCache.php b/framework/caching/FileCache.php index f97861f..bf46fb6 100644 --- a/framework/caching/FileCache.php +++ b/framework/caching/FileCache.php @@ -28,7 +28,7 @@ class FileCache extends Cache /** * @var string the directory to store cache files. You may use path alias here. */ - public $cachePath = '@application/runtime/cache'; + public $cachePath = '@app/runtime/cache'; /** * @var string cache file suffix. Defaults to '.bin'. */ diff --git a/framework/console/controllers/HelpController.php b/framework/console/controllers/HelpController.php index 4a37f3d..5ad3bbc 100644 --- a/framework/console/controllers/HelpController.php +++ b/framework/console/controllers/HelpController.php @@ -55,7 +55,7 @@ class HelpController extends Controller public function actionIndex($command = null) { if ($command !== null) { - $result = Yii::$application->createController($command); + $result = Yii::$app->createController($command); if ($result === false) { throw new Exception(Yii::t('yii|No help for unknown command "{command}".', array( '{command}' => $command, @@ -81,7 +81,7 @@ class HelpController extends Controller */ public function getCommands() { - $commands = $this->getModuleCommands(Yii::$application); + $commands = $this->getModuleCommands(Yii::$app); sort($commands); return array_unique($commands); } diff --git a/framework/console/controllers/MigrateController.php b/framework/console/controllers/MigrateController.php index 62e7d40..0f05d61 100644 --- a/framework/console/controllers/MigrateController.php +++ b/framework/console/controllers/MigrateController.php @@ -70,7 +70,7 @@ class MigrateController extends Controller * @var string the directory storing the migration classes. This can be either * a path alias or a directory. */ - public $migrationPath = '@application/migrations'; + public $migrationPath = '@app/migrations'; /** * @var string the name of the table for keeping applied migration information. */ @@ -82,7 +82,7 @@ class MigrateController extends Controller public $connectionID = 'db'; /** * @var string the template file for generating new migrations. - * This can be either a path alias (e.g. "@application/migrations/template.php") + * This can be either a path alias (e.g. "@app/migrations/template.php") * or a file path. */ public $templateFile = '@yii/views/migration.php'; @@ -121,7 +121,7 @@ class MigrateController extends Controller } $this->migrationPath = $path; - $this->db = Yii::$application->getComponent($this->connectionID); + $this->db = Yii::$app->getComponent($this->connectionID); if (!$this->db instanceof Connection) { throw new Exception("Invalid DB connection \"{$this->connectionID}\"."); } @@ -150,7 +150,7 @@ class MigrateController extends Controller { if (($migrations = $this->getNewMigrations()) === array()) { echo "No new migration found. Your system is up-to-date.\n"; - Yii::$application->end(); + Yii::$app->end(); } $total = count($migrations); @@ -576,7 +576,7 @@ class MigrateController extends Controller if ($this->db !== null) { return $this->db; } else { - $this->db = Yii::$application->getComponent($this->connectionID); + $this->db = Yii::$app->getComponent($this->connectionID); if ($this->db instanceof Connection) { return $this->db; } else { diff --git a/framework/db/ActiveRecord.php b/framework/db/ActiveRecord.php index c6d3d81..0b2451f 100644 --- a/framework/db/ActiveRecord.php +++ b/framework/db/ActiveRecord.php @@ -96,7 +96,7 @@ class ActiveRecord extends Model */ public static function getDb() { - return \Yii::$application->getDb(); + return \Yii::$app->getDb(); } /** diff --git a/framework/db/Command.php b/framework/db/Command.php index f893c8c..bb68f33 100644 --- a/framework/db/Command.php +++ b/framework/db/Command.php @@ -387,7 +387,7 @@ class Command extends \yii\base\Component /** @var $cache \yii\caching\Cache */ if ($db->enableQueryCache && $method !== '') { - $cache = \Yii::$application->getComponent($db->queryCacheID); + $cache = \Yii::$app->getComponent($db->queryCacheID); } if (isset($cache)) { diff --git a/framework/db/Migration.php b/framework/db/Migration.php index 6dbaa78..c12da29 100644 --- a/framework/db/Migration.php +++ b/framework/db/Migration.php @@ -51,7 +51,7 @@ class Migration extends \yii\base\Component { parent::init(); if ($this->db === null) { - $this->db = \Yii::$application->getComponent('db'); + $this->db = \Yii::$app->getComponent('db'); } } diff --git a/framework/db/Query.php b/framework/db/Query.php index 10bba08..ffc6206 100644 --- a/framework/db/Query.php +++ b/framework/db/Query.php @@ -117,7 +117,7 @@ class Query extends \yii\base\Component public function createCommand($db = null) { if ($db === null) { - $db = \Yii::$application->db; + $db = \Yii::$app->db; } $sql = $db->getQueryBuilder()->build($this); return $db->createCommand($sql, $this->params); diff --git a/framework/db/Schema.php b/framework/db/Schema.php index 7415bee..6306776 100644 --- a/framework/db/Schema.php +++ b/framework/db/Schema.php @@ -87,7 +87,7 @@ abstract class Schema extends \yii\base\Object $realName = $this->getRealTableName($name); /** @var $cache Cache */ - if ($db->enableSchemaCache && ($cache = \Yii::$application->getComponent($db->schemaCacheID)) !== null && !in_array($name, $db->schemaCacheExclude, true)) { + 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); @@ -171,7 +171,7 @@ abstract class Schema extends \yii\base\Object public function refresh() { /** @var $cache \yii\caching\Cache */ - if ($this->db->enableSchemaCache && ($cache = \Yii::$application->getComponent($this->db->schemaCacheID)) !== null) { + if ($this->db->enableSchemaCache && ($cache = \Yii::$app->getComponent($this->db->schemaCacheID)) !== null) { foreach ($this->_tables as $name => $table) { $cache->delete($this->getCacheKey($cache, $name)); } diff --git a/framework/i18n/I18N.php b/framework/i18n/I18N.php index fe09a84..42b7e07 100644 --- a/framework/i18n/I18N.php +++ b/framework/i18n/I18N.php @@ -10,10 +10,10 @@ class I18N extends Component public function translate($message, $params = array(), $language = null) { if ($language === null) { - $language = Yii::$application->language; + $language = Yii::$app->language; } - if (preg_match('/^([\w\-\.]+)\|(.*)/', $message, $matches)) { + if (strpos($message, '|') !== false && preg_match('/^([\w\-\.]+)\|(.*)/', $message, $matches)) { $category = $matches[1]; $message = $matches[2]; } else { diff --git a/framework/i18n/MessageSource.php b/framework/i18n/MessageSource.php index 4f6bf94..e1df935 100644 --- a/framework/i18n/MessageSource.php +++ b/framework/i18n/MessageSource.php @@ -49,7 +49,7 @@ class MessageSource extends Component { parent::init(); if ($this->sourceLanguage === null) { - $this->sourceLanguage = Yii::$application->sourceLanguage; + $this->sourceLanguage = Yii::$app->sourceLanguage; } } diff --git a/framework/logging/DbTarget.php b/framework/logging/DbTarget.php index 129e4d4..8f38eb8 100644 --- a/framework/logging/DbTarget.php +++ b/framework/logging/DbTarget.php @@ -69,7 +69,7 @@ class DbTarget extends Target public function getDb() { if ($this->_db === null) { - $db = \Yii::$application->getComponent($this->connectionID); + $db = \Yii::$app->getComponent($this->connectionID); if ($db instanceof Connection) { $this->_db = $db; } else { diff --git a/framework/logging/FileTarget.php b/framework/logging/FileTarget.php index 0eb897e..e2dd56b 100644 --- a/framework/logging/FileTarget.php +++ b/framework/logging/FileTarget.php @@ -48,7 +48,7 @@ class FileTarget extends Target { parent::init(); if ($this->logFile === null) { - $this->logFile = \Yii::$application->getRuntimePath() . DIRECTORY_SEPARATOR . 'application.log'; + $this->logFile = \Yii::$app->getRuntimePath() . DIRECTORY_SEPARATOR . 'application.log'; } else { $this->logFile = \Yii::getAlias($this->logFile); } diff --git a/framework/logging/ProfileTarget.php b/framework/logging/ProfileTarget.php index 6030669..d80ec18 100644 --- a/framework/logging/ProfileTarget.php +++ b/framework/logging/ProfileTarget.php @@ -71,7 +71,7 @@ class CProfileLogRoute extends CWebLogRoute */ public function processLogs($logs) { - $app = \Yii::$application; + $app = \Yii::$app; if (!($app instanceof CWebApplication) || $app->getRequest()->getIsAjaxRequest()) return; diff --git a/framework/logging/Router.php b/framework/logging/Router.php index 2e6a8dd..1e1f16c 100644 --- a/framework/logging/Router.php +++ b/framework/logging/Router.php @@ -52,7 +52,7 @@ use yii\base\Application; * as follows: * * ~~~ - * Yii::$application->log->targets['file']->enabled = false; + * Yii::$app->log->targets['file']->enabled = false; * ~~~ * * @author Qiang Xue diff --git a/framework/logging/Target.php b/framework/logging/Target.php index c9e175a..a75a512 100644 --- a/framework/logging/Target.php +++ b/framework/logging/Target.php @@ -110,7 +110,7 @@ abstract class Target extends \yii\base\Component protected function getContextMessage() { $context = array(); - if ($this->logUser && ($user = \Yii::$application->getComponent('user', false)) !== null) { + if ($this->logUser && ($user = \Yii::$app->getComponent('user', false)) !== null) { $context[] = 'User: ' . $user->getName() . ' (ID: ' . $user->getId() . ')'; } diff --git a/framework/logging/WebTarget.php b/framework/logging/WebTarget.php index 6ce8ea0..7198b3a 100644 --- a/framework/logging/WebTarget.php +++ b/framework/logging/WebTarget.php @@ -46,7 +46,7 @@ class CWebLogRoute extends CLogRoute */ protected function render($view, $data) { - $app = \Yii::$application; + $app = \Yii::$app; $isAjax = $app->getRequest()->getIsAjaxRequest(); if ($this->showInFireBug) diff --git a/framework/util/FileHelper.php b/framework/util/FileHelper.php index c65e4f0..6565c77 100644 --- a/framework/util/FileHelper.php +++ b/framework/util/FileHelper.php @@ -91,10 +91,10 @@ class FileHelper public static function localize($file, $language = null, $sourceLanguage = null) { if ($language === null) { - $language = \Yii::$application->getLanguage(); + $language = \Yii::$app->getLanguage(); } if ($sourceLanguage === null) { - $sourceLanguage = \Yii::$application->sourceLanguage; + $sourceLanguage = \Yii::$app->sourceLanguage; } if ($language === $sourceLanguage) { return $file; diff --git a/framework/validators/CaptchaValidator.php b/framework/validators/CaptchaValidator.php index 1b6c303..2303333 100644 --- a/framework/validators/CaptchaValidator.php +++ b/framework/validators/CaptchaValidator.php @@ -61,13 +61,13 @@ class CaptchaValidator extends Validator public function getCaptchaAction() { if (strpos($this->captchaAction, '/') !== false) { // contains controller or module - $ca = \Yii::$application->createController($this->captchaAction); + $ca = \Yii::$app->createController($this->captchaAction); if ($ca !== null) { list($controller, $actionID) = $ca; $action = $controller->createAction($actionID); } } else { - $action = \Yii::$application->getController()->createAction($this->captchaAction); + $action = \Yii::$app->getController()->createAction($this->captchaAction); } if ($action === null) { diff --git a/framework/validators/StringValidator.php b/framework/validators/StringValidator.php index c57e183..b39abb8 100644 --- a/framework/validators/StringValidator.php +++ b/framework/validators/StringValidator.php @@ -82,7 +82,7 @@ class StringValidator extends Validator } if (function_exists('mb_strlen') && $this->encoding !== false) { - $length = mb_strlen($value, $this->encoding ? $this->encoding : \Yii::$application->charset); + $length = mb_strlen($value, $this->encoding ? $this->encoding : \Yii::$app->charset); } else { $length = strlen($value); } diff --git a/framework/web/AssetManager.php b/framework/web/AssetManager.php index 1c5965a..0d3248b 100644 --- a/framework/web/AssetManager.php +++ b/framework/web/AssetManager.php @@ -97,7 +97,7 @@ class CAssetManager extends CApplicationComponent { if($this->_basePath===null) { - $request=\Yii::$application->getRequest(); + $request=\Yii::$app->getRequest(); $this->setBasePath(dirname($request->getScriptFile()).DIRECTORY_SEPARATOR.self::DEFAULT_BASEPATH); } return $this->_basePath; @@ -125,7 +125,7 @@ class CAssetManager extends CApplicationComponent { if($this->_baseUrl===null) { - $request=\Yii::$application->getRequest(); + $request=\Yii::$app->getRequest(); $this->setBaseUrl($request->getBaseUrl().'/'.self::DEFAULT_BASEPATH); } return $this->_baseUrl; diff --git a/framework/web/Controller.php b/framework/web/Controller.php index 6c0bc28..22cb8e7 100644 --- a/framework/web/Controller.php +++ b/framework/web/Controller.php @@ -59,9 +59,9 @@ class Controller extends \yii\base\Controller else { $name = ucfirst(basename($this->id)); if($this->action!==null && strcasecmp($this->action->id,$this->defaultAction)) - return $this->_pageTitle=\Yii::$application->name.' - '.ucfirst($this->action->id).' '.$name; + return $this->_pageTitle=\Yii::$app->name.' - '.ucfirst($this->action->id).' '.$name; else - return $this->_pageTitle=\Yii::$application->name.' - '.$name; + return $this->_pageTitle=\Yii::$app->name.' - '.$name; } } diff --git a/framework/web/Request.php b/framework/web/Request.php index 68d6044..8337382 100644 --- a/framework/web/Request.php +++ b/framework/web/Request.php @@ -87,7 +87,7 @@ class Request extends \yii\base\Request } if ($this->enableCsrfValidation) { - \Yii::$application->on('beginRequest', array($this, 'validateCsrfToken')); + \Yii::$app->on('beginRequest', array($this, 'validateCsrfToken')); } } diff --git a/framework/web/Sort.php b/framework/web/Sort.php index 12e16a5..6b128e7 100644 --- a/framework/web/Sort.php +++ b/framework/web/Sort.php @@ -307,7 +307,7 @@ class CSort extends CComponent $directions = array($attribute => $descending); } - $url = $this->createUrl(\Yii::$application->getController(), $directions); + $url = $this->createUrl(\Yii::$app->getController(), $directions); return $this->createLink($attribute, $label, $url, $htmlOptions); } diff --git a/framework/web/Theme.php b/framework/web/Theme.php index 5dcd601..22fe608 100644 --- a/framework/web/Theme.php +++ b/framework/web/Theme.php @@ -126,7 +126,7 @@ class CTheme extends CComponent $module=$module->getParentModule(); } if($module===null) - $layoutName=\Yii::$application->layout; + $layoutName=\Yii::$app->layout; else { $layoutName=$module->layout; From a07a67ad8d230f614a550461a9439b3a71fa4f3b Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Tue, 5 Feb 2013 16:17:07 -0500 Subject: [PATCH 012/117] minor fix of console commands. --- framework/console/controllers/HelpController.php | 4 ++++ framework/console/controllers/MigrateController.php | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/framework/console/controllers/HelpController.php b/framework/console/controllers/HelpController.php index 5ad3bbc..3e2556c 100644 --- a/framework/console/controllers/HelpController.php +++ b/framework/console/controllers/HelpController.php @@ -252,6 +252,7 @@ class HelpController extends Controller } $tags = $this->parseComment($method->getDocComment()); + $options = $this->getOptionHelps($controller); if ($tags['description'] !== '') { echo "\nDESCRIPTION"; @@ -271,6 +272,9 @@ class HelpController extends Controller if (!empty($optional)) { echo ' [' . implode('] [', array_keys($optional)) . ']'; } + if (!empty($options)) { + echo ' [...options...]'; + } echo "\n\n"; if (!empty($required) || !empty($optional)) { diff --git a/framework/console/controllers/MigrateController.php b/framework/console/controllers/MigrateController.php index 0f05d61..59dcb3f 100644 --- a/framework/console/controllers/MigrateController.php +++ b/framework/console/controllers/MigrateController.php @@ -22,8 +22,8 @@ use yii\util\ArrayHelper; * * 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. + * 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. From 1c375b90975907fd6e5894d37478ffbcc00273ea Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Wed, 6 Feb 2013 10:47:40 -0500 Subject: [PATCH 013/117] bug fixes. --- framework/YiiBase.php | 2 +- framework/base/Module.php | 3 ++- framework/i18n/I18N.php | 26 ++++++++++----------- framework/util/FileHelper.php | 2 +- framework/views/error.php | 2 +- framework/web/Application.php | 9 ++------ framework/web/Controller.php | 54 ------------------------------------------- 7 files changed, 20 insertions(+), 78 deletions(-) diff --git a/framework/YiiBase.php b/framework/YiiBase.php index 464bcea..d57c92d 100644 --- a/framework/YiiBase.php +++ b/framework/YiiBase.php @@ -521,6 +521,6 @@ class YiiBase */ public static function t($message, $params = array(), $language = null) { - Yii::$app->getI18N()->translate($message, $params, $language); + return Yii::$app->getI18N()->translate($message, $params, $language); } } diff --git a/framework/base/Module.php b/framework/base/Module.php index 3217471..3ac53ff 100644 --- a/framework/base/Module.php +++ b/framework/base/Module.php @@ -569,7 +569,7 @@ abstract class Module extends Component Yii::$app->controller = $oldController; return $status; } else { - throw new InvalidRouteException('Unable to resolve the request: ' . trim($this->getUniqueId() . '/' . $route, '/')); + throw new InvalidRouteException('Unable to resolve the request "' . trim($this->getUniqueId() . '/' . $route, '/') . '".'); } } @@ -607,6 +607,7 @@ abstract class Module extends Component $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; diff --git a/framework/i18n/I18N.php b/framework/i18n/I18N.php index 42b7e07..ab87dfc 100644 --- a/framework/i18n/I18N.php +++ b/framework/i18n/I18N.php @@ -20,19 +20,19 @@ class I18N extends Component $category = 'app'; } - $message = $this->getMessageSource($category)->translate($category, $message, $language); - - if (!is_array($params)) { - $params = array($params); - } - - if (isset($params[0])) { - $message = $this->getPluralFormat($message, $params[0], $language); - if (!isset($params['{n}'])) { - $params['{n}'] = $params[0]; - } - unset($params[0]); - } +// $message = $this->getMessageSource($category)->translate($category, $message, $language); +// +// if (!is_array($params)) { +// $params = array($params); +// } +// +// if (isset($params[0])) { +// $message = $this->getPluralFormat($message, $params[0], $language); +// if (!isset($params['{n}'])) { +// $params['{n}'] = $params[0]; +// } +// unset($params[0]); +// } return $params === array() ? $message : strtr($message, $params); } diff --git a/framework/util/FileHelper.php b/framework/util/FileHelper.php index 6565c77..996fba0 100644 --- a/framework/util/FileHelper.php +++ b/framework/util/FileHelper.php @@ -91,7 +91,7 @@ class FileHelper public static function localize($file, $language = null, $sourceLanguage = null) { if ($language === null) { - $language = \Yii::$app->getLanguage(); + $language = \Yii::$app->language; } if ($sourceLanguage === null) { $sourceLanguage = \Yii::$app->sourceLanguage; diff --git a/framework/views/error.php b/framework/views/error.php index dbbc848..c1700f2 100644 --- a/framework/views/error.php +++ b/framework/views/error.php @@ -50,7 +50,7 @@ $owner = $this->owner; -

    +

    htmlEncode($exception instanceof \yii\base\Exception ? $exception->getName() : get_class($exception)); ?>

    htmlEncode($exception->getMessage()))?>

    The above error occurred while the Web server was processing your request. diff --git a/framework/web/Application.php b/framework/web/Application.php index a25df7a..fcc8910 100644 --- a/framework/web/Application.php +++ b/framework/web/Application.php @@ -32,13 +32,8 @@ class Application extends \yii\base\Application */ public function processRequest() { - $route = $this->resolveRequest(); - return $this->runController($route, null); - } - - protected function resolveRequest() - { - return array(); + $route = isset($_GET['r']) ? $_GET['r'] : ''; + return $this->runAction($route, $_GET); } /** diff --git a/framework/web/Controller.php b/framework/web/Controller.php index 22cb8e7..c694f9a 100644 --- a/framework/web/Controller.php +++ b/framework/web/Controller.php @@ -9,10 +9,6 @@ namespace yii\web; -use yii\base\Action; -use yii\base\Exception; -use yii\base\HttpException; - /** * Controller is the base class of Web controllers. * @@ -22,54 +18,4 @@ use yii\base\HttpException; */ class Controller extends \yii\base\Controller { - private $_pageTitle; - - /** - * Returns the request parameters that will be used for action parameter binding. - * Default implementation simply returns an empty array. - * Child classes may override this method to customize the parameters to be provided - * for action parameter binding (e.g. `$_GET`). - * @return array the request parameters (name-value pairs) to be used for action parameter binding - */ - public function getActionParams() - { - return $_GET; - } - - /** - * This method is invoked when the request parameters do not satisfy the requirement of the specified action. - * The default implementation will throw an exception. - * @param Action $action the action being executed - * @param Exception $exception the exception about the invalid parameters - * @throws HttpException $exception a 400 HTTP exception - */ - public function invalidActionParams($action, $exception) - { - throw new HttpException(400, \Yii::t('yii|Your request is invalid.')); - } - - /** - * @return string the page title. Defaults to the controller name and the action name. - */ - public function getPageTitle() - { - if($this->_pageTitle !== null) { - return $this->_pageTitle; - } - else { - $name = ucfirst(basename($this->id)); - if($this->action!==null && strcasecmp($this->action->id,$this->defaultAction)) - return $this->_pageTitle=\Yii::$app->name.' - '.ucfirst($this->action->id).' '.$name; - else - return $this->_pageTitle=\Yii::$app->name.' - '.$name; - } - } - - /** - * @param string $value the page title. - */ - public function setPageTitle($value) - { - $this->_pageTitle = $value; - } } \ No newline at end of file From e26a9ff4f52aeba008b298a657fd1146b88d4d74 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Wed, 6 Feb 2013 11:06:21 -0500 Subject: [PATCH 014/117] ... --- framework/base/Application.php | 3 - framework/base/UrlManager.php | 837 ----------------------------------------- framework/web/Application.php | 22 +- framework/web/UrlManager.php | 52 +++ 4 files changed, 73 insertions(+), 841 deletions(-) delete mode 100644 framework/base/UrlManager.php create mode 100644 framework/web/UrlManager.php diff --git a/framework/base/Application.php b/framework/base/Application.php index 9277e70..55dc0cc 100644 --- a/framework/base/Application.php +++ b/framework/base/Application.php @@ -363,9 +363,6 @@ class Application extends Module 'securityManager' => array( 'class' => 'yii\base\SecurityManager', ), - 'translator' => array( - 'class' => 'yii\i18n\Translator', - ), )); } diff --git a/framework/base/UrlManager.php b/framework/base/UrlManager.php deleted file mode 100644 index e17685d..0000000 --- a/framework/base/UrlManager.php +++ /dev/null @@ -1,837 +0,0 @@ - - *

  • 'path' format: /path/to/EntryScript.php/name1/value1/name2/value2...
  • - *
  • 'get' format: /path/to/EntryScript.php?name1=value1&name2=value2...
  • - * - * - * When using 'path' format, UrlManager uses a set of {@link setRules rules} to: - *
      - *
    • parse the requested URL into a route ('ControllerID/ActionID') and GET parameters;
    • - *
    • create URLs based on the given route and GET parameters.
    • - *
    - * - * A rule consists of a route and a pattern. The latter is used by UrlManager to determine - * which rule is used for parsing/creating URLs. A pattern is meant to match the path info - * part of a URL. It may contain named parameters using the syntax '<ParamName:RegExp>'. - * - * When parsing a URL, a matching rule will extract the named parameters from the path info - * and put them into the $_GET variable; when creating a URL, a matching rule will extract - * the named parameters from $_GET and put them into the path info part of the created URL. - * - * If a pattern ends with '/*', it means additional GET parameters may be appended to the path - * info part of the URL; otherwise, the GET parameters can only appear in the query string part. - * - * To specify URL rules, set the {@link setRules rules} property as an array of rules (pattern=>route). - * For example, - *
    - * array(
    - *     'articles'=>'article/list',
    - *     'article//*'=>'article/read',
    - * )
    - * 
    - * Two rules are specified in the above: - *
      - *
    • The first rule says that if the user requests the URL '/path/to/index.php/articles', - * it should be treated as '/path/to/index.php/article/list'; and vice versa applies - * when constructing such a URL.
    • - *
    • The second rule contains a named parameter 'id' which is specified using - * the <ParamName:RegExp> syntax. It says that if the user requests the URL - * '/path/to/index.php/article/13', it should be treated as '/path/to/index.php/article/read?id=13'; - * and vice versa applies when constructing such a URL.
    • - *
    - * - * The route part may contain references to named parameters defined in the pattern part. - * This allows a rule to be applied to different routes based on matching criteria. - * For example, - *
    - * array(
    - *      '<_c:(post|comment)>//<_a:(create|update|delete)>'=>'<_c>/<_a>',
    - *      '<_c:(post|comment)>/'=>'<_c>/view',
    - *      '<_c:(post|comment)>s/*'=>'<_c>/list',
    - * )
    - * 
    - * In the above, we use two named parameters '<_c>' and '<_a>' in the route part. The '<_c>' - * parameter matches either 'post' or 'comment', while the '<_a>' parameter matches an action ID. - * - * Like normal rules, these rules can be used for both parsing and creating URLs. - * For example, using the rules above, the URL '/index.php/post/123/create' - * would be parsed as the route 'post/create' with GET parameter 'id' being 123. - * And given the route 'post/list' and GET parameter 'page' being 2, we should get a URL - * '/index.php/posts/page/2'. - * - * It is also possible to include hostname into the rules for parsing and creating URLs. - * One may extract part of the hostname to be a GET parameter. - * For example, the URL http://admin.example.com/en/profile may be parsed into GET parameters - * user=admin and lang=en. On the other hand, rules with hostname may also be used to - * create URLs with parameterized hostnames. - * - * In order to use parameterized hostnames, simply declare URL rules with host info, e.g.: - *
    - * array(
    - *     'http://.example.com//profile' => 'user/profile',
    - * )
    - * 
    - * - * If you want to customize URL generation and parsing you can write custom - * URL rule classes and use them for one or several URL rules. For example, - *
    - * array(
    - *   // a standard rule
    - *   '' => 'site/',
    - *   // a custom rule using data in DB
    - *   array(
    - *     'class' => '\application\components\MyUrlRule',
    - *     'connectionID' => 'db',
    - *   ),
    - * )
    - * 
    - * Please note that the custom URL rule class should extend from {@link BaseUrlRule} and - * implement the following two methods, - *
      - *
    • {@link BaseUrlRule::createUrl()}
    • - *
    • {@link BaseUrlRule::parseUrl()}
    • - *
    - * - * UrlManager is a default application component that may be accessed via - * {@link \Yii::$app->urlManager}. - * - * @property string $baseUrl The base URL of the application (the part after host name and before query string). - * If {@link showScriptName} is true, it will include the script name part. - * Otherwise, it will not, and the ending slashes are stripped off. - * @property string $urlFormat The URL format. Defaults to 'path'. Valid values include 'path' and 'get'. - * Please refer to the guide for more details about the difference between these two formats. - * - * @author Qiang Xue - * @since 2.0 - */ -class UrlManager extends Component -{ - const CACHE_KEY='Yii.UrlManager.rules'; - const GET_FORMAT='get'; - const PATH_FORMAT='path'; - - /** - * @var array the URL rules (pattern=>route). - */ - public $rules=array(); - /** - * @var string the URL suffix used when in 'path' format. - * For example, ".html" can be used so that the URL looks like pointing to a static HTML page. Defaults to empty. - */ - public $urlSuffix=''; - /** - * @var boolean whether to show entry script name in the constructed URL. Defaults to true. - */ - public $showScriptName=true; - /** - * @var boolean whether to append GET parameters to the path info part. Defaults to true. - * This property is only effective when {@link urlFormat} is 'path' and is mainly used when - * creating URLs. When it is true, GET parameters will be appended to the path info and - * separate from each other using slashes. If this is false, GET parameters will be in query part. - */ - public $appendParams=true; - /** - * @var string the GET variable name for route. Defaults to 'r'. - */ - public $routeVar='r'; - /** - * @var boolean whether routes are case-sensitive. Defaults to true. By setting this to false, - * the route in the incoming request will be turned to lower case first before further processing. - * As a result, you should follow the convention that you use lower case when specifying - * controller mapping ({@link CWebApplication::controllerMap}) and action mapping - * ({@link CController::actions}). Also, the directory names for organizing controllers should - * be in lower case. - */ - public $caseSensitive=true; - /** - * @var boolean whether the GET parameter values should match the corresponding - * sub-patterns in a rule before using it to create a URL. Defaults to false, meaning - * a rule will be used for creating a URL only if its route and parameter names match the given ones. - * If this property is set true, then the given parameter values must also match the corresponding - * parameter sub-patterns. Note that setting this property to true will degrade performance. - * @since 1.1.0 - */ - public $matchValue=false; - /** - * @var string the ID of the cache application component that is used to cache the parsed URL rules. - * Defaults to 'cache' which refers to the primary cache application component. - * Set this property to false if you want to disable caching URL rules. - */ - public $cacheID='cache'; - /** - * @var boolean whether to enable strict URL parsing. - * This property is only effective when {@link urlFormat} is 'path'. - * If it is set true, then an incoming URL must match one of the {@link rules URL rules}. - * Otherwise, it will be treated as an invalid request and trigger a 404 HTTP exception. - * Defaults to false. - */ - public $useStrictParsing=false; - /** - * @var string the class name or path alias for the URL rule instances. Defaults to 'CUrlRule'. - * If you change this to something else, please make sure that the new class must extend from - * {@link CBaseUrlRule} and have the same constructor signature as {@link CUrlRule}. - * It must also be serializable and autoloadable. - */ - public $urlRuleClass='UrlRule'; - - private $_urlFormat=self::GET_FORMAT; - private $_rules=array(); - private $_baseUrl; - - - /** - * Initializes the application component. - */ - public function init() - { - parent::init(); - $this->processRules(); - } - - /** - * Processes the URL rules. - */ - protected function processRules() - { - if(empty($this->rules) || $this->getUrlFormat()===self::GET_FORMAT) - return; - if($this->cacheID!==false && ($cache=\Yii::$app->getComponent($this->cacheID))!==null) - { - $hash=md5(serialize($this->rules)); - if(($data=$cache->get(self::CACHE_KEY))!==false && isset($data[1]) && $data[1]===$hash) - { - $this->_rules=$data[0]; - return; - } - } - foreach($this->rules as $pattern=>$route) - $this->_rules[]=$this->createUrlRule($route,$pattern); - if(isset($cache)) - $cache->set(self::CACHE_KEY,array($this->_rules,$hash)); - } - - /** - * Adds new URL rules. - * In order to make the new rules effective, this method must be called BEFORE - * {@link CWebApplication::processRequest}. - * @param array $rules new URL rules (pattern=>route). - * @param boolean $append whether the new URL rules should be appended to the existing ones. If false, - * they will be inserted at the beginning. - */ - public function addRules($rules, $append=true) - { - if ($append) - { - foreach($rules as $pattern=>$route) - $this->_rules[]=$this->createUrlRule($route,$pattern); - } - else - { - foreach($rules as $pattern=>$route) - array_unshift($this->_rules, $this->createUrlRule($route,$pattern)); - } - } - - /** - * Creates a URL rule instance. - * The default implementation returns a CUrlRule object. - * @param mixed $route the route part of the rule. This could be a string or an array - * @param string $pattern the pattern part of the rule - * @return CUrlRule the URL rule instance - */ - protected function createUrlRule($route,$pattern) - { - if(is_array($route) && isset($route['class'])) - return $route; - else - return new $this->urlRuleClass($route,$pattern); - } - - /** - * Constructs a URL. - * @param string $route the controller and the action (e.g. article/read) - * @param array $params list of GET parameters (name=>value). Both the name and value will be URL-encoded. - * If the name is '#', the corresponding value will be treated as an anchor - * and will be appended at the end of the URL. - * @param string $ampersand the token separating name-value pairs in the URL. Defaults to '&'. - * @return string the constructed URL - */ - public function createUrl($route,$params=array(),$ampersand='&') - { - unset($params[$this->routeVar]); - foreach($params as $i=>$param) - if($param===null) - $params[$i]=''; - - if(isset($params['#'])) - { - $anchor='#'.$params['#']; - unset($params['#']); - } - else - $anchor=''; - $route=trim($route,'/'); - foreach($this->_rules as $i=>$rule) - { - if(is_array($rule)) - $this->_rules[$i]=$rule=Yii::createComponent($rule); - if(($url=$rule->createUrl($this,$route,$params,$ampersand))!==false) - { - if($rule->hasHostInfo) - return $url==='' ? '/'.$anchor : $url.$anchor; - else - return $this->getBaseUrl().'/'.$url.$anchor; - } - } - return $this->createUrlDefault($route,$params,$ampersand).$anchor; - } - - /** - * Creates a URL based on default settings. - * @param string $route the controller and the action (e.g. article/read) - * @param array $params list of GET parameters - * @param string $ampersand the token separating name-value pairs in the URL. - * @return string the constructed URL - */ - protected function createUrlDefault($route,$params,$ampersand) - { - if($this->getUrlFormat()===self::PATH_FORMAT) - { - $url=rtrim($this->getBaseUrl().'/'.$route,'/'); - if($this->appendParams) - { - $url=rtrim($url.'/'.$this->createPathInfo($params,'/','/'),'/'); - return $route==='' ? $url : $url.$this->urlSuffix; - } - else - { - if($route!=='') - $url.=$this->urlSuffix; - $query=$this->createPathInfo($params,'=',$ampersand); - return $query==='' ? $url : $url.'?'.$query; - } - } - else - { - $url=$this->getBaseUrl(); - if(!$this->showScriptName) - $url.='/'; - if($route!=='') - { - $url.='?'.$this->routeVar.'='.$route; - if(($query=$this->createPathInfo($params,'=',$ampersand))!=='') - $url.=$ampersand.$query; - } - else if(($query=$this->createPathInfo($params,'=',$ampersand))!=='') - $url.='?'.$query; - return $url; - } - } - - /** - * Parses the user request. - * @param HttpRequest $request the request application component - * @return string the route (controllerID/actionID) and perhaps GET parameters in path format. - */ - public function parseUrl($request) - { - if($this->getUrlFormat()===self::PATH_FORMAT) - { - $rawPathInfo=$request->getPathInfo(); - $pathInfo=$this->removeUrlSuffix($rawPathInfo,$this->urlSuffix); - foreach($this->_rules as $i=>$rule) - { - if(is_array($rule)) - $this->_rules[$i]=$rule=Yii::createComponent($rule); - if(($r=$rule->parseUrl($this,$request,$pathInfo,$rawPathInfo))!==false) - return isset($_GET[$this->routeVar]) ? $_GET[$this->routeVar] : $r; - } - if($this->useStrictParsing) - throw new HttpException(404,Yii::t('yii|Unable to resolve the request "{route}".', - array('{route}'=>$pathInfo))); - else - return $pathInfo; - } - else if(isset($_GET[$this->routeVar])) - return $_GET[$this->routeVar]; - else if(isset($_POST[$this->routeVar])) - return $_POST[$this->routeVar]; - else - return ''; - } - - /** - * Parses a path info into URL segments and saves them to $_GET and $_REQUEST. - * @param string $pathInfo path info - */ - public function parsePathInfo($pathInfo) - { - if($pathInfo==='') - return; - $segs=explode('/',$pathInfo.'/'); - $n=count($segs); - for($i=0;$i<$n-1;$i+=2) - { - $key=$segs[$i]; - if($key==='') continue; - $value=$segs[$i+1]; - if(($pos=strpos($key,'['))!==false && ($m=preg_match_all('/\[(.*?)\]/',$key,$matches))>0) - { - $name=substr($key,0,$pos); - for($j=$m-1;$j>=0;--$j) - { - if($matches[1][$j]==='') - $value=array($value); - else - $value=array($matches[1][$j]=>$value); - } - if(isset($_GET[$name]) && is_array($_GET[$name])) - $value=CMap::mergeArray($_GET[$name],$value); - $_REQUEST[$name]=$_GET[$name]=$value; - } - else - $_REQUEST[$key]=$_GET[$key]=$value; - } - } - - /** - * Creates a path info based on the given parameters. - * @param array $params list of GET parameters - * @param string $equal the separator between name and value - * @param string $ampersand the separator between name-value pairs - * @param string $key this is used internally. - * @return string the created path info - */ - public function createPathInfo($params,$equal,$ampersand, $key=null) - { - $pairs = array(); - foreach($params as $k => $v) - { - if ($key!==null) - $k = $key.'['.$k.']'; - - if (is_array($v)) - $pairs[]=$this->createPathInfo($v,$equal,$ampersand, $k); - else - $pairs[]=urlencode($k).$equal.urlencode($v); - } - return implode($ampersand,$pairs); - } - - /** - * Removes the URL suffix from path info. - * @param string $pathInfo path info part in the URL - * @param string $urlSuffix the URL suffix to be removed - * @return string path info with URL suffix removed. - */ - public function removeUrlSuffix($pathInfo,$urlSuffix) - { - if($urlSuffix!=='' && substr($pathInfo,-strlen($urlSuffix))===$urlSuffix) - return substr($pathInfo,0,-strlen($urlSuffix)); - else - return $pathInfo; - } - - /** - * Returns the base URL of the application. - * @return string the base URL of the application (the part after host name and before query string). - * If {@link showScriptName} is true, it will include the script name part. - * Otherwise, it will not, and the ending slashes are stripped off. - */ - public function getBaseUrl() - { - if($this->_baseUrl!==null) - return $this->_baseUrl; - else - { - if($this->showScriptName) - $this->_baseUrl=\Yii::$app->getRequest()->getScriptUrl(); - else - $this->_baseUrl=\Yii::$app->getRequest()->getBaseUrl(); - return $this->_baseUrl; - } - } - - /** - * Sets the base URL of the application (the part after host name and before query string). - * This method is provided in case the {@link baseUrl} cannot be determined automatically. - * The ending slashes should be stripped off. And you are also responsible to remove the script name - * if you set {@link showScriptName} to be false. - * @param string $value the base URL of the application - */ - public function setBaseUrl($value) - { - $this->_baseUrl=$value; - } - - /** - * Returns the URL format. - * @return string the URL format. Defaults to 'path'. Valid values include 'path' and 'get'. - * Please refer to the guide for more details about the difference between these two formats. - */ - public function getUrlFormat() - { - return $this->_urlFormat; - } - - /** - * Sets the URL format. - * @param string $value the URL format. It must be either 'path' or 'get'. - */ - public function setUrlFormat($value) - { - if($value===self::PATH_FORMAT || $value===self::GET_FORMAT) - $this->_urlFormat=$value; - else - throw new CException(Yii::t('yii|CUrlManager.UrlFormat must be either "path" or "get".')); - } -} - - -/** - * CBaseUrlRule is the base class for a URL rule class. - * - * Custom URL rule classes should extend from this class and implement two methods: - * {@link createUrl} and {@link parseUrl}. - * - * @author Qiang Xue - */ -abstract class CBaseUrlRule extends CComponent -{ - /** - * @var boolean whether this rule will also parse the host info part. Defaults to false. - */ - public $hasHostInfo=false; - /** - * Creates a URL based on this rule. - * @param CUrlManager $manager the manager - * @param string $route the route - * @param array $params list of parameters (name=>value) associated with the route - * @param string $ampersand the token separating name-value pairs in the URL. - * @return mixed the constructed URL. False if this rule does not apply. - */ - abstract public function createUrl($manager,$route,$params,$ampersand); - /** - * Parses a URL based on this rule. - * @param UrlManager $manager the URL manager - * @param HttpRequest $request the request object - * @param string $pathInfo path info part of the URL (URL suffix is already removed based on {@link CUrlManager::urlSuffix}) - * @param string $rawPathInfo path info that contains the potential URL suffix - * @return mixed the route that consists of the controller ID and action ID. False if this rule does not apply. - */ - abstract public function parseUrl($manager,$request,$pathInfo,$rawPathInfo); -} - -/** - * CUrlRule represents a URL formatting/parsing rule. - * - * It mainly consists of two parts: route and pattern. The former classifies - * the rule so that it only applies to specific controller-action route. - * The latter performs the actual formatting and parsing role. The pattern - * may have a set of named parameters. - * - * @author Qiang Xue - */ -class CUrlRule extends CBaseUrlRule -{ - /** - * @var string the URL suffix used for this rule. - * For example, ".html" can be used so that the URL looks like pointing to a static HTML page. - * Defaults to null, meaning using the value of {@link CUrlManager::urlSuffix}. - */ - public $urlSuffix; - /** - * @var boolean whether the rule is case sensitive. Defaults to null, meaning - * using the value of {@link CUrlManager::caseSensitive}. - */ - public $caseSensitive; - /** - * @var array the default GET parameters (name=>value) that this rule provides. - * When this rule is used to parse the incoming request, the values declared in this property - * will be injected into $_GET. - */ - public $defaultParams=array(); - /** - * @var boolean whether the GET parameter values should match the corresponding - * sub-patterns in the rule when creating a URL. Defaults to null, meaning using the value - * of {@link CUrlManager::matchValue}. When this property is false, it means - * a rule will be used for creating a URL if its route and parameter names match the given ones. - * If this property is set true, then the given parameter values must also match the corresponding - * parameter sub-patterns. Note that setting this property to true will degrade performance. - */ - public $matchValue; - /** - * @var string the HTTP verb (e.g. GET, POST, DELETE) that this rule should match. - * If this rule can match multiple verbs, please separate them with commas. - * If this property is not set, the rule can match any verb. - * Note that this property is only used when parsing a request. It is ignored for URL creation. - */ - public $verb; - /** - * @var boolean whether this rule is only used for request parsing. - * Defaults to false, meaning the rule is used for both URL parsing and creation. - */ - public $parsingOnly=false; - /** - * @var string the controller/action pair - */ - public $route; - /** - * @var array the mapping from route param name to token name (e.g. _r1=><1>) - */ - public $references=array(); - /** - * @var string the pattern used to match route - */ - public $routePattern; - /** - * @var string regular expression used to parse a URL - */ - public $pattern; - /** - * @var string template used to construct a URL - */ - public $template; - /** - * @var array list of parameters (name=>regular expression) - */ - public $params=array(); - /** - * @var boolean whether the URL allows additional parameters at the end of the path info. - */ - public $append; - /** - * @var boolean whether host info should be considered for this rule - */ - public $hasHostInfo; - - /** - * Constructor. - * @param string $route the route of the URL (controller/action) - * @param string $pattern the pattern for matching the URL - */ - public function __construct($route,$pattern) - { - if(is_array($route)) - { - foreach(array('urlSuffix', 'caseSensitive', 'defaultParams', 'matchValue', 'verb', 'parsingOnly') as $name) - { - if(isset($route[$name])) - $this->$name=$route[$name]; - } - if(isset($route['pattern'])) - $pattern=$route['pattern']; - $route=$route[0]; - } - $this->route=trim($route,'/'); - - $tr2['/']=$tr['/']='\\/'; - - if(strpos($route,'<')!==false && preg_match_all('/<(\w+)>/',$route,$matches2)) - { - foreach($matches2[1] as $name) - $this->references[$name]="<$name>"; - } - - $this->hasHostInfo=!strncasecmp($pattern,'http://',7) || !strncasecmp($pattern,'https://',8); - - if($this->verb!==null) - $this->verb=preg_split('/[\s,]+/',strtoupper($this->verb),-1,PREG_SPLIT_NO_EMPTY); - - if(preg_match_all('/<(\w+):?(.*?)?>/',$pattern,$matches)) - { - $tokens=array_combine($matches[1],$matches[2]); - foreach($tokens as $name=>$value) - { - if($value==='') - $value='[^\/]+'; - $tr["<$name>"]="(?P<$name>$value)"; - if(isset($this->references[$name])) - $tr2["<$name>"]=$tr["<$name>"]; - else - $this->params[$name]=$value; - } - } - $p=rtrim($pattern,'*'); - $this->append=$p!==$pattern; - $p=trim($p,'/'); - $this->template=preg_replace('/<(\w+):?.*?>/','<$1>',$p); - $this->pattern='/^'.strtr($this->template,$tr).'\/'; - if($this->append) - $this->pattern.='/u'; - else - $this->pattern.='$/u'; - - if($this->references!==array()) - $this->routePattern='/^'.strtr($this->route,$tr2).'$/u'; - - if(YII_DEBUG && @preg_match($this->pattern,'test')===false) - throw new CException(Yii::t('yii|The URL pattern "{pattern}" for route "{route}" is not a valid regular expression.', - array('{route}'=>$route,'{pattern}'=>$pattern))); - } - - /** - * Creates a URL based on this rule. - * @param CUrlManager $manager the manager - * @param string $route the route - * @param array $params list of parameters - * @param string $ampersand the token separating name-value pairs in the URL. - * @return mixed the constructed URL or false on error - */ - public function createUrl($manager,$route,$params,$ampersand) - { - if($this->parsingOnly) - return false; - - if($manager->caseSensitive && $this->caseSensitive===null || $this->caseSensitive) - $case=''; - else - $case='i'; - - $tr=array(); - if($route!==$this->route) - { - if($this->routePattern!==null && preg_match($this->routePattern.$case,$route,$matches)) - { - foreach($this->references as $key=>$name) - $tr[$name]=$matches[$key]; - } - else - return false; - } - - foreach($this->defaultParams as $key=>$value) - { - if(isset($params[$key])) - { - if($params[$key]==$value) - unset($params[$key]); - else - return false; - } - } - - foreach($this->params as $key=>$value) - if(!isset($params[$key])) - return false; - - if($manager->matchValue && $this->matchValue===null || $this->matchValue) - { - foreach($this->params as $key=>$value) - { - if(!preg_match('/\A'.$value.'\z/u'.$case,$params[$key])) - return false; - } - } - - foreach($this->params as $key=>$value) - { - $tr["<$key>"]=urlencode($params[$key]); - unset($params[$key]); - } - - $suffix=$this->urlSuffix===null ? $manager->urlSuffix : $this->urlSuffix; - - $url=strtr($this->template,$tr); - - if($this->hasHostInfo) - { - $hostInfo=\Yii::$app->getRequest()->getHostInfo(); - if(stripos($url,$hostInfo)===0) - $url=substr($url,strlen($hostInfo)); - } - - if(empty($params)) - return $url!=='' ? $url.$suffix : $url; - - if($this->append) - $url.='/'.$manager->createPathInfo($params,'/','/').$suffix; - else - { - if($url!=='') - $url.=$suffix; - $url.='?'.$manager->createPathInfo($params,'=',$ampersand); - } - - return $url; - } - - /** - * Parses a URL based on this rule. - * @param UrlManager $manager the URL manager - * @param HttpRequest $request the request object - * @param string $pathInfo path info part of the URL - * @param string $rawPathInfo path info that contains the potential URL suffix - * @return mixed the route that consists of the controller ID and action ID or false on error - */ - public function parseUrl($manager,$request,$pathInfo,$rawPathInfo) - { - if($this->verb!==null && !in_array($request->getRequestType(), $this->verb, true)) - return false; - - if($manager->caseSensitive && $this->caseSensitive===null || $this->caseSensitive) - $case=''; - else - $case='i'; - - if($this->urlSuffix!==null) - $pathInfo=$manager->removeUrlSuffix($rawPathInfo,$this->urlSuffix); - - // URL suffix required, but not found in the requested URL - if($manager->useStrictParsing && $pathInfo===$rawPathInfo) - { - $urlSuffix=$this->urlSuffix===null ? $manager->urlSuffix : $this->urlSuffix; - if($urlSuffix!='' && $urlSuffix!=='/') - return false; - } - - if($this->hasHostInfo) - $pathInfo=strtolower($request->getHostInfo()).rtrim('/'.$pathInfo,'/'); - - $pathInfo.='/'; - - if(preg_match($this->pattern.$case,$pathInfo,$matches)) - { - foreach($this->defaultParams as $name=>$value) - { - if(!isset($_GET[$name])) - $_REQUEST[$name]=$_GET[$name]=$value; - } - $tr=array(); - foreach($matches as $key=>$value) - { - if(isset($this->references[$key])) - $tr[$this->references[$key]]=$value; - else if(isset($this->params[$key])) - $_REQUEST[$key]=$_GET[$key]=$value; - } - if($pathInfo!==$matches[0]) // there're additional GET params - $manager->parsePathInfo(ltrim(substr($pathInfo,strlen($matches[0])),'/')); - if($this->routePattern!==null) - return strtr($this->route,$tr); - else - return $this->route; - } - else - return false; - } -} \ No newline at end of file diff --git a/framework/web/Application.php b/framework/web/Application.php index fcc8910..2159878 100644 --- a/framework/web/Application.php +++ b/framework/web/Application.php @@ -32,11 +32,28 @@ class Application extends \yii\base\Application */ public function processRequest() { - $route = isset($_GET['r']) ? $_GET['r'] : ''; + $route = $this->getUrlManager()->parseUrl($this->getRequest()); return $this->runAction($route, $_GET); } /** + * Returns the request component. + * @return Request the request component + */ + public function getRequest() + { + return $this->getComponent('request'); + } + + /** + * @return UrlManager + */ + public function getUrlManager() + { + return $this->getComponent('urlManager'); + } + + /** * Registers the core application components. * @see setComponents */ @@ -50,6 +67,9 @@ class Application extends \yii\base\Application 'response' => array( 'class' => 'yii\web\Response', ), + 'urlManager' => array( + 'class' => 'yii\web\UrlManager', + ), )); } } diff --git a/framework/web/UrlManager.php b/framework/web/UrlManager.php new file mode 100644 index 0000000..e890740 --- /dev/null +++ b/framework/web/UrlManager.php @@ -0,0 +1,52 @@ + + * @since 2.0 + */ +class UrlManager extends Component +{ + public $routeVar = 'r'; + + /** + * Initializes the application component. + */ + public function init() + { + parent::init(); + $this->processRules(); + } + + /** + * Processes the URL rules. + */ + protected function processRules() + { + } + + /** + * Parses the user request. + * @param HttpRequest $request the request application component + * @return string the route (controllerID/actionID) and perhaps GET parameters in path format. + */ + public function parseUrl($request) + { + if(isset($_GET[$this->routeVar])) + return $_GET[$this->routeVar]; + else + return ''; + } +} From 811880031c0e985613db1a31d4b99b777b643ffc Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Thu, 7 Feb 2013 16:13:57 +0400 Subject: [PATCH 015/117] fixed YII_DEBUG exception view --- framework/views/exception.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/framework/views/exception.php b/framework/views/exception.php index 8e8e905..356fd3b 100644 --- a/framework/views/exception.php +++ b/framework/views/exception.php @@ -1,9 +1,9 @@ context; +$owner = $this->owner; ?> @@ -163,26 +163,26 @@ $context = $this->context;

    - htmlEncode($exception->getMessage()))?> + htmlEncode($exception->getMessage()))?>

    - htmlEncode($exception->getFile()) . '(' . $exception->getLine() . ')'?> + htmlEncode($exception->getFile()) . '(' . $exception->getLine() . ')'?>

    - renderSourceCode($exception->getFile(), $exception->getLine(), $context->maxSourceLines)?> + renderSourceCode($exception->getFile(), $exception->getLine(), $owner->maxSourceLines)?>

    Stack Trace

    - renderTrace($exception->getTrace())?> + renderTrace($exception->getTrace())?>
    - versionInfo : ''?> + versionInfo : ''?>
    From 64fb2a815c440a32e9f024652f133c525ae0aada Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Thu, 7 Feb 2013 23:48:55 +0400 Subject: [PATCH 016/117] added tests for cache --- framework/caching/DbCache.php | 4 +- tests/unit/.gitignore | 1 + tests/unit/framework/caching/ApcCacheTest.php | 27 +++++++ tests/unit/framework/caching/CacheTest.php | 82 ++++++++++++++++++++++ tests/unit/framework/caching/DbCacheTest.php | 70 ++++++++++++++++++ tests/unit/framework/caching/FileCacheTest.php | 25 +++++++ tests/unit/framework/caching/MemCacheTest.php | 27 +++++++ tests/unit/framework/caching/MemCachedTest.php | 29 ++++++++ tests/unit/framework/caching/WinCacheTest.php | 31 ++++++++ tests/unit/framework/caching/XCacheTest.php | 27 +++++++ tests/unit/framework/caching/ZendDataCacheTest.php | 27 +++++++ 11 files changed, 348 insertions(+), 2 deletions(-) create mode 100644 tests/unit/.gitignore create mode 100644 tests/unit/framework/caching/ApcCacheTest.php create mode 100644 tests/unit/framework/caching/CacheTest.php create mode 100644 tests/unit/framework/caching/DbCacheTest.php create mode 100644 tests/unit/framework/caching/FileCacheTest.php create mode 100644 tests/unit/framework/caching/MemCacheTest.php create mode 100644 tests/unit/framework/caching/MemCachedTest.php create mode 100644 tests/unit/framework/caching/WinCacheTest.php create mode 100644 tests/unit/framework/caching/XCacheTest.php create mode 100644 tests/unit/framework/caching/ZendDataCacheTest.php diff --git a/framework/caching/DbCache.php b/framework/caching/DbCache.php index 75f13b0..b74c5e0 100644 --- a/framework/caching/DbCache.php +++ b/framework/caching/DbCache.php @@ -104,7 +104,7 @@ class DbCache extends Cache $query = new Query; $query->select(array('data')) ->from($this->cacheTableName) - ->where('id = :id AND (expire = 0 OR expire > :time)', array(':id' => $key, ':time' => time())); + ->where('id = :id AND (expire = 0 OR expire >' . time() . ')', array(':id' => $key)); $db = $this->getDb(); if ($db->enableQueryCache) { // temporarily disable and re-enable query caching @@ -131,7 +131,7 @@ class DbCache extends Cache $query->select(array('id', 'data')) ->from($this->cacheTableName) ->where(array('id' => $keys)) - ->andWhere("expire = 0 OR expire > " . time() . ")"); + ->andWhere('(expire = 0 OR expire > ' . time() . ')'); $db = $this->getDb(); if ($db->enableQueryCache) { diff --git a/tests/unit/.gitignore b/tests/unit/.gitignore new file mode 100644 index 0000000..34651d7 --- /dev/null +++ b/tests/unit/.gitignore @@ -0,0 +1 @@ +runtime/cache/* \ No newline at end of file diff --git a/tests/unit/framework/caching/ApcCacheTest.php b/tests/unit/framework/caching/ApcCacheTest.php new file mode 100644 index 0000000..74ede2a --- /dev/null +++ b/tests/unit/framework/caching/ApcCacheTest.php @@ -0,0 +1,27 @@ +markTestSkipped("APC not installed. Skipping."); + } + + if($this->_cacheInstance === null) { + $this->_cacheInstance = new ApcCache(); + } + return $this->_cacheInstance; + } +} \ No newline at end of file diff --git a/tests/unit/framework/caching/CacheTest.php b/tests/unit/framework/caching/CacheTest.php new file mode 100644 index 0000000..ad2fcf5 --- /dev/null +++ b/tests/unit/framework/caching/CacheTest.php @@ -0,0 +1,82 @@ +getCacheInstance(); + $cache->set('string_test', 'string_test'); + $cache->set('number_test', 42); + $cache->set('array_test', array('array_test' => 'array_test')); + $cache['arrayaccess_test'] = new \stdClass(); + } + + public function testGet() + { + $cache = $this->getCacheInstance(); + $this->assertEquals('string_test', $cache->get('string_test')); + + $this->assertEquals(42, $cache->get('number_test')); + + $array = $cache->get('array_test'); + $this->assertArrayHasKey('array_test', $array); + $this->assertEquals('array_test', $array['array_test']); + + $this->assertInstanceOf('stdClass', $cache['arrayaccess_test']); + } + + public function testMget() + { + $cache = $this->getCacheInstance(); + $this->assertEquals(array('string_test' => 'string_test', 'number_test' => 42), $cache->mget(array('string_test', 'number_test'))); + } + + public function testExpire() + { + $cache = $this->getCacheInstance(); + $cache->set('expire_test', 'expire_test', 2); + sleep(1); + $this->assertEquals('expire_test', $cache->get('expire_test')); + sleep(2); + $this->assertEquals(false, $cache->get('expire_test')); + } + + public function testAdd() + { + $cache = $this->getCacheInstance(); + + // should not change existing keys + $cache->add('number_test', 13); + $this->assertEquals(42, $cache->get('number_test')); + + // should store data is it's not there yet + $cache->add('add_test', 13); + $this->assertEquals(13, $cache->get('add_test')); + } + + public function testDelete() + { + $cache = $this->getCacheInstance(); + + $cache->delete('number_test'); + $this->assertEquals(null, $cache->get('number_test')); + } + + public function testFlush() + { + $cache = $this->getCacheInstance(); + $cache->flush(); + $this->assertEquals(null, $cache->get('add_test')); + } +} diff --git a/tests/unit/framework/caching/DbCacheTest.php b/tests/unit/framework/caching/DbCacheTest.php new file mode 100644 index 0000000..3977ee8 --- /dev/null +++ b/tests/unit/framework/caching/DbCacheTest.php @@ -0,0 +1,70 @@ +markTestSkipped('pdo and pdo_mysql extensions are required.'); + } + + $this->getConnection()->createCommand(" + CREATE TABLE IF NOT EXISTS tbl_cache ( + id char(128) NOT NULL, + expire int(11) DEFAULT NULL, + data LONGBLOB, + PRIMARY KEY (id), + KEY expire (expire) + ); + ")->execute(); + } + + /** + * @param bool $reset whether to clean up the test database + * @return \yii\db\Connection + */ + function getConnection($reset = true) + { + if($this->_connection === null) { + $params = $this->getParam('mysql'); + $db = new \yii\db\Connection; + $db->dsn = $params['dsn']; + $db->username = $params['username']; + $db->password = $params['password']; + if ($reset) { + $db->open(); + $lines = explode(';', file_get_contents($params['fixture'])); + foreach ($lines as $line) { + if (trim($line) !== '') { + $db->pdo->exec($line); + } + } + } + $this->_connection = $db; + } + return $this->_connection; + } + + + /** + * @return DbCache + */ + protected function getCacheInstance() + { + if($this->_cacheInstance === null) { + $this->_cacheInstance = new DbCache(array( + 'db' => $this->getConnection(), + )); + } + return $this->_cacheInstance; + } +} \ No newline at end of file diff --git a/tests/unit/framework/caching/FileCacheTest.php b/tests/unit/framework/caching/FileCacheTest.php new file mode 100644 index 0000000..1f6debd --- /dev/null +++ b/tests/unit/framework/caching/FileCacheTest.php @@ -0,0 +1,25 @@ +_cacheInstance === null) { + $this->_cacheInstance = new FileCache(array( + 'cachePath' => '@yiiunit/runtime/cache', + )); + } + return $this->_cacheInstance; + } +} \ No newline at end of file diff --git a/tests/unit/framework/caching/MemCacheTest.php b/tests/unit/framework/caching/MemCacheTest.php new file mode 100644 index 0000000..e4804d9 --- /dev/null +++ b/tests/unit/framework/caching/MemCacheTest.php @@ -0,0 +1,27 @@ +markTestSkipped("memcache not installed. Skipping."); + } + + if($this->_cacheInstance === null) { + $this->_cacheInstance = new MemCache(); + } + return $this->_cacheInstance; + } +} \ No newline at end of file diff --git a/tests/unit/framework/caching/MemCachedTest.php b/tests/unit/framework/caching/MemCachedTest.php new file mode 100644 index 0000000..59396df --- /dev/null +++ b/tests/unit/framework/caching/MemCachedTest.php @@ -0,0 +1,29 @@ +markTestSkipped("memcached not installed. Skipping."); + } + + if($this->_cacheInstance === null) { + $this->_cacheInstance = new MemCache(array( + 'useMemcached' => true, + )); + } + return $this->_cacheInstance; + } +} \ No newline at end of file diff --git a/tests/unit/framework/caching/WinCacheTest.php b/tests/unit/framework/caching/WinCacheTest.php new file mode 100644 index 0000000..b78d57b --- /dev/null +++ b/tests/unit/framework/caching/WinCacheTest.php @@ -0,0 +1,31 @@ +markTestSkipped("Wincache not installed. Skipping."); + } + + if(!ini_get('wincache.ucenabled')) { + $this->markTestSkipped("Wincache user cache disabled. Skipping."); + } + + if($this->_cacheInstance === null) { + $this->_cacheInstance = new WinCache(); + } + return $this->_cacheInstance; + } +} \ No newline at end of file diff --git a/tests/unit/framework/caching/XCacheTest.php b/tests/unit/framework/caching/XCacheTest.php new file mode 100644 index 0000000..e1ed844 --- /dev/null +++ b/tests/unit/framework/caching/XCacheTest.php @@ -0,0 +1,27 @@ +markTestSkipped("XCache not installed. Skipping."); + } + + if($this->_cacheInstance === null) { + $this->_cacheInstance = new XCache(); + } + return $this->_cacheInstance; + } +} \ No newline at end of file diff --git a/tests/unit/framework/caching/ZendDataCacheTest.php b/tests/unit/framework/caching/ZendDataCacheTest.php new file mode 100644 index 0000000..91dfbb5 --- /dev/null +++ b/tests/unit/framework/caching/ZendDataCacheTest.php @@ -0,0 +1,27 @@ +markTestSkipped("Zend Data cache not installed. Skipping."); + } + + if($this->_cacheInstance === null) { + $this->_cacheInstance = new ZendDataCache(); + } + return $this->_cacheInstance; + } +} \ No newline at end of file From 137d472838bd299a6a4cd898565ed06d7e67f4c7 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 8 Feb 2013 00:29:03 +0400 Subject: [PATCH 017/117] console command to flush cache --- framework/console/Application.php | 1 + framework/console/controllers/CacheController.php | 49 +++++++++++++++++++++++ todo.md | 3 +- 3 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 framework/console/controllers/CacheController.php diff --git a/framework/console/Application.php b/framework/console/Application.php index 5b7d190..fb579e2 100644 --- a/framework/console/Application.php +++ b/framework/console/Application.php @@ -127,6 +127,7 @@ class Application extends \yii\base\Application 'help' => 'yii\console\controllers\HelpController', 'migrate' => 'yii\console\controllers\MigrateController', 'app' => 'yii\console\controllers\CreateController', + 'cache' => 'yii\console\controllers\CacheController', ); } diff --git a/framework/console/controllers/CacheController.php b/framework/console/controllers/CacheController.php new file mode 100644 index 0000000..866db12 --- /dev/null +++ b/framework/console/controllers/CacheController.php @@ -0,0 +1,49 @@ + + * @since 2.0 + */ +class CacheController extends Controller +{ + public function actionIndex() + { + $this->forward('help/index', array('-args' => array('cache/flush'))); + } + + /** + * Flushes cache. + * @param string $component Name of the cache application component to use. + * + * @throws \yii\console\Exception + */ + public function actionFlush($component = 'cache') + { + /** @var $cache Cache */ + $cache = \Yii::$app->getComponent($component); + if(!$cache || !$cache instanceof Cache) { + throw new Exception('Application component "'.$component.'" is not defined or not a cache.'); + } + + if(!$cache->flush()) { + throw new Exception('Unable to flush cache.'); + } + + echo "\nDone.\n"; + } +} diff --git a/todo.md b/todo.md index 60e37c5..7249235 100644 --- a/todo.md +++ b/todo.md @@ -13,8 +13,7 @@ * ProfileTarget (TBD after web is in place): should consider using javascript and make it into a toolbar * unit tests - caching - * a console command to clear cached data - * unit tests + * backend-specific unit tests - validators * FileValidator: depends on CUploadedFile * CaptchaValidator: depends on CaptchaAction From 54c539dd5183aac90910973413f0d8f753d49fda Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Thu, 7 Feb 2013 15:46:15 -0500 Subject: [PATCH 018/117] Fixed buildInCondition bug. --- framework/db/QueryBuilder.php | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/framework/db/QueryBuilder.php b/framework/db/QueryBuilder.php index ebca888..67dcbb0 100644 --- a/framework/db/QueryBuilder.php +++ b/framework/db/QueryBuilder.php @@ -592,21 +592,19 @@ class QueryBuilder extends \yii\base\Object return $operator === 'IN' ? '0=1' : ''; } - if (is_array($column)) { - if (count($column) > 1) { - return $this->buildCompositeInCondition($operator, $column, $values); + 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 { - $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; - } - } + $values[$i] = is_string($value) ? $this->db->quoteValue($value) : (string)$value; } } if (strpos($column, '(') === false) { From 80e29130dc71665768fd67bf5cec148a8dc3482c Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 8 Feb 2013 01:37:07 +0400 Subject: [PATCH 019/117] more todos --- todo.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/todo.md b/todo.md index 7249235..6282ec5 100644 --- a/todo.md +++ b/todo.md @@ -6,14 +6,15 @@ * key-value-based (should allow storage-specific methods additionally to generic ones) * redis (put it under framework/db/redis or perhaps framework/caching?) - base - * TwigViewRenderer - * SmartyViewRenderer + * TwigViewRenderer (Alex) + * SmartyViewRenderer (Alex) - logging * WebTarget (TBD after web is in place): should consider using javascript and make it into a toolbar * ProfileTarget (TBD after web is in place): should consider using javascript and make it into a toolbar * unit tests - caching * backend-specific unit tests + * dependency unit tests - validators * FileValidator: depends on CUploadedFile * CaptchaValidator: depends on CaptchaAction @@ -27,6 +28,7 @@ - Module should be able to define its own configuration including routes. Application should be able to overwrite it. * application * security + - backport 1.1 changes - built-in console commands + api doc builder * support for markdown syntax From 45020ebc72370b5611f35b5800578ccd1f6e8825 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 8 Feb 2013 03:23:48 +0400 Subject: [PATCH 020/117] autoloader draft docs --- docs/autoloader.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 docs/autoloader.md diff --git a/docs/autoloader.md b/docs/autoloader.md new file mode 100644 index 0000000..b7696d7 --- /dev/null +++ b/docs/autoloader.md @@ -0,0 +1,19 @@ +Yii2 class loader +================= + +Yii 2 class loader is PSR-0 compliant. That means it can handle most of the PHP +libraries and frameworks out there. + +In order to autoload a library you need to set a root alias for it. + +PEAR-style libraries +-------------------- + +```php +\Yii::setAlias('@Twig', '@app/vendors/Twig'); +``` + +References +---------- + +- YiiBase::autoload \ No newline at end of file From 379fddfec35f3045a55211264d0b73afc6b9a675 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Fri, 8 Feb 2013 08:41:22 -0500 Subject: [PATCH 021/117] URL manager WIP --- framework/web/UrlManager.php | 5 +++ framework/web/UrlRule.php | 95 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 framework/web/UrlRule.php diff --git a/framework/web/UrlManager.php b/framework/web/UrlManager.php index e890740..1c80938 100644 --- a/framework/web/UrlManager.php +++ b/framework/web/UrlManager.php @@ -49,4 +49,9 @@ class UrlManager extends Component else return ''; } + + public function createUrl($route, $params = array(), $ampersand = '&') + { + + } } diff --git a/framework/web/UrlRule.php b/framework/web/UrlRule.php new file mode 100644 index 0000000..096c9ba --- /dev/null +++ b/framework/web/UrlRule.php @@ -0,0 +1,95 @@ + + * @since 2.0 + */ +class UrlRule extends Object +{ + /** + * @var string the URL suffix used for this rule. + * For example, ".html" can be used so that the URL looks like pointing to a static HTML page. + * Defaults to null, meaning using the value of {@link CUrlManager::urlSuffix}. + */ + public $urlSuffix; + /** + * @var boolean whether the rule is case sensitive. Defaults to null, meaning + * using the value of {@link CUrlManager::caseSensitive}. + */ + public $caseSensitive; + /** + * @var array the default GET parameters (name=>value) that this rule provides. + * When this rule is used to parse the incoming request, the values declared in this property + * will be injected into $_GET. + */ + public $defaultParams = array(); + /** + * @var boolean whether the GET parameter values should match the corresponding + * sub-patterns in the rule when creating a URL. Defaults to null, meaning using the value + * of {@link CUrlManager::matchValue}. When this property is false, it means + * a rule will be used for creating a URL if its route and parameter names match the given ones. + * If this property is set true, then the given parameter values must also match the corresponding + * parameter sub-patterns. Note that setting this property to true will degrade performance. + * @since 1.1.0 + */ + public $matchValue; + /** + * @var string the HTTP verb (e.g. GET, POST, DELETE) that this rule should match. + * If this rule can match multiple verbs, please separate them with commas. + * If this property is not set, the rule can match any verb. + * Note that this property is only used when parsing a request. It is ignored for URL creation. + * @since 1.1.7 + */ + public $verb; + /** + * @var boolean whether this rule is only used for request parsing. + * Defaults to false, meaning the rule is used for both URL parsing and creation. + * @since 1.1.7 + */ + public $parsingOnly = false; + /** + * @var string the controller/action pair + */ + public $route; + /** + * @var array the mapping from route param name to token name (e.g. _r1=><1>) + */ + public $references = array(); + /** + * @var string the pattern used to match route + */ + public $routePattern; + /** + * @var string regular expression used to parse a URL + */ + public $pattern; + /** + * @var string template used to construct a URL + */ + public $template; + /** + * @var array list of parameters (name=>regular expression) + */ + public $params = array(); + /** + * @var boolean whether the URL allows additional parameters at the end of the path info. + */ + public $append; + /** + * @var boolean whether host info should be considered for this rule + */ + public $hasHostInfo; +} From ab42ab2d52c96661d802a0e01b43b6988e0da10a Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Fri, 8 Feb 2013 16:32:43 -0500 Subject: [PATCH 022/117] url wip --- framework/web/UrlManager.php | 68 +++++++++++++++++++++++++++++++++-- framework/web/UrlRule.php | 84 +++++++++++--------------------------------- 2 files changed, 87 insertions(+), 65 deletions(-) diff --git a/framework/web/UrlManager.php b/framework/web/UrlManager.php index 1c80938..4b9a606 100644 --- a/framework/web/UrlManager.php +++ b/framework/web/UrlManager.php @@ -19,7 +19,70 @@ use \yii\base\Component; */ class UrlManager extends Component { + /** + * @var array the URL rules (pattern=>route). + */ + public $rules = array(); + /** + * @var string the URL suffix used when in 'path' format. + * For example, ".html" can be used so that the URL looks like pointing to a static HTML page. Defaults to empty. + */ + public $urlSuffix = ''; + /** + * @var boolean whether to show entry script name in the constructed URL. Defaults to true. + */ + public $showScriptName = true; + /** + * @var boolean whether to append GET parameters to the path info part. Defaults to true. + * This property is only effective when {@link urlFormat} is 'path' and is mainly used when + * creating URLs. When it is true, GET parameters will be appended to the path info and + * separate from each other using slashes. If this is false, GET parameters will be in query part. + */ + public $appendParams = true; + /** + * @var string the GET variable name for route. Defaults to 'r'. + */ public $routeVar = 'r'; + /** + * @var boolean whether routes are case-sensitive. Defaults to true. By setting this to false, + * the route in the incoming request will be turned to lower case first before further processing. + * As a result, you should follow the convention that you use lower case when specifying + * controller mapping ({@link CWebApplication::controllerMap}) and action mapping + * ({@link CController::actions}). Also, the directory names for organizing controllers should + * be in lower case. + */ + public $caseSensitive = true; + /** + * @var boolean whether the GET parameter values should match the corresponding + * sub-patterns in a rule before using it to create a URL. Defaults to false, meaning + * a rule will be used for creating a URL only if its route and parameter names match the given ones. + * If this property is set true, then the given parameter values must also match the corresponding + * parameter sub-patterns. Note that setting this property to true will degrade performance. + * @since 1.1.0 + */ + public $matchValue = false; + /** + * @var string the ID of the cache application component that is used to cache the parsed URL rules. + * Defaults to 'cache' which refers to the primary cache application component. + * Set this property to false if you want to disable caching URL rules. + */ + public $cacheID = 'cache'; + /** + * @var boolean whether to enable strict URL parsing. + * This property is only effective when {@link urlFormat} is 'path'. + * If it is set true, then an incoming URL must match one of the {@link rules URL rules}. + * Otherwise, it will be treated as an invalid request and trigger a 404 HTTP exception. + * Defaults to false. + */ + public $useStrictParsing = false; + /** + * @var string the class name or path alias for the URL rule instances. Defaults to 'CUrlRule'. + * If you change this to something else, please make sure that the new class must extend from + * {@link CBaseUrlRule} and have the same constructor signature as {@link CUrlRule}. + * It must also be serializable and autoloadable. + * @since 1.1.8 + */ + public $urlRuleClass = 'CUrlRule'; /** * Initializes the application component. @@ -44,10 +107,11 @@ class UrlManager extends Component */ public function parseUrl($request) { - if(isset($_GET[$this->routeVar])) + if (isset($_GET[$this->routeVar])) { return $_GET[$this->routeVar]; - else + } else { return ''; + } } public function createUrl($route, $params = array(), $ampersand = '&') diff --git a/framework/web/UrlRule.php b/framework/web/UrlRule.php index 096c9ba..1a4e77e 100644 --- a/framework/web/UrlRule.php +++ b/framework/web/UrlRule.php @@ -13,6 +13,11 @@ use yii\base\Object; /** * UrlManager manages the URLs of Yii applications. + * array( + * 'pattern' => 'post/', + * 'route' => 'post/view', + * 'params' => array('id' => 1), + * ) * * @author Qiang Xue * @since 2.0 @@ -20,76 +25,29 @@ use yii\base\Object; class UrlRule extends Object { /** - * @var string the URL suffix used for this rule. - * For example, ".html" can be used so that the URL looks like pointing to a static HTML page. - * Defaults to null, meaning using the value of {@link CUrlManager::urlSuffix}. - */ - public $urlSuffix; - /** - * @var boolean whether the rule is case sensitive. Defaults to null, meaning - * using the value of {@link CUrlManager::caseSensitive}. + * @var string regular expression used to parse a URL */ - public $caseSensitive; + public $pattern; /** * @var array the default GET parameters (name=>value) that this rule provides. * When this rule is used to parse the incoming request, the values declared in this property * will be injected into $_GET. */ - public $defaultParams = array(); - /** - * @var boolean whether the GET parameter values should match the corresponding - * sub-patterns in the rule when creating a URL. Defaults to null, meaning using the value - * of {@link CUrlManager::matchValue}. When this property is false, it means - * a rule will be used for creating a URL if its route and parameter names match the given ones. - * If this property is set true, then the given parameter values must also match the corresponding - * parameter sub-patterns. Note that setting this property to true will degrade performance. - * @since 1.1.0 - */ - public $matchValue; - /** - * @var string the HTTP verb (e.g. GET, POST, DELETE) that this rule should match. - * If this rule can match multiple verbs, please separate them with commas. - * If this property is not set, the rule can match any verb. - * Note that this property is only used when parsing a request. It is ignored for URL creation. - * @since 1.1.7 - */ - public $verb; - /** - * @var boolean whether this rule is only used for request parsing. - * Defaults to false, meaning the rule is used for both URL parsing and creation. - * @since 1.1.7 - */ - public $parsingOnly = false; - /** - * @var string the controller/action pair - */ - public $route; - /** - * @var array the mapping from route param name to token name (e.g. _r1=><1>) - */ - public $references = array(); - /** - * @var string the pattern used to match route - */ - public $routePattern; - /** - * @var string regular expression used to parse a URL - */ - public $pattern; - /** - * @var string template used to construct a URL - */ - public $template; - /** - * @var array list of parameters (name=>regular expression) - */ public $params = array(); /** - * @var boolean whether the URL allows additional parameters at the end of the path info. - */ - public $append; - /** - * @var boolean whether host info should be considered for this rule + * @var string the route to the controller action */ - public $hasHostInfo; + public $route; + + public function createUrl($route, $params, $ampersand) + { + + } + + public function parse($path) + { + $route = ''; + $params = array(); + return array($route, $params); + } } From 712f4dae0d4a8ba87b8a8f91d02281703d4f038f Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Sun, 10 Feb 2013 14:53:35 -0500 Subject: [PATCH 023/117] url wip --- docs/api/db/ActiveRecord.md | 4 +- framework/web/UrlManager.php | 38 ++- framework/web/UrlRule.php | 130 +++++++- tests/unit/framework/web/UrlManagerTest.php | 7 + tests/unit/framework/web/UrlRuleTest.php | 441 ++++++++++++++++++++++++++++ 5 files changed, 603 insertions(+), 17 deletions(-) create mode 100644 tests/unit/framework/web/UrlManagerTest.php create mode 100644 tests/unit/framework/web/UrlRuleTest.php diff --git a/docs/api/db/ActiveRecord.md b/docs/api/db/ActiveRecord.md index da281d8..822c548 100644 --- a/docs/api/db/ActiveRecord.md +++ b/docs/api/db/ActiveRecord.md @@ -300,7 +300,7 @@ foreach ($customers as $customer) { ~~~ How many SQL queries will be performed in the above code, assuming there are more than 100 customers in -the database? 101! The first SQL query brings back 100 customers. Then for each customer, another SQL query +the database? 101! The first SQL query brings back 100 customers. Then for each customer, a SQL query is performed to bring back the customer's orders. To solve the above performance problem, you can use the so-called *eager loading* by calling [[ActiveQuery::with()]]: @@ -318,7 +318,7 @@ foreach ($customers as $customer) { } ~~~ -As you can see, only two SQL queries were needed for the same task. +As you can see, only two SQL queries are needed for the same task. Sometimes, you may want to customize the relational queries on the fly. It can be diff --git a/framework/web/UrlManager.php b/framework/web/UrlManager.php index 4b9a606..e03f086 100644 --- a/framework/web/UrlManager.php +++ b/framework/web/UrlManager.php @@ -98,24 +98,50 @@ class UrlManager extends Component */ protected function processRules() { + foreach ($this->rules as $i => $rule) { + if (!isset($rule['class'])) { + $rule['class'] = 'yii\web\UrlRule'; + } + $this->rules[$i] = \Yii::createObject($rule); + } } /** * Parses the user request. - * @param HttpRequest $request the request application component + * @param Request $request the request application component * @return string the route (controllerID/actionID) and perhaps GET parameters in path format. */ public function parseUrl($request) { - if (isset($_GET[$this->routeVar])) { - return $_GET[$this->routeVar]; - } else { - return ''; + } + + public function createUrl($route, $params = array()) + { + $anchor = isset($params['#']) ? '#' . $params['#'] : ''; + unset($anchor['#']); + + /** @var $rule UrlRule */ + foreach ($this->rules as $rule) { + if (($url = $rule->createUrl($route, $params)) !== false) { + return $this->getBaseUrl() . $url . $anchor; + } } + + if ($params !== array()) { + $route .= '?' . http_build_query($params); + } + return $this->getBaseUrl() . '/' . $route . $anchor; } - public function createUrl($route, $params = array(), $ampersand = '&') + private $_baseUrl; + + public function getBaseUrl() { + return $this->_baseUrl; + } + public function setBaseUrl($value) + { + $this->_baseUrl = trim($value, '/'); } } diff --git a/framework/web/UrlRule.php b/framework/web/UrlRule.php index 1a4e77e..2961275 100644 --- a/framework/web/UrlRule.php +++ b/framework/web/UrlRule.php @@ -14,9 +14,15 @@ use yii\base\Object; /** * UrlManager manages the URLs of Yii applications. * array( - * 'pattern' => 'post/', + * 'pattern' => 'post/', * 'route' => 'post/view', - * 'params' => array('id' => 1), + * 'defaults' => array('page' => 1), + * ) + * + * array( + * 'pattern' => 'about', + * 'route' => 'site/page', + * 'defaults' => array('view' => 'about'), * ) * * @author Qiang Xue @@ -29,22 +35,128 @@ class UrlRule extends Object */ public $pattern; /** + * @var string the route to the controller action + */ + public $route; + /** * @var array the default GET parameters (name=>value) that this rule provides. * When this rule is used to parse the incoming request, the values declared in this property * will be injected into $_GET. */ - public $params = array(); - /** - * @var string the route to the controller action - */ - public $route; + public $defaults = array(); + + protected $paramRules = array(); + protected $routeRule; + protected $template; + protected $routeParams = array(); + + public function init() + { + $this->pattern = trim($this->pattern, '/'); + if ($this->pattern === '') { + $this->template = ''; + $this->pattern = '#^$#u'; + return; + } else { + $this->pattern = '/' . $this->pattern . '/'; + } + + $this->route = trim($this->route, '/'); + if (strpos($this->route, '<') !== false && preg_match_all('/<(\w+)>/', $this->route, $matches)) { + foreach ($matches[1] as $name) { + $this->routeParams[$name] = "<$name>"; + } + } + + $tr = $tr2 = array(); + if (preg_match_all('/<(\w+):?([^>]+)?>/', $this->pattern, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) { + foreach ($matches as $match) { + $name = $match[1][0]; + $pattern = isset($match[2][0]) ? $match[2][0] : '[^\/]+'; + if (isset($this->defaults[$name])) { + $length = strlen($match[0][0]); + $offset = $match[0][1]; + if ($this->pattern[$offset - 1] === '/' && $this->pattern[$offset + $length] === '/') { + $tr["<$name>"] = "(?P<$name>(?:/$pattern)?)"; + } else { + $tr["<$name>"] = "(?P<$name>(?:$pattern)?)"; + } + } else { + $tr["<$name>"] = "(?P<$name>$pattern)"; + } + if (isset($this->routeParams[$name])) { + $tr2["<$name>"] = "(?P<$name>$pattern)"; + } else { + $this->paramRules[$name] = $pattern === '[^\/]+' ? '' : "#^$pattern$#"; + } + } + } + + $this->template = preg_replace('/<(\w+):?([^>]+)?>/', '<$1>', $this->pattern); + $this->pattern = '#^' . strtr($this->template, $tr) . '$#u'; - public function createUrl($route, $params, $ampersand) + if ($this->routeParams !== array()) { + $this->routeRule = '#^' . strtr($this->route, $tr2) . '$#u'; + } + } + + public function parseUrl($pathInfo) { } - public function parse($path) + public function createUrl($route, $params) + { + $tr = array(); + + // match the route part first + if ($route !== $this->route) { + if ($this->routeRule !== null && preg_match($this->routeRule, $route, $matches)) { + foreach ($this->routeParams as $key => $name) { + $tr[$name] = $matches[$key]; + } + } else { + return false; + } + } + + // match default params + // if a default param is not in the route pattern, its value must also be matched + foreach ($this->defaults as $name => $value) { + if (!isset($params[$name])) { + return false; + } elseif (strcmp($params[$name], $value) === 0) { // strcmp will do string conversion automatically + unset($params[$name]); + if (isset($this->paramRules[$name])) { + $tr["<$name>"] = ''; + $tr["/<$name>/"] = '/'; + } + } elseif (!isset($this->paramRules[$name])) { + return false; + } + } + + // match params in the pattern + foreach ($this->paramRules as $name => $rule) { + if (isset($params[$name]) && ($rule === '' || preg_match($rule, $params[$name]))) { + $tr["<$name>"] = urlencode($params[$name]); + unset($params[$name]); + } elseif (!isset($this->defaults[$name]) || isset($params[$name])) { + return false; + } + } + + $url = trim(strtr($this->template, $tr), '/'); + if (strpos($url, '//') !== false) { + $url = preg_replace('#/+#', '/', $url); + } + if ($params !== array()) { + $url .= '?' . http_build_query($params); + } + return $url; + } + + public function parse($pathInfo) { $route = ''; $params = array(); diff --git a/tests/unit/framework/web/UrlManagerTest.php b/tests/unit/framework/web/UrlManagerTest.php new file mode 100644 index 0000000..3bd3007 --- /dev/null +++ b/tests/unit/framework/web/UrlManagerTest.php @@ -0,0 +1,7 @@ +getTestsForCreateUrl(); + foreach ($suites as $i => $suite) { + list ($name, $config, $tests) = $suite; + $rule = new UrlRule($config); + foreach ($tests as $j => $test) { + list ($route, $params, $expected) = $test; + $url = $rule->createUrl($route, $params); + $this->assertEquals($expected, $url, "Test#$i-$j: $name"); + } + } + } + + public function testParseUrl() + { + $suites = $this->getTestsForParseUrl(); + foreach ($suites as $i => $suite) { + list ($name, $config, $tests) = $suite; + $rule = new UrlRule($config); + foreach ($tests as $j => $test) { + $pathInfo = $test[0]; + $route = $test[1]; + $params = isset($test[2]) ? $test[2] : array(); + $result = $rule->parseUrl($pathInfo); + if ($route === false) { + $this->assertFalse($result, "Test#$i-$j: $name"); + } else { + $this->assertEquals(array($route, $params), $result, "Test#$i-$j: $name"); + } + } + } + } + + protected function getTestsForCreateUrl() + { + // structure of each test + // message for the test + // config for the URL rule + // list of inputs and outputs + // route + // params + // expected output + return array( + array( + 'empty pattern', + array( + 'pattern' => '', + 'route' => 'post/index', + ), + array( + array('post/index', array(), ''), + array('comment/index', array(), false), + array('post/index', array('page' => 1), '?page=1'), + ), + ), + array( + 'without param', + array( + 'pattern' => 'posts', + 'route' => 'post/index', + ), + array( + array('post/index', array(), 'posts'), + array('comment/index', array(), false), + array('post/index', array('page' => 1), 'posts?page=1'), + ), + ), + array( + 'with param', + array( + 'pattern' => 'post/', + 'route' => 'post/index', + ), + array( + array('post/index', array(), false), + array('comment/index', array(), false), + array('post/index', array('page' => 1), 'post/1'), + array('post/index', array('page' => 1, 'tag' => 'a'), 'post/1?tag=a'), + ), + ), + array( + 'with param requirement', + array( + 'pattern' => 'post/', + 'route' => 'post/index', + ), + array( + array('post/index', array('page' => 'abc'), false), + array('post/index', array('page' => 1), 'post/1'), + array('post/index', array('page' => 1, 'tag' => 'a'), 'post/1?tag=a'), + ), + ), + array( + 'with multiple params', + array( + 'pattern' => 'post/-', + 'route' => 'post/index', + ), + array( + array('post/index', array('page' => '1abc'), false), + array('post/index', array('page' => 1), false), + array('post/index', array('page' => 1, 'tag' => 'a'), 'post/1-a'), + ), + ), + array( + 'with optional param', + array( + 'pattern' => 'post//', + 'route' => 'post/index', + 'defaults' => array('page' => 1), + ), + array( + array('post/index', array('page' => 1), false), + array('post/index', array('page' => '1abc', 'tag' => 'a'), false), + array('post/index', array('page' => 1, 'tag' => 'a'), 'post/a'), + array('post/index', array('page' => 2, 'tag' => 'a'), 'post/2/a'), + ), + ), + array( + 'with optional param not in pattern', + array( + 'pattern' => 'post/', + 'route' => 'post/index', + 'defaults' => array('page' => 1), + ), + array( + array('post/index', array('page' => 1), false), + array('post/index', array('page' => '1abc', 'tag' => 'a'), false), + array('post/index', array('page' => 2, 'tag' => 'a'), false), + array('post/index', array('page' => 1, 'tag' => 'a'), 'post/a'), + ), + ), + array( + 'multiple optional params', + array( + 'pattern' => 'post///', + 'route' => 'post/index', + 'defaults' => array('page' => 1, 'sort' => 'yes'), + ), + array( + array('post/index', array('page' => 1), false), + array('post/index', array('page' => '1abc', 'tag' => 'a'), false), + array('post/index', array('page' => 1, 'tag' => 'a', 'sort' => 'YES'), false), + array('post/index', array('page' => 1, 'tag' => 'a', 'sort' => 'yes'), 'post/a'), + array('post/index', array('page' => 2, 'tag' => 'a', 'sort' => 'yes'), 'post/2/a'), + array('post/index', array('page' => 2, 'tag' => 'a', 'sort' => 'no'), 'post/2/a/no'), + array('post/index', array('page' => 1, 'tag' => 'a', 'sort' => 'no'), 'post/a/no'), + ), + ), + array( + 'optional param and required param separated by dashes', + array( + 'pattern' => 'post/-', + 'route' => 'post/index', + 'defaults' => array('page' => 1), + ), + array( + array('post/index', array('page' => 1), false), + array('post/index', array('page' => '1abc', 'tag' => 'a'), false), + array('post/index', array('page' => 1, 'tag' => 'a'), 'post/-a'), + array('post/index', array('page' => 2, 'tag' => 'a'), 'post/2-a'), + ), + ), + array( + 'optional param at the end', + array( + 'pattern' => 'post//', + 'route' => 'post/index', + 'defaults' => array('page' => 1), + ), + array( + array('post/index', array('page' => 1), false), + array('post/index', array('page' => '1abc', 'tag' => 'a'), false), + array('post/index', array('page' => 1, 'tag' => 'a'), 'post/a'), + array('post/index', array('page' => 2, 'tag' => 'a'), 'post/a/2'), + ), + ), + array( + 'consecutive optional params', + array( + 'pattern' => 'post//', + 'route' => 'post/index', + 'defaults' => array('page' => 1, 'tag' => 'a'), + ), + array( + array('post/index', array('page' => 1), false), + array('post/index', array('page' => '1abc', 'tag' => 'a'), false), + array('post/index', array('page' => 1, 'tag' => 'a'), 'post'), + array('post/index', array('page' => 2, 'tag' => 'a'), 'post/2'), + array('post/index', array('page' => 1, 'tag' => 'b'), 'post/b'), + array('post/index', array('page' => 2, 'tag' => 'b'), 'post/2/b'), + ), + ), + array( + 'consecutive optional params separated by dash', + array( + 'pattern' => 'post/-', + 'route' => 'post/index', + 'defaults' => array('page' => 1, 'tag' => 'a'), + ), + array( + array('post/index', array('page' => 1), false), + array('post/index', array('page' => '1abc', 'tag' => 'a'), false), + array('post/index', array('page' => 1, 'tag' => 'a'), 'post/-'), + array('post/index', array('page' => 1, 'tag' => 'b'), 'post/-b'), + array('post/index', array('page' => 2, 'tag' => 'a'), 'post/2-'), + array('post/index', array('page' => 2, 'tag' => 'b'), 'post/2-b'), + ), + ), + array( + 'route has parameters', + array( + 'pattern' => '/', + 'route' => '/', + 'defaults' => array(), + ), + array( + array('post/index', array('page' => 1), 'post/index?page=1'), + array('module/post/index', array(), false), + ), + ), + array( + 'route has parameters with regex', + array( + 'pattern' => '/', + 'route' => '/', + 'defaults' => array(), + ), + array( + array('post/index', array('page' => 1), 'post/index?page=1'), + array('comment/index', array('page' => 1), 'comment/index?page=1'), + array('test/index', array('page' => 1), false), + array('post', array(), false), + array('module/post/index', array(), false), + array('post/index', array('controller' => 'comment'), 'post/index?controller=comment'), + ), + ), + /* this is not supported + array( + 'route has default parameter', + array( + 'pattern' => '/', + 'route' => '/', + 'defaults' => array('action' => 'index'), + ), + array( + array('post/view', array('page' => 1), 'post/view?page=1'), + array('comment/view', array('page' => 1), 'comment/view?page=1'), + array('test/view', array('page' => 1), false), + array('post/index', array('page' => 1), 'post?page=1'), + ), + ), + */ + ); + } + + protected function getTestsForParseUrl() + { + // structure of each test + // message for the test + // config for the URL rule + // list of inputs and outputs + // pathInfo + // expected route, or false if the rule doesn't apply + // expected params, or not set if empty + return array( + array( + 'empty pattern', + array( + 'pattern' => '', + 'route' => 'post/index', + ), + array( + array('', 'post/index'), + array('a', false), + ), + ), + array( + 'without param', + array( + 'pattern' => 'posts', + 'route' => 'post/index', + ), + array( + array('posts', 'post/index'), + array('a', false), + ), + ), + array( + 'with param', + array( + 'pattern' => 'post/', + 'route' => 'post/index', + ), + array( + array('post/1', 'post/index', array('page' => '1')), + array('post/a', 'post/index', array('page' => 'a')), + array('post', false), + array('posts', false), + ), + ), + array( + 'with param requirement', + array( + 'pattern' => 'post/', + 'route' => 'post/index', + ), + array( + array('post/1', 'post/index', array('page' => '1')), + array('post/a', false), + array('post/1/a', false), + ), + ), + array( + 'with multiple params', + array( + 'pattern' => 'post/-', + 'route' => 'post/index', + ), + array( + array('post/1-a', 'post/index', array('page' => '1', 'tag' => 'a')), + array('post/a', false), + array('post/1', false), + array('post/1/a', false), + ), + ), + array( + 'with optional param', + array( + 'pattern' => 'post//', + 'route' => 'post/index', + 'defaults' => array('page' => 1), + ), + array( + array('post/1/a', 'post/index', array('page' => '1', 'tag' => 'a')), + array('post/2/a', 'post/index', array('page' => '2', 'tag' => 'a')), + array('post/a', 'post/index', array('page' => '1', 'tag' => 'a')), + array('post/1', 'post/index', array('page' => '1', 'tag' => '1')), + ), + ), + array( + 'with optional param not in pattern', + array( + 'pattern' => 'post/', + 'route' => 'post/index', + 'defaults' => array('page' => 1), + ), + array( + array('post/a', 'post/index', array('page' => '1', 'tag' => 'a')), + array('post/1', 'post/index', array('page' => '1', 'tag' => '1')), + array('post', false), + ), + ), + array( + 'multiple optional params', + array( + 'pattern' => 'post///', + 'route' => 'post/index', + 'defaults' => array('page' => 1, 'sort' => 'yes'), + ), + array( + array('post/1/a/yes', 'post/index', array('page' => '1', 'tag' => 'a', 'sort' => 'yes')), + array('post/2/a/no', 'post/index', array('page' => '2', 'tag' => 'a', 'sort' => 'no')), + array('post/2/a', 'post/index', array('page' => '2', 'tag' => 'a', 'sort' => 'yes')), + array('post/a/no', 'post/index', array('page' => '1', 'tag' => 'a', 'sort' => 'no')), + array('post/a', 'post/index', array('page' => '1', 'tag' => 'a', 'sort' => 'yes')), + array('post', false), + ), + ), + array( + 'optional param and required param separated by dashes', + array( + 'pattern' => 'post/-', + 'route' => 'post/index', + 'defaults' => array('page' => 1), + ), + array( + array('post/1-a', 'post/index', array('page' => '1', 'tag' => 'a')), + array('post/2-a', 'post/index', array('page' => '2', 'tag' => 'a')), + array('post/-a', 'post/index', array('page' => '1', 'tag' => 'a')), + array('post/a', false), + array('post-a', false), + ), + ), + array( + 'optional param at the end', + array( + 'pattern' => 'post//', + 'route' => 'post/index', + 'defaults' => array('page' => 1), + ), + array( + array('post/a/1', 'post/index', array('page' => '1', 'tag' => 'a')), + array('post/a/2', 'post/index', array('page' => '2', 'tag' => 'a')), + array('post/a', 'post/index', array('page' => '1', 'tag' => 'a')), + array('post/2', 'post/index', array('page' => '1', 'tag' => '2')), + array('post', false), + ), + ), + array( + 'consecutive optional params', + array( + 'pattern' => 'post//', + 'route' => 'post/index', + 'defaults' => array('page' => 1, 'tag' => 'a'), + ), + array( + array('post/2/b', 'post/index', array('page' => '2', 'tag' => 'b')), + array('post/2', 'post/index', array('page' => '2', 'tag' => 'a')), + array('post', 'post/index', array('page' => '1', 'tag' => 'a')), + array('post/b', 'post/index', array('page' => '1', 'tag' => 'b')), + array('post//b', false), + ), + ), + array( + 'consecutive optional params separated by dash', + array( + 'pattern' => 'post/-', + 'route' => 'post/index', + 'defaults' => array('page' => 1, 'tag' => 'a'), + ), + array( + array('post/2-b', 'post/index', array('page' => '2', 'tag' => 'b')), + array('post/2-', 'post/index', array('page' => '2', 'tag' => 'a')), + array('post/-b', 'post/index', array('page' => '1', 'tag' => 'b')), + array('post/-', 'post/index', array('page' => '1', 'tag' => 'a')), + array('post', false), + ), + ), + ); + } +} From f31369e7c177d45eb94a82964ac8cbc1b08fc1f0 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Sun, 10 Feb 2013 21:55:15 -0500 Subject: [PATCH 024/117] url wip --- framework/web/UrlRule.php | 12 ++++++++++-- tests/unit/framework/web/UrlRuleTest.php | 3 +-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/framework/web/UrlRule.php b/framework/web/UrlRule.php index 2961275..457b735 100644 --- a/framework/web/UrlRule.php +++ b/framework/web/UrlRule.php @@ -112,8 +112,13 @@ class UrlRule extends Object // match the route part first if ($route !== $this->route) { if ($this->routeRule !== null && preg_match($this->routeRule, $route, $matches)) { - foreach ($this->routeParams as $key => $name) { - $tr[$name] = $matches[$key]; + foreach ($this->routeParams as $name => $token) { + if (isset($this->defaults[$name]) && strcmp($this->defaults[$name], $matches[$name]) === 0) { + $tr[$token] = ''; + $tr["/$token/"] = '/'; + } else { + $tr[$token] = $matches[$name]; + } } } else { return false; @@ -123,6 +128,9 @@ class UrlRule extends Object // match default params // if a default param is not in the route pattern, its value must also be matched foreach ($this->defaults as $name => $value) { + if (isset($this->routeParams[$name])) { + continue; + } if (!isset($params[$name])) { return false; } elseif (strcmp($params[$name], $value) === 0) { // strcmp will do string conversion automatically diff --git a/tests/unit/framework/web/UrlRuleTest.php b/tests/unit/framework/web/UrlRuleTest.php index a73e395..1467454 100644 --- a/tests/unit/framework/web/UrlRuleTest.php +++ b/tests/unit/framework/web/UrlRuleTest.php @@ -244,7 +244,6 @@ class UrlRuleTest extends \yiiunit\TestCase array('post/index', array('controller' => 'comment'), 'post/index?controller=comment'), ), ), - /* this is not supported array( 'route has default parameter', array( @@ -256,10 +255,10 @@ class UrlRuleTest extends \yiiunit\TestCase array('post/view', array('page' => 1), 'post/view?page=1'), array('comment/view', array('page' => 1), 'comment/view?page=1'), array('test/view', array('page' => 1), false), + array('test/index', array('page' => 1), false), array('post/index', array('page' => 1), 'post?page=1'), ), ), - */ ); } From a946a863861f42f7f13c2a334015152acb24bbf9 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Mon, 11 Feb 2013 08:49:44 -0500 Subject: [PATCH 025/117] fixed parseUrl. --- framework/web/UrlRule.php | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/framework/web/UrlRule.php b/framework/web/UrlRule.php index 457b735..247633e 100644 --- a/framework/web/UrlRule.php +++ b/framework/web/UrlRule.php @@ -77,9 +77,9 @@ class UrlRule extends Object $length = strlen($match[0][0]); $offset = $match[0][1]; if ($this->pattern[$offset - 1] === '/' && $this->pattern[$offset + $length] === '/') { - $tr["<$name>"] = "(?P<$name>(?:/$pattern)?)"; + $tr["/<$name>"] = "(/(?P<$name>$pattern))?"; } else { - $tr["<$name>"] = "(?P<$name>(?:$pattern)?)"; + $tr["<$name>"] = "(?P<$name>$pattern)?"; } } else { $tr["<$name>"] = "(?P<$name>$pattern)"; @@ -93,7 +93,7 @@ class UrlRule extends Object } $this->template = preg_replace('/<(\w+):?([^>]+)?>/', '<$1>', $this->pattern); - $this->pattern = '#^' . strtr($this->template, $tr) . '$#u'; + $this->pattern = '#^' . trim(strtr($this->template, $tr), '/') . '$#u'; if ($this->routeParams !== array()) { $this->routeRule = '#^' . strtr($this->route, $tr2) . '$#u'; @@ -102,7 +102,26 @@ class UrlRule extends Object public function parseUrl($pathInfo) { - + if (!preg_match($this->pattern, $pathInfo, $matches)) { + return false; + } + $params = $this->defaults; + $tr = array(); + foreach ($matches as $name => $value) { + if ($value !== '') { + if (isset($this->routeParams[$name])) { + $tr[$this->routeParams[$name]] = $value; + } elseif (isset($this->paramRules[$name])) { + $params[$name] = $value; + } + } + } + if ($this->routeRule !== null) { + $route = strtr($this->route, $tr); + } else { + $route = $this->route; + } + return array($route, $params); } public function createUrl($route, $params) @@ -115,7 +134,6 @@ class UrlRule extends Object foreach ($this->routeParams as $name => $token) { if (isset($this->defaults[$name]) && strcmp($this->defaults[$name], $matches[$name]) === 0) { $tr[$token] = ''; - $tr["/$token/"] = '/'; } else { $tr[$token] = $matches[$name]; } @@ -137,7 +155,6 @@ class UrlRule extends Object unset($params[$name]); if (isset($this->paramRules[$name])) { $tr["<$name>"] = ''; - $tr["/<$name>/"] = '/'; } } elseif (!isset($this->paramRules[$name])) { return false; @@ -163,11 +180,4 @@ class UrlRule extends Object } return $url; } - - public function parse($pathInfo) - { - $route = ''; - $params = array(); - return array($route, $params); - } } From e25ad4bc88810e97f55585c9204a72eb7ea19470 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Mon, 11 Feb 2013 13:29:53 -0500 Subject: [PATCH 026/117] URL wip. --- framework/web/UrlRule.php | 58 ++++++++++++++++++++------------ tests/unit/framework/web/UrlRuleTest.php | 44 ++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 21 deletions(-) diff --git a/framework/web/UrlRule.php b/framework/web/UrlRule.php index 247633e..612a793 100644 --- a/framework/web/UrlRule.php +++ b/framework/web/UrlRule.php @@ -1,6 +1,6 @@ 'post/', - * 'route' => 'post/view', - * 'defaults' => array('page' => 1), - * ) - * - * array( - * 'pattern' => 'about', - * 'route' => 'site/page', - * 'defaults' => array('view' => 'about'), - * ) + * UrlRule represents a rule used for parsing and generating URLs. * * @author Qiang Xue * @since 2.0 @@ -45,13 +34,36 @@ class UrlRule extends Object */ public $defaults = array(); - protected $paramRules = array(); - protected $routeRule; + /** + * @var string the template for generating a new URL. This is derived from [[pattern]] and is used in generating URL. + */ protected $template; + /** + * @var string the regex for matching the route part. This is used in generating URL. + */ + protected $routeRule; + /** + * @var array list of regex for matching parameters. This is used in generating URL. + */ + protected $paramRules = array(); + /** + * @var array list of parameters used in the route. + */ protected $routeParams = array(); + /** + * Initializes this rule. + */ public function init() { + $this->compileRule(); + } + + /** + * Compiles the rule using the current configuration. + */ + protected function compileRule() + { $this->pattern = trim($this->pattern, '/'); if ($this->pattern === '') { $this->template = ''; @@ -105,15 +117,19 @@ class UrlRule extends Object if (!preg_match($this->pattern, $pathInfo, $matches)) { return false; } + foreach ($this->defaults as $name => $value) { + if (!isset($matches[$name]) || $matches[$name] === '') { + $matches[$name] = $value; + } + } $params = $this->defaults; $tr = array(); foreach ($matches as $name => $value) { - if ($value !== '') { - if (isset($this->routeParams[$name])) { - $tr[$this->routeParams[$name]] = $value; - } elseif (isset($this->paramRules[$name])) { - $params[$name] = $value; - } + if (isset($this->routeParams[$name])) { + $tr[$this->routeParams[$name]] = $value; + unset($params[$name]); + } elseif (isset($this->paramRules[$name])) { + $params[$name] = $value; } } if ($this->routeRule !== null) { diff --git a/tests/unit/framework/web/UrlRuleTest.php b/tests/unit/framework/web/UrlRuleTest.php index 1467454..e9148c7 100644 --- a/tests/unit/framework/web/UrlRuleTest.php +++ b/tests/unit/framework/web/UrlRuleTest.php @@ -435,6 +435,50 @@ class UrlRuleTest extends \yiiunit\TestCase array('post', false), ), ), + array( + 'route has parameters', + array( + 'pattern' => '/', + 'route' => '/', + 'defaults' => array(), + ), + array( + array('post/index', 'post/index'), + array('module/post/index', false), + ), + ), + array( + 'route has parameters with regex', + array( + 'pattern' => '/', + 'route' => '/', + 'defaults' => array(), + ), + array( + array('post/index', 'post/index'), + array('comment/index', 'comment/index'), + array('test/index', false), + array('post', false), + array('module/post/index', false), + ), + ), + array( + 'route has default parameter', + array( + 'pattern' => '/', + 'route' => '/', + 'defaults' => array('action' => 'index'), + ), + array( + array('post/view', 'post/view'), + array('comment/view', 'comment/view'), + array('test/view', false), + array('post', 'post/index'), + array('posts', false), + array('test', false), + array('index', false), + ), + ), ); } } From 842caa3aaedfde0477ae21fc6f6bf5a7225479d6 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Tue, 12 Feb 2013 00:08:28 +0400 Subject: [PATCH 027/117] Yii-style for PHP fatal and parse errors --- framework/base/Application.php | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/framework/base/Application.php b/framework/base/Application.php index 55dc0cc..b0167df 100644 --- a/framework/base/Application.php +++ b/framework/base/Application.php @@ -126,6 +126,7 @@ class Application extends Module */ public function init() { + ob_start(); $this->preloadComponents(); } @@ -138,10 +139,27 @@ class Application extends Module */ public function end($status = 0, $exit = true) { + $lastError = error_get_last(); + + if(isset($lastError['type']) && in_array($lastError['type'], array(E_ERROR, E_PARSE))) { + ob_end_clean(); + $exception = new \ErrorException($lastError['message'], 0, $lastError['type'], $lastError['file'], $lastError['line']); + $this->logException($exception); + + if (($handler = $this->getErrorHandler()) !== null) { + $handler->handle($exception); + } else { + $this->renderException($exception); + } + + die(1); + } + if (!$this->_ended) { $this->_ended = true; $this->afterRequest(); } + ob_end_flush(); if ($exit) { exit($status); } @@ -155,6 +173,7 @@ class Application extends Module public function run() { $this->beforeRequest(); + register_shutdown_function(array($this,'end'),0,false); $status = $this->processRequest(); $this->afterRequest(); return $status; From f65a0bd9acc946ab068152a31d7783ce0b47ef4a Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Mon, 11 Feb 2013 16:34:02 -0500 Subject: [PATCH 028/117] url wip --- framework/web/UrlRule.php | 94 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 64 insertions(+), 30 deletions(-) diff --git a/framework/web/UrlRule.php b/framework/web/UrlRule.php index 612a793..f11fd16 100644 --- a/framework/web/UrlRule.php +++ b/framework/web/UrlRule.php @@ -33,40 +33,66 @@ class UrlRule extends Object * will be injected into $_GET. */ public $defaults = array(); + /** + * @var boolean whether this rule is only used for request parsing. + * Defaults to false, meaning the rule is used for both URL parsing and creation. + */ + public $parsingOnly = false; + /** + * @var string the URL suffix used for this rule. + * For example, ".html" can be used so that the URL looks like pointing to a static HTML page. + * If not, the value of [[UrlManager::suffix]] will be used. + */ + public $suffix; + /** + * @var string|array the HTTP verb (e.g. GET, POST, DELETE) that this rule should match. + * Use array to represent multiple verbs that this rule may match. + * If this property is not set, the rule can match any verb. + * Note that this property is only used when parsing a request. It is ignored for URL creation. + * @see parsingOnly + */ + public $verb; + /** + * @var string the host info (e.g. `http://www.example.com`) that this rule should match. + * If not set, it means the host info is ignored. + */ + public $hostInfo; /** * @var string the template for generating a new URL. This is derived from [[pattern]] and is used in generating URL. */ - protected $template; + private $_template; /** * @var string the regex for matching the route part. This is used in generating URL. */ - protected $routeRule; + private $_routeRule; /** * @var array list of regex for matching parameters. This is used in generating URL. */ - protected $paramRules = array(); + private $_paramRules = array(); /** * @var array list of parameters used in the route. */ - protected $routeParams = array(); + private $_routeParams = array(); /** * Initializes this rule. */ public function init() { - $this->compileRule(); - } + if ($this->verb !== null) { + if (is_array($this->verb)) { + foreach ($this->verb as $i => $verb) { + $this->verb[$i] = strtoupper($verb); + } + } else { + $this->verb = array(strtoupper($this->verb)); + } + } - /** - * Compiles the rule using the current configuration. - */ - protected function compileRule() - { $this->pattern = trim($this->pattern, '/'); if ($this->pattern === '') { - $this->template = ''; + $this->_template = ''; $this->pattern = '#^$#u'; return; } else { @@ -76,7 +102,7 @@ class UrlRule extends Object $this->route = trim($this->route, '/'); if (strpos($this->route, '<') !== false && preg_match_all('/<(\w+)>/', $this->route, $matches)) { foreach ($matches[1] as $name) { - $this->routeParams[$name] = "<$name>"; + $this->_routeParams[$name] = "<$name>"; } } @@ -96,24 +122,28 @@ class UrlRule extends Object } else { $tr["<$name>"] = "(?P<$name>$pattern)"; } - if (isset($this->routeParams[$name])) { + if (isset($this->_routeParams[$name])) { $tr2["<$name>"] = "(?P<$name>$pattern)"; } else { - $this->paramRules[$name] = $pattern === '[^\/]+' ? '' : "#^$pattern$#"; + $this->_paramRules[$name] = $pattern === '[^\/]+' ? '' : "#^$pattern$#"; } } } - $this->template = preg_replace('/<(\w+):?([^>]+)?>/', '<$1>', $this->pattern); - $this->pattern = '#^' . trim(strtr($this->template, $tr), '/') . '$#u'; + $this->_template = preg_replace('/<(\w+):?([^>]+)?>/', '<$1>', $this->pattern); + $this->pattern = '#^' . trim(strtr($this->_template, $tr), '/') . '$#u'; - if ($this->routeParams !== array()) { - $this->routeRule = '#^' . strtr($this->route, $tr2) . '$#u'; + if ($this->_routeParams !== array()) { + $this->_routeRule = '#^' . strtr($this->route, $tr2) . '$#u'; } } public function parseUrl($pathInfo) { + if ($this->verb !== null && !in_array(\Yii::$app->getRequest()->verb, $this->verb, true)) { + return false; + } + if (!preg_match($this->pattern, $pathInfo, $matches)) { return false; } @@ -125,14 +155,14 @@ class UrlRule extends Object $params = $this->defaults; $tr = array(); foreach ($matches as $name => $value) { - if (isset($this->routeParams[$name])) { - $tr[$this->routeParams[$name]] = $value; + if (isset($this->_routeParams[$name])) { + $tr[$this->_routeParams[$name]] = $value; unset($params[$name]); - } elseif (isset($this->paramRules[$name])) { + } elseif (isset($this->_paramRules[$name])) { $params[$name] = $value; } } - if ($this->routeRule !== null) { + if ($this->_routeRule !== null) { $route = strtr($this->route, $tr); } else { $route = $this->route; @@ -142,12 +172,16 @@ class UrlRule extends Object public function createUrl($route, $params) { + if ($this->parsingOnly) { + return false; + } + $tr = array(); // match the route part first if ($route !== $this->route) { - if ($this->routeRule !== null && preg_match($this->routeRule, $route, $matches)) { - foreach ($this->routeParams as $name => $token) { + if ($this->_routeRule !== null && preg_match($this->_routeRule, $route, $matches)) { + foreach ($this->_routeParams as $name => $token) { if (isset($this->defaults[$name]) && strcmp($this->defaults[$name], $matches[$name]) === 0) { $tr[$token] = ''; } else { @@ -162,23 +196,23 @@ class UrlRule extends Object // match default params // if a default param is not in the route pattern, its value must also be matched foreach ($this->defaults as $name => $value) { - if (isset($this->routeParams[$name])) { + if (isset($this->_routeParams[$name])) { continue; } if (!isset($params[$name])) { return false; } elseif (strcmp($params[$name], $value) === 0) { // strcmp will do string conversion automatically unset($params[$name]); - if (isset($this->paramRules[$name])) { + if (isset($this->_paramRules[$name])) { $tr["<$name>"] = ''; } - } elseif (!isset($this->paramRules[$name])) { + } elseif (!isset($this->_paramRules[$name])) { return false; } } // match params in the pattern - foreach ($this->paramRules as $name => $rule) { + foreach ($this->_paramRules as $name => $rule) { if (isset($params[$name]) && ($rule === '' || preg_match($rule, $params[$name]))) { $tr["<$name>"] = urlencode($params[$name]); unset($params[$name]); @@ -187,7 +221,7 @@ class UrlRule extends Object } } - $url = trim(strtr($this->template, $tr), '/'); + $url = trim(strtr($this->_template, $tr), '/'); if (strpos($url, '//') !== false) { $url = preg_replace('#/+#', '/', $url); } From 12fbb0f71d2357df5d4ad776160e3b34fafeb8f7 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Mon, 11 Feb 2013 19:51:18 -0500 Subject: [PATCH 029/117] url WIP --- framework/web/UrlManager.php | 20 ++++- framework/web/UrlRule.php | 67 +++++++++++++--- tests/unit/framework/web/UrlRuleTest.php | 133 ++++++++++++++++++++++++++++++- 3 files changed, 207 insertions(+), 13 deletions(-) diff --git a/framework/web/UrlManager.php b/framework/web/UrlManager.php index e03f086..8f927e8 100644 --- a/framework/web/UrlManager.php +++ b/framework/web/UrlManager.php @@ -27,7 +27,7 @@ class UrlManager extends Component * @var string the URL suffix used when in 'path' format. * For example, ".html" can be used so that the URL looks like pointing to a static HTML page. Defaults to empty. */ - public $urlSuffix = ''; + public $suffix; /** * @var boolean whether to show entry script name in the constructed URL. Defaults to true. */ @@ -122,7 +122,7 @@ class UrlManager extends Component /** @var $rule UrlRule */ foreach ($this->rules as $rule) { - if (($url = $rule->createUrl($route, $params)) !== false) { + if (($url = $rule->createUrl($this, $route, $params)) !== false) { return $this->getBaseUrl() . $url . $anchor; } } @@ -144,4 +144,20 @@ class UrlManager extends Component { $this->_baseUrl = trim($value, '/'); } + + /** + * Removes the URL suffix from path info. + * @param string $pathInfo path info part in the URL + * @param string $suffix the URL suffix to be removed + * @return string path info with URL suffix removed. + */ + public function removeSuffix($pathInfo, $suffix) + { + $n = strlen($suffix); + if ($n > 0 && substr($pathInfo, -$n) === $suffix) { + return substr($pathInfo, 0, -$n); + } else { + return $pathInfo; + } + } } diff --git a/framework/web/UrlRule.php b/framework/web/UrlRule.php index f11fd16..471712b 100644 --- a/framework/web/UrlRule.php +++ b/framework/web/UrlRule.php @@ -20,6 +20,15 @@ use yii\base\Object; class UrlRule extends Object { /** + * Set [[mode]] with this value to mark that this rule is for URL parsing only + */ + const PARSING_ONLY = 1; + /** + * Set [[mode]] with this value to mark that this rule is for URL creation only + */ + const CREATION_ONLY = 2; + + /** * @var string regular expression used to parse a URL */ public $pattern; @@ -34,11 +43,6 @@ class UrlRule extends Object */ public $defaults = array(); /** - * @var boolean whether this rule is only used for request parsing. - * Defaults to false, meaning the rule is used for both URL parsing and creation. - */ - public $parsingOnly = false; - /** * @var string the URL suffix used for this rule. * For example, ".html" can be used so that the URL looks like pointing to a static HTML page. * If not, the value of [[UrlManager::suffix]] will be used. @@ -49,7 +53,6 @@ class UrlRule extends Object * Use array to represent multiple verbs that this rule may match. * If this property is not set, the rule can match any verb. * Note that this property is only used when parsing a request. It is ignored for URL creation. - * @see parsingOnly */ public $verb; /** @@ -57,6 +60,14 @@ class UrlRule extends Object * If not set, it means the host info is ignored. */ public $hostInfo; + /** + * @var integer a value indicating if this rule should be used for both URL parsing and creation, + * parsing only, or creation only. + * If not set, it means the rule is both URL parsing and creation. + * If it is [[PARSING_ONLY]], the rule is for URL parsing only. + * If it is [[CREATION_ONLY]], the rule is for URL creation only. + */ + public $mode; /** * @var string the template for generating a new URL. This is derived from [[pattern]] and is used in generating URL. @@ -138,12 +149,38 @@ class UrlRule extends Object } } - public function parseUrl($pathInfo) + /** + * Parses the given path info and returns the corresponding route and parameters. + * @param UrlManager $manager the URL manager + * @param string $pathInfo the path info to be parsed. It should not have slashes at the beginning or the end. + * @return array|boolean the parsing result. The route and the parameters are returned as an array. + * If false, it means this rule cannot be used to parse this path info. + */ + public function parseUrl($manager, $pathInfo) { + if ($this->mode === self::CREATION_ONLY) { + return false; + } + if ($this->verb !== null && !in_array(\Yii::$app->getRequest()->verb, $this->verb, true)) { return false; } + $suffix = (string)($this->suffix === null ? $manager->suffix : $this->suffix); + if ($suffix !== '' && $pathInfo !== '') { + $n = strlen($suffix); + if (substr($pathInfo, -$n) === $suffix) { + $pathInfo = substr($pathInfo, 0, -$n); + if ($pathInfo === '') { + // suffix alone is not allowed + return false; + } + } elseif ($suffix !== '/') { + // we allow the ending '/' to be optional if it is a suffix + return false; + } + } + if (!preg_match($this->pattern, $pathInfo, $matches)) { return false; } @@ -170,9 +207,16 @@ class UrlRule extends Object return array($route, $params); } - public function createUrl($route, $params) + /** + * Creates a URL according to the given route and parameters. + * @param UrlManager $manager the URL manager + * @param string $route the route. It should not have slashes at the beginning or the end. + * @param array $params the parameters + * @return string|boolean the created URL, or false if this rule cannot be used for creating this URL. + */ + public function createUrl($manager, $route, $params) { - if ($this->parsingOnly) { + if ($this->mode === self::PARSING_ONLY) { return false; } @@ -225,6 +269,11 @@ class UrlRule extends Object if (strpos($url, '//') !== false) { $url = preg_replace('#/+#', '/', $url); } + + if ($url !== '') { + $url .= ($this->suffix === null ? $manager->suffix : $this->suffix); + } + if ($params !== array()) { $url .= '?' . http_build_query($params); } diff --git a/tests/unit/framework/web/UrlRuleTest.php b/tests/unit/framework/web/UrlRuleTest.php index e9148c7..fd9a8bd 100644 --- a/tests/unit/framework/web/UrlRuleTest.php +++ b/tests/unit/framework/web/UrlRuleTest.php @@ -2,19 +2,21 @@ namespace yiiunit\framework\web; +use yii\web\UrlManager; use yii\web\UrlRule; class UrlRuleTest extends \yiiunit\TestCase { public function testCreateUrl() { + $manager = new UrlManager; $suites = $this->getTestsForCreateUrl(); foreach ($suites as $i => $suite) { list ($name, $config, $tests) = $suite; $rule = new UrlRule($config); foreach ($tests as $j => $test) { list ($route, $params, $expected) = $test; - $url = $rule->createUrl($route, $params); + $url = $rule->createUrl($manager, $route, $params); $this->assertEquals($expected, $url, "Test#$i-$j: $name"); } } @@ -22,6 +24,7 @@ class UrlRuleTest extends \yiiunit\TestCase public function testParseUrl() { + $manager = new UrlManager; $suites = $this->getTestsForParseUrl(); foreach ($suites as $i => $suite) { list ($name, $config, $tests) = $suite; @@ -30,7 +33,7 @@ class UrlRuleTest extends \yiiunit\TestCase $pathInfo = $test[0]; $route = $test[1]; $params = isset($test[2]) ? $test[2] : array(); - $result = $rule->parseUrl($pathInfo); + $result = $rule->parseUrl($manager, $pathInfo); if ($route === false) { $this->assertFalse($result, "Test#$i-$j: $name"); } else { @@ -75,6 +78,17 @@ class UrlRuleTest extends \yiiunit\TestCase ), ), array( + 'parsing only', + array( + 'pattern' => 'posts', + 'route' => 'post/index', + 'mode' => UrlRule::PARSING_ONLY, + ), + array( + array('post/index', array(), false), + ), + ), + array( 'with param', array( 'pattern' => 'post/', @@ -259,6 +273,58 @@ class UrlRuleTest extends \yiiunit\TestCase array('post/index', array('page' => 1), 'post?page=1'), ), ), + array( + 'empty pattern with suffix', + array( + 'pattern' => '', + 'route' => 'post/index', + 'suffix' => '.html', + ), + array( + array('post/index', array(), ''), + array('comment/index', array(), false), + array('post/index', array('page' => 1), '?page=1'), + ), + ), + array( + 'regular pattern with suffix', + array( + 'pattern' => 'posts', + 'route' => 'post/index', + 'suffix' => '.html', + ), + array( + array('post/index', array(), 'posts.html'), + array('comment/index', array(), false), + array('post/index', array('page' => 1), 'posts.html?page=1'), + ), + ), + array( + 'empty pattern with slash suffix', + array( + 'pattern' => '', + 'route' => 'post/index', + 'suffix' => '/', + ), + array( + array('post/index', array(), ''), + array('comment/index', array(), false), + array('post/index', array('page' => 1), '?page=1'), + ), + ), + array( + 'regular pattern with slash suffix', + array( + 'pattern' => 'posts', + 'route' => 'post/index', + 'suffix' => '/', + ), + array( + array('post/index', array(), 'posts/'), + array('comment/index', array(), false), + array('post/index', array('page' => 1), 'posts/?page=1'), + ), + ), ); } @@ -295,6 +361,17 @@ class UrlRuleTest extends \yiiunit\TestCase ), ), array( + 'creation only', + array( + 'pattern' => 'posts', + 'route' => 'post/index', + 'mode' => UrlRule::CREATION_ONLY, + ), + array( + array('posts', false), + ), + ), + array( 'with param', array( 'pattern' => 'post/', @@ -479,6 +556,58 @@ class UrlRuleTest extends \yiiunit\TestCase array('index', false), ), ), + array( + 'empty pattern with suffix', + array( + 'pattern' => '', + 'route' => 'post/index', + 'suffix' => '.html', + ), + array( + array('', 'post/index'), + array('.html', false), + array('a.html', false), + ), + ), + array( + 'regular pattern with suffix', + array( + 'pattern' => 'posts', + 'route' => 'post/index', + 'suffix' => '.html', + ), + array( + array('posts.html', 'post/index'), + array('posts', false), + array('posts.HTML', false), + array('a.html', false), + array('a', false), + ), + ), + array( + 'empty pattern with slash suffix', + array( + 'pattern' => '', + 'route' => 'post/index', + 'suffix' => '/', + ), + array( + array('', 'post/index'), + array('a', false), + ), + ), + array( + 'regular pattern with slash suffix', + array( + 'pattern' => 'posts', + 'route' => 'post/index', + 'suffix' => '/', + ), + array( + array('posts', 'post/index'), + array('a', false), + ), + ), ); } } From 2d5db95149ed711ceca24144aa2208cd4561674b Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Wed, 13 Feb 2013 03:51:29 +0400 Subject: [PATCH 030/117] More on error handling: - Correct exit code for require errors in CLI mode. It was 0 even in case of error. - Solved handling fatals and displaying custom error template w/o output buffering. - If XDEBUG is available, error screen will display trace (implementation inspired by Kohana and Nette). - Added ErrorException to store/display type of the error and user-friendly messages. - Fatals are still logged in PHP log. - Turned off native display_errors since we're now catching all errors. - Added reserving memory (256kb) . Can be removed safely. In case of removal we're losing only memory exhausted error when allocating last very small chunk of memory. - Properly handled errors in __toString (exception can't be thrown in this case). - In the YII_DEBUG===false mode it's bad to display exception name even in page title. - In the YII_DEBUG===true mode it's still useful to get user-friendly message additionally to exception name. --- framework/base/Application.php | 88 ++++++++++++++++++++++++++++++--------- framework/base/ErrorException.php | 47 +++++++++++++++++++++ framework/base/Exception.php | 3 +- framework/views/error.php | 7 ++-- framework/views/exception.php | 5 ++- 5 files changed, 123 insertions(+), 27 deletions(-) create mode 100644 framework/base/ErrorException.php diff --git a/framework/base/Application.php b/framework/base/Application.php index b0167df..7c06e79 100644 --- a/framework/base/Application.php +++ b/framework/base/Application.php @@ -97,6 +97,12 @@ class Application extends Module private $_language; /** + * @var string Used to reserve memory for fatal error handler. This memory + * reserve can be removed if it's OK to write to PHP log only in this particular case. + */ + private $_memoryReserve; + + /** * Constructor. * @param string $id the ID of this application. The ID should uniquely identify the application from others. * @param string $basePath the base path of this application. This should point to @@ -110,6 +116,7 @@ class Application extends Module $this->setBasePath($basePath); if (YII_ENABLE_ERROR_HANDLER) { + ini_set('display_errors', 0); set_exception_handler(array($this, 'handleException')); set_error_handler(array($this, 'handleError'), error_reporting()); } @@ -126,7 +133,6 @@ class Application extends Module */ public function init() { - ob_start(); $this->preloadComponents(); } @@ -139,27 +145,54 @@ class Application extends Module */ public function end($status = 0, $exit = true) { - $lastError = error_get_last(); - - if(isset($lastError['type']) && in_array($lastError['type'], array(E_ERROR, E_PARSE))) { - ob_end_clean(); - $exception = new \ErrorException($lastError['message'], 0, $lastError['type'], $lastError['file'], $lastError['line']); - $this->logException($exception); - - if (($handler = $this->getErrorHandler()) !== null) { - $handler->handle($exception); - } else { - $this->renderException($exception); - } - - die(1); - } - if (!$this->_ended) { $this->_ended = true; $this->afterRequest(); } - ob_end_flush(); + + if(YII_ENABLE_ERROR_HANDLER) { + $error = error_get_last(); + + if(isset($error['type']) && in_array($error['type'], ErrorException::getFatalCodes())) { + unset($this->_memoryReserve); + $exception = new ErrorException($error['message'], $error['type'], $error['type'], $error['file'], $error['line']); + + if(function_exists('xdebug_get_function_stack')) { + $trace = array_slice(array_reverse(xdebug_get_function_stack()), 4, -1); + foreach($trace as &$frame) { + if(!isset($frame['function'])) { + $frame['function'] = 'unknown'; + } + + // XDebug < 2.1.1: http://bugs.xdebug.org/view.php?id=695 + if(!isset($frame['type'])) { + $frame['type'] = '::'; + } + + // XDebug has a different key name + $frame['args'] = array(); + if(isset($frame['params']) && !isset($frame['args'])) { + $frame['args'] = $frame['params']; + } + } + + $ref = new \ReflectionProperty('Exception', 'trace'); + $ref->setAccessible(true); + $ref->setValue($exception, $trace); + } + + $this->logException($exception); + + if (($handler = $this->getErrorHandler()) !== null) { + $handler->handle($exception); + } else { + $this->renderException($exception); + } + + $status = 1; + } + } + if ($exit) { exit($status); } @@ -173,6 +206,9 @@ class Application extends Module public function run() { $this->beforeRequest(); + // Allocating twice more than required to display memory exhausted error + // in case of trying to allocate last 1 byte while all memory is taken. + $this->_memoryReserve = str_repeat('x', 1024*256); register_shutdown_function(array($this,'end'),0,false); $status = $this->processRequest(); $this->afterRequest(); @@ -394,12 +430,24 @@ class Application extends Module * @param string $message the error message * @param string $file the filename that the error was raised in * @param integer $line the line number the error was raised at - * @throws \ErrorException the error exception + * + * @throws ErrorException */ public function handleError($code, $message, $file, $line) { if (error_reporting() !== 0) { - throw new \ErrorException($message, 0, $code, $file, $line); + $exception = new ErrorException($message, $code, $code, $file, $line); + + // in case error appeared in __toString method we can't throw any exception + $trace = debug_backtrace(false); + array_shift($trace); + foreach($trace as $frame) { + if($frame['function'] == '__toString') { + $this->handleException($exception); + } + } + + throw $exception; } } diff --git a/framework/base/ErrorException.php b/framework/base/ErrorException.php new file mode 100644 index 0000000..073c1c3 --- /dev/null +++ b/framework/base/ErrorException.php @@ -0,0 +1,47 @@ + + * @since 2.0 + */ +class ErrorException extends \ErrorException +{ + public static function getFatalCodes() + { + return array(E_ERROR, E_PARSE, E_CORE_ERROR, E_CORE_WARNING, E_COMPILE_ERROR, E_COMPILE_WARNING); + } + + /** + * @return string the user-friendly name of this exception + */ + public function getName() + { + $names = array( + E_ERROR => \Yii::t('yii|Fatal Error'), + E_PARSE => \Yii::t('yii|Parse Error'), + E_CORE_ERROR => \Yii::t('yii|Core Error'), + E_COMPILE_ERROR => \Yii::t('yii|Compile Error'), + E_USER_ERROR => \Yii::t('yii|User Error'), + E_WARNING => \Yii::t('yii|Warning'), + E_CORE_WARNING => \Yii::t('yii|Core Warning'), + E_COMPILE_WARNING => \Yii::t('yii|Compile Warning'), + E_USER_WARNING => \Yii::t('yii|User Warning'), + E_STRICT => \Yii::t('yii|Strict'), + E_NOTICE => \Yii::t('yii|Notice'), + E_RECOVERABLE_ERROR => \Yii::t('yii|Recoverable Error'), + E_DEPRECATED => \Yii::t('yii|Deprecated'), + ); + return isset($names[$this->getCode()]) ? $names[$this->getCode()] : \Yii::t('yii|Error'); + } +} diff --git a/framework/base/Exception.php b/framework/base/Exception.php index 0088a55..7dcd7c5 100644 --- a/framework/base/Exception.php +++ b/framework/base/Exception.php @@ -24,5 +24,4 @@ class Exception extends \Exception { return \Yii::t('yii|Exception'); } -} - +} \ No newline at end of file diff --git a/framework/views/error.php b/framework/views/error.php index c1700f2..28c50f1 100644 --- a/framework/views/error.php +++ b/framework/views/error.php @@ -4,12 +4,13 @@ * @var \yii\base\ErrorHandler $owner */ $owner = $this->owner; +$title = $owner->htmlEncode($exception instanceof \yii\base\Exception || $exception instanceof \yii\base\ErrorException ? $exception->getName() : get_class($exception)); ?> - <?php echo get_class($exception)?> + <?php echo $title?> "; + } + + /** + * Registers a 'refresh' meta tag. + * This method can be invoked anywhere in a view. It will register a 'refresh' + * meta tag with {@link CClientScript} so that the page can be refreshed in + * the specified seconds. + * @param integer $seconds the number of seconds to wait before refreshing the page + * @param string $url the URL to which the page should be redirected to. If empty, it means the current page. + * @since 1.1.1 + */ + public static function refresh($seconds, $url = '') + { + $content = "$seconds"; + if ($url !== '') { + $content .= ';' . static::normalizeUrl($url); + } + Yii::app()->clientScript->registerMetaTag($content, null, 'refresh'); + } + + /** + * Links to the specified CSS file. + * @param string $url the CSS URL + * @param string $media the media that this CSS should apply to. + * @return string the CSS link. + */ + public static function cssFile($url, $media = '') + { + return CHtml::linkTag('stylesheet', 'text/css', $url, $media !== '' ? $media : null); + } + + /** + * Encloses the given JavaScript within a script tag. + * @param string $text the JavaScript to be enclosed + * @return string the enclosed JavaScript + */ + public static function script($text) + { + return ""; + } + + /** + * Includes a JavaScript file. + * @param string $url URL for the JavaScript file + * @return string the JavaScript file tag + */ + public static function scriptFile($url) + { + return ''; + } + + /** + * Generates an opening form tag. + * This is a shortcut to {@link beginForm}. + * @param mixed $action the form action URL (see {@link normalizeUrl} for details about this parameter.) + * @param string $method form method (e.g. post, get) + * @param array $htmlOptions additional HTML attributes (see {@link tag}). + * @return string the generated form tag. + */ + public static function form($action = '', $method = 'post', $htmlOptions = array()) + { + return static::beginForm($action, $method, $htmlOptions); + } + + /** + * Generates an opening form tag. + * Note, only the open tag is generated. A close tag should be placed manually + * at the end of the form. + * @param mixed $action the form action URL (see {@link normalizeUrl} for details about this parameter.) + * @param string $method form method (e.g. post, get) + * @param array $htmlOptions additional HTML attributes (see {@link tag}). + * @return string the generated form tag. + * @see endForm + */ + public static function beginForm($action = '', $method = 'post', $htmlOptions = array()) + { + $htmlOptions['action'] = $url = static::normalizeUrl($action); + $htmlOptions['method'] = $method; + $form = static::tag('form', $htmlOptions, false, false); + $hiddens = array(); + if (!strcasecmp($method, 'get') && ($pos = strpos($url, '?')) !== false) { + foreach (explode('&', substr($url, $pos + 1)) as $pair) { + if (($pos = strpos($pair, '=')) !== false) { + $hiddens[] = static::hiddenField(urldecode(substr($pair, 0, $pos)), urldecode(substr($pair, $pos + 1)), array('id' => false)); + } else { + $hiddens[] = static::hiddenField(urldecode($pair), '', array('id' => false)); + } + } + } + $request = Yii::app()->request; + if ($request->enableCsrfValidation && !strcasecmp($method, 'post')) { + $hiddens[] = static::hiddenField($request->csrfTokenName, $request->getCsrfToken(), array('id' => false)); + } + if ($hiddens !== array()) { + $form .= "\n" . static::tag('div', array('style' => 'display:none'), implode("\n", $hiddens)); + } + return $form; + } + + /** + * Generates a closing form tag. + * @return string the generated tag + * @see beginForm + */ + public static function endForm() + { + return ''; + } + + /** + * Generates a hyperlink tag. + * @param string $text link body. It will NOT be HTML-encoded. Therefore you can pass in HTML code such as an image tag. + * @param mixed $url a URL or an action route that can be used to create a URL. + * See {@link normalizeUrl} for more details about how to specify this parameter. + * @param array $htmlOptions additional HTML attributes. Besides normal HTML attributes, a few special + * attributes are also recognized (see {@link clientChange} and {@link tag} for more details.) + * @return string the generated hyperlink + * @see normalizeUrl + * @see clientChange + */ + public static function link($text, $url = '#', $htmlOptions = array()) + { + if ($url !== '') { + $htmlOptions['href'] = static::normalizeUrl($url); + } + static::clientChange('click', $htmlOptions); + return static::tag('a', $htmlOptions, $text); + } + + /** + * Generates a mailto link. + * @param string $text link body. It will NOT be HTML-encoded. Therefore you can pass in HTML code such as an image tag. + * @param string $email email address. If this is empty, the first parameter (link body) will be treated as the email address. + * @param array $htmlOptions additional HTML attributes. Besides normal HTML attributes, a few special + * attributes are also recognized (see {@link clientChange} and {@link tag} for more details.) + * @return string the generated mailto link + * @see clientChange + */ + public static function mailto($text, $email = '', $htmlOptions = array()) + { + if ($email === '') { + $email = $text; + } + return static::link($text, 'mailto:' . $email, $htmlOptions); + } + + /** + * Generates an image tag. + * @param string $src the image URL + * @param string $alt the alternative text display + * @param array $htmlOptions additional HTML attributes (see {@link tag}). + * @return string the generated image tag + */ + public static function image($src, $alt = '', $htmlOptions = array()) + { + $htmlOptions['src'] = $src; + $htmlOptions['alt'] = $alt; + return static::tag('img', $htmlOptions); + } + + /** + * Generates a button. + * @param string $label the button label + * @param array $htmlOptions additional HTML attributes. Besides normal HTML attributes, a few special + * attributes are also recognized (see {@link clientChange} and {@link tag} for more details.) + * @return string the generated button tag + * @see clientChange + */ + public static function button($name, $label = 'button', $htmlOptions = array()) + { + if (!isset($htmlOptions['name'])) { + if (!array_key_exists('name', $htmlOptions)) { + $htmlOptions['name'] = static::ID_PREFIX . static::$count++; + } + } + if (!isset($htmlOptions['type'])) { + $htmlOptions['type'] = 'button'; + } + if (!isset($htmlOptions['value'])) { + $htmlOptions['value'] = $label; + } + static::clientChange('click', $htmlOptions); + return static::tag('input', $htmlOptions); + } + + /** + * Generates a button using HTML button tag. + * This method is similar to {@link button} except that it generates a 'button' + * tag instead of 'input' tag. + * @param string $label the button label. Note that this value will be directly inserted in the button element + * without being HTML-encoded. + * @param array $htmlOptions additional HTML attributes. Besides normal HTML attributes, a few special + * attributes are also recognized (see {@link clientChange} and {@link tag} for more details.) + * @return string the generated button tag + * @see clientChange + */ + public static function htmlButton($label = 'button', $htmlOptions = array()) + { + if (!isset($htmlOptions['name'])) { + $htmlOptions['name'] = static::ID_PREFIX . static::$count++; + } + if (!isset($htmlOptions['type'])) { + $htmlOptions['type'] = 'button'; + } + static::clientChange('click', $htmlOptions); + return static::tag('button', $htmlOptions, $label); + } + + /** + * Generates a submit button. + * @param string $label the button label + * @param array $htmlOptions additional HTML attributes. Besides normal HTML attributes, a few special + * attributes are also recognized (see {@link clientChange} and {@link tag} for more details.) + * @return string the generated button tag + * @see clientChange + */ + public static function submitButton($label = 'submit', $htmlOptions = array()) + { + $htmlOptions['type'] = 'submit'; + return static::button($label, $htmlOptions); + } + + /** + * Generates a reset button. + * @param string $label the button label + * @param array $htmlOptions additional HTML attributes. Besides normal HTML attributes, a few special + * attributes are also recognized (see {@link clientChange} and {@link tag} for more details.) + * @return string the generated button tag + * @see clientChange + */ + public static function resetButton($label = 'reset', $htmlOptions = array()) + { + $htmlOptions['type'] = 'reset'; + return static::button($label, $htmlOptions); + } + + /** + * Generates an image submit button. + * @param string $src the image URL + * @param array $htmlOptions additional HTML attributes. Besides normal HTML attributes, a few special + * attributes are also recognized (see {@link clientChange} and {@link tag} for more details.) + * @return string the generated button tag + * @see clientChange + */ + public static function imageButton($src, $htmlOptions = array()) + { + $htmlOptions['src'] = $src; + $htmlOptions['type'] = 'image'; + return static::button('submit', $htmlOptions); + } + + /** + * Generates a link submit button. + * @param string $label the button label + * @param array $htmlOptions additional HTML attributes. Besides normal HTML attributes, a few special + * attributes are also recognized (see {@link clientChange} and {@link tag} for more details.) + * @return string the generated button tag + * @see clientChange + */ + public static function linkButton($label = 'submit', $htmlOptions = array()) + { + if (!isset($htmlOptions['submit'])) { + $htmlOptions['submit'] = isset($htmlOptions['href']) ? $htmlOptions['href'] : ''; + } + return static::link($label, '#', $htmlOptions); + } + + /** + * Generates a label tag. + * @param string $label label text. Note, you should HTML-encode the text if needed. + * @param string $for the ID of the HTML element that this label is associated with. + * If this is false, the 'for' attribute for the label tag will not be rendered. + * @param array $htmlOptions additional HTML attributes. + * The following HTML option is recognized: + *
      + *
    • required: if this is set and is true, the label will be styled + * with CSS class 'required' (customizable with CHtml::$requiredCss), + * and be decorated with {@link CHtml::beforeRequiredLabel} and + * {@link CHtml::afterRequiredLabel}.
    • + *
    + * @return string the generated label tag + */ + public static function label($label, $for, $htmlOptions = array()) + { + if ($for === false) { + unset($htmlOptions['for']); + } else { + $htmlOptions['for'] = $for; + } + if (isset($htmlOptions['required'])) { + if ($htmlOptions['required']) { + if (isset($htmlOptions['class'])) { + $htmlOptions['class'] .= ' ' . static::$requiredCss; + } else { + $htmlOptions['class'] = static::$requiredCss; + } + $label = static::$beforeRequiredLabel . $label . static::$afterRequiredLabel; + } + unset($htmlOptions['required']); + } + return static::tag('label', $htmlOptions, $label); + } + + /** + * Generates a text field input. + * @param string $name the input name + * @param string $value the input value + * @param array $htmlOptions additional HTML attributes. Besides normal HTML attributes, a few special + * attributes are also recognized (see {@link clientChange} and {@link tag} for more details.) + * @return string the generated input field + * @see clientChange + * @see inputField + */ + public static function textField($name, $value = '', $htmlOptions = array()) + { + static::clientChange('change', $htmlOptions); + return static::inputField('text', $name, $value, $htmlOptions); + } + + /** + * Generates a hidden input. + * @param string $name the input name + * @param string $value the input value + * @param array $htmlOptions additional HTML attributes (see {@link tag}). + * @return string the generated input field + * @see inputField + */ + public static function hiddenField($name, $value = '', $htmlOptions = array()) + { + return static::inputField('hidden', $name, $value, $htmlOptions); + } + + /** + * Generates a password field input. + * @param string $name the input name + * @param string $value the input value + * @param array $htmlOptions additional HTML attributes. Besides normal HTML attributes, a few special + * attributes are also recognized (see {@link clientChange} and {@link tag} for more details.) + * @return string the generated input field + * @see clientChange + * @see inputField + */ + public static function passwordField($name, $value = '', $htmlOptions = array()) + { + static::clientChange('change', $htmlOptions); + return static::inputField('password', $name, $value, $htmlOptions); + } + + /** + * Generates a file input. + * Note, you have to set the enclosing form's 'enctype' attribute to be 'multipart/form-data'. + * After the form is submitted, the uploaded file information can be obtained via $_FILES[$name] (see + * PHP documentation). + * @param string $name the input name + * @param string $value the input value + * @param array $htmlOptions additional HTML attributes (see {@link tag}). + * @return string the generated input field + * @see inputField + */ + public static function fileField($name, $value = '', $htmlOptions = array()) + { + return static::inputField('file', $name, $value, $htmlOptions); + } + + /** + * Generates a text area input. + * @param string $name the input name + * @param string $value the input value + * @param array $htmlOptions additional HTML attributes. Besides normal HTML attributes, a few special + * attributes are also recognized (see {@link clientChange} and {@link tag} for more details.) + * @return string the generated text area + * @see clientChange + * @see inputField + */ + public static function textArea($name, $value = '', $htmlOptions = array()) + { + $htmlOptions['name'] = $name; + if (!isset($htmlOptions['id'])) { + $htmlOptions['id'] = static::getIdByName($name); + } elseif ($htmlOptions['id'] === false) { + unset($htmlOptions['id']); + } + static::clientChange('change', $htmlOptions); + return static::tag('textarea', $htmlOptions, isset($htmlOptions['encode']) && !$htmlOptions['encode'] ? $value : static::encode($value)); + } + + /** + * Generates a radio button. + * @param string $name the input name + * @param boolean $checked whether the radio button is checked + * @param array $htmlOptions additional HTML attributes. Besides normal HTML attributes, a few special + * attributes are also recognized (see {@link clientChange} and {@link tag} for more details.) + * Since version 1.1.2, a special option named 'uncheckValue' is available that can be used to specify + * the value returned when the radio button is not checked. When set, a hidden field is rendered so that + * when the radio button is not checked, we can still obtain the posted uncheck value. + * If 'uncheckValue' is not set or set to NULL, the hidden field will not be rendered. + * @return string the generated radio button + * @see clientChange + * @see inputField + */ + public static function radioButton($name, $checked = false, $htmlOptions = array()) + { + if ($checked) { + $htmlOptions['checked'] = 'checked'; + } else { + unset($htmlOptions['checked']); + } + $value = isset($htmlOptions['value']) ? $htmlOptions['value'] : 1; + static::clientChange('click', $htmlOptions); + + if (array_key_exists('uncheckValue', $htmlOptions)) { + $uncheck = $htmlOptions['uncheckValue']; + unset($htmlOptions['uncheckValue']); + } else { + $uncheck = null; + } + + if ($uncheck !== null) { + // add a hidden field so that if the radio button is not selected, it still submits a value + if (isset($htmlOptions['id']) && $htmlOptions['id'] !== false) { + $uncheckOptions = array('id' => static::ID_PREFIX . $htmlOptions['id']); + } else { + $uncheckOptions = array('id' => false); + } + $hidden = static::hiddenField($name, $uncheck, $uncheckOptions); + } else { + $hidden = ''; + } + + // add a hidden field so that if the radio button is not selected, it still submits a value + return $hidden . static::inputField('radio', $name, $value, $htmlOptions); + } + + /** + * Generates a check box. + * @param string $name the input name + * @param boolean $checked whether the check box is checked + * @param array $htmlOptions additional HTML attributes. Besides normal HTML attributes, a few special + * attributes are also recognized (see {@link clientChange} and {@link tag} for more details.) + * Since version 1.1.2, a special option named 'uncheckValue' is available that can be used to specify + * the value returned when the checkbox is not checked. When set, a hidden field is rendered so that + * when the checkbox is not checked, we can still obtain the posted uncheck value. + * If 'uncheckValue' is not set or set to NULL, the hidden field will not be rendered. + * @return string the generated check box + * @see clientChange + * @see inputField + */ + public static function checkBox($name, $checked = false, $htmlOptions = array()) + { + if ($checked) { + $htmlOptions['checked'] = 'checked'; + } else { + unset($htmlOptions['checked']); + } + $value = isset($htmlOptions['value']) ? $htmlOptions['value'] : 1; + static::clientChange('click', $htmlOptions); + + if (array_key_exists('uncheckValue', $htmlOptions)) { + $uncheck = $htmlOptions['uncheckValue']; + unset($htmlOptions['uncheckValue']); + } else { + $uncheck = null; + } + + if ($uncheck !== null) { + // add a hidden field so that if the check box is not checked, it still submits a value + if (isset($htmlOptions['id']) && $htmlOptions['id'] !== false) { + $uncheckOptions = array('id' => static::ID_PREFIX . $htmlOptions['id']); + } else { + $uncheckOptions = array('id' => false); + } + $hidden = static::hiddenField($name, $uncheck, $uncheckOptions); + } else { + $hidden = ''; + } + + // add a hidden field so that if the check box is not checked, it still submits a value + return $hidden . static::inputField('checkbox', $name, $value, $htmlOptions); + } + + /** + * Generates a drop down list. + * @param string $name the input name + * @param string $select the selected value + * @param array $data data for generating the list options (value=>display). + * You may use {@link listData} to generate this data. + * Please refer to {@link listOptions} on how this data is used to generate the list options. + * Note, the values and labels will be automatically HTML-encoded by this method. + * @param array $htmlOptions additional HTML attributes. Besides normal HTML attributes, a few special + * attributes are recognized. See {@link clientChange} and {@link tag} for more details. + * In addition, the following options are also supported specifically for dropdown list: + *
      + *
    • encode: boolean, specifies whether to encode the values. Defaults to true.
    • + *
    • prompt: string, specifies the prompt text shown as the first list option. Its value is empty. Note, the prompt text will NOT be HTML-encoded.
    • + *
    • empty: string, specifies the text corresponding to empty selection. Its value is empty. + * The 'empty' option can also be an array of value-label pairs. + * Each pair will be used to render a list option at the beginning. Note, the text label will NOT be HTML-encoded.
    • + *
    • options: array, specifies additional attributes for each OPTION tag. + * The array keys must be the option values, and the array values are the extra + * OPTION tag attributes in the name-value pairs. For example, + *
      +	 *     array(
      +	 *         'value1'=>array('disabled'=>true, 'label'=>'value 1'),
      +	 *         'value2'=>array('label'=>'value 2'),
      +	 *     );
      +	 * 
      + *
    • + *
    + * Since 1.1.13, a special option named 'unselectValue' is available. It can be used to set the value + * that will be returned when no option is selected in multiple mode. When set, a hidden field is + * rendered so that if no option is selected in multiple mode, we can still obtain the posted + * unselect value. If 'unselectValue' is not set or set to NULL, the hidden field will not be rendered. + * @return string the generated drop down list + * @see clientChange + * @see inputField + * @see listData + */ + public static function dropDownList($name, $select, $data, $htmlOptions = array()) + { + $htmlOptions['name'] = $name; + + if (!isset($htmlOptions['id'])) { + $htmlOptions['id'] = static::getIdByName($name); + } elseif ($htmlOptions['id'] === false) { + unset($htmlOptions['id']); + } + + static::clientChange('change', $htmlOptions); + $options = "\n" . static::listOptions($select, $data, $htmlOptions); + $hidden = ''; + + if (isset($htmlOptions['multiple'])) { + if (substr($htmlOptions['name'], -2) !== '[]') { + $htmlOptions['name'] .= '[]'; + } + + if (isset($htmlOptions['unselectValue'])) { + $hiddenOptions = isset($htmlOptions['id']) ? array('id' => static::ID_PREFIX . $htmlOptions['id']) : array('id' => false); + $hidden = static::hiddenField(substr($htmlOptions['name'], 0, -2), $htmlOptions['unselectValue'], $hiddenOptions); + unset($htmlOptions['unselectValue']); + } + } + // add a hidden field so that if the option is not selected, it still submits a value + return $hidden . static::tag('select', $htmlOptions, $options); + } + + /** + * Generates a list box. + * @param string $name the input name + * @param mixed $select the selected value(s). This can be either a string for single selection or an array for multiple selections. + * @param array $data data for generating the list options (value=>display) + * You may use {@link listData} to generate this data. + * Please refer to {@link listOptions} on how this data is used to generate the list options. + * Note, the values and labels will be automatically HTML-encoded by this method. + * @param array $htmlOptions additional HTML attributes. Besides normal HTML attributes, a few special + * attributes are also recognized. See {@link clientChange} and {@link tag} for more details. + * In addition, the following options are also supported specifically for list box: + *
      + *
    • encode: boolean, specifies whether to encode the values. Defaults to true.
    • + *
    • prompt: string, specifies the prompt text shown as the first list option. Its value is empty. Note, the prompt text will NOT be HTML-encoded.
    • + *
    • empty: string, specifies the text corresponding to empty selection. Its value is empty. + * The 'empty' option can also be an array of value-label pairs. + * Each pair will be used to render a list option at the beginning. Note, the text label will NOT be HTML-encoded.
    • + *
    • options: array, specifies additional attributes for each OPTION tag. + * The array keys must be the option values, and the array values are the extra + * OPTION tag attributes in the name-value pairs. For example, + *
      +	 *     array(
      +	 *         'value1'=>array('disabled'=>true, 'label'=>'value 1'),
      +	 *         'value2'=>array('label'=>'value 2'),
      +	 *     );
      +	 * 
      + *
    • + *
    + * @return string the generated list box + * @see clientChange + * @see inputField + * @see listData + */ + public static function listBox($name, $select, $data, $htmlOptions = array()) + { + if (!isset($htmlOptions['size'])) { + $htmlOptions['size'] = 4; + } + if (isset($htmlOptions['multiple'])) { + if (substr($name, -2) !== '[]') { + $name .= '[]'; + } + } + return static::dropDownList($name, $select, $data, $htmlOptions); + } + + /** + * Generates a check box list. + * A check box list allows multiple selection, like {@link listBox}. + * As a result, the corresponding POST value is an array. + * @param string $name name of the check box list. You can use this name to retrieve + * the selected value(s) once the form is submitted. + * @param mixed $select selection of the check boxes. This can be either a string + * for single selection or an array for multiple selections. + * @param array $data value-label pairs used to generate the check box list. + * Note, the values will be automatically HTML-encoded, while the labels will not. + * @param array $htmlOptions additional HTML options. The options will be applied to + * each checkbox input. The following special options are recognized: + *
      + *
    • template: string, specifies how each checkbox is rendered. Defaults + * to "{input} {label}", where "{input}" will be replaced by the generated + * check box input tag while "{label}" be replaced by the corresponding check box label.
    • + *
    • separator: string, specifies the string that separates the generated check boxes.
    • + *
    • checkAll: string, specifies the label for the "check all" checkbox. + * If this option is specified, a 'check all' checkbox will be displayed. Clicking on + * this checkbox will cause all checkboxes checked or unchecked.
    • + *
    • checkAllLast: boolean, specifies whether the 'check all' checkbox should be + * displayed at the end of the checkbox list. If this option is not set (default) + * or is false, the 'check all' checkbox will be displayed at the beginning of + * the checkbox list.
    • + *
    • labelOptions: array, specifies the additional HTML attributes to be rendered + * for every label tag in the list.
    • + *
    • container: string, specifies the checkboxes enclosing tag. Defaults to 'span'. + * If the value is an empty string, no enclosing tag will be generated
    • + *
    • baseID: string, specifies the base ID prefix to be used for checkboxes in the list. + * This option is available since version 1.1.13.
    • + *
    + * @return string the generated check box list + */ + public static function checkBoxList($name, $select, $data, $htmlOptions = array()) + { + $template = isset($htmlOptions['template']) ? $htmlOptions['template'] : '{input} {label}'; + $separator = isset($htmlOptions['separator']) ? $htmlOptions['separator'] : "
    \n"; + $container = isset($htmlOptions['container']) ? $htmlOptions['container'] : 'span'; + unset($htmlOptions['template'], $htmlOptions['separator'], $htmlOptions['container']); + + if (substr($name, -2) !== '[]') { + $name .= '[]'; + } + + if (isset($htmlOptions['checkAll'])) { + $checkAllLabel = $htmlOptions['checkAll']; + $checkAllLast = isset($htmlOptions['checkAllLast']) && $htmlOptions['checkAllLast']; + } + unset($htmlOptions['checkAll'], $htmlOptions['checkAllLast']); + + $labelOptions = isset($htmlOptions['labelOptions']) ? $htmlOptions['labelOptions'] : array(); + unset($htmlOptions['labelOptions']); + + $items = array(); + $baseID = isset($htmlOptions['baseID']) ? $htmlOptions['baseID'] : static::getIdByName($name); + unset($htmlOptions['baseID']); + $id = 0; + $checkAll = true; + + foreach ($data as $value => $label) { + $checked = !is_array($select) && !strcmp($value, $select) || is_array($select) && in_array($value, $select); + $checkAll = $checkAll && $checked; + $htmlOptions['value'] = $value; + $htmlOptions['id'] = $baseID . '_' . $id++; + $option = static::checkBox($name, $checked, $htmlOptions); + $label = static::label($label, $htmlOptions['id'], $labelOptions); + $items[] = strtr($template, array('{input}' => $option, '{label}' => $label)); + } + + if (isset($checkAllLabel)) { + $htmlOptions['value'] = 1; + $htmlOptions['id'] = $id = $baseID . '_all'; + $option = static::checkBox($id, $checkAll, $htmlOptions); + $label = static::label($checkAllLabel, $id, $labelOptions); + $item = strtr($template, array('{input}' => $option, '{label}' => $label)); + if ($checkAllLast) { + $items[] = $item; + } else { + array_unshift($items, $item); + } + $name = strtr($name, array('[' => '\\[', ']' => '\\]')); + $js = <<getClientScript(); + $cs->registerCoreScript('jquery'); + $cs->registerScript($id, $js); + } + + if (empty($container)) { + return implode($separator, $items); + } else { + return static::tag($container, array('id' => $baseID), implode($separator, $items)); + } + } + + /** + * Generates a radio button list. + * A radio button list is like a {@link checkBoxList check box list}, except that + * it only allows single selection. + * @param string $name name of the radio button list. You can use this name to retrieve + * the selected value(s) once the form is submitted. + * @param string $select selection of the radio buttons. + * @param array $data value-label pairs used to generate the radio button list. + * Note, the values will be automatically HTML-encoded, while the labels will not. + * @param array $htmlOptions additional HTML options. The options will be applied to + * each radio button input. The following special options are recognized: + *
      + *
    • template: string, specifies how each radio button is rendered. Defaults + * to "{input} {label}", where "{input}" will be replaced by the generated + * radio button input tag while "{label}" will be replaced by the corresponding radio button label.
    • + *
    • separator: string, specifies the string that separates the generated radio buttons. Defaults to new line (
      ).
    • + *
    • labelOptions: array, specifies the additional HTML attributes to be rendered + * for every label tag in the list.
    • + *
    • container: string, specifies the radio buttons enclosing tag. Defaults to 'span'. + * If the value is an empty string, no enclosing tag will be generated
    • + *
    • baseID: string, specifies the base ID prefix to be used for radio buttons in the list. + * This option is available since version 1.1.13.
    • + *
    + * @return string the generated radio button list + */ + public static function radioButtonList($name, $select, $data, $htmlOptions = array()) + { + $template = isset($htmlOptions['template']) ? $htmlOptions['template'] : '{input} {label}'; + $separator = isset($htmlOptions['separator']) ? $htmlOptions['separator'] : "
    \n"; + $container = isset($htmlOptions['container']) ? $htmlOptions['container'] : 'span'; + unset($htmlOptions['template'], $htmlOptions['separator'], $htmlOptions['container']); + + $labelOptions = isset($htmlOptions['labelOptions']) ? $htmlOptions['labelOptions'] : array(); + unset($htmlOptions['labelOptions']); + + $items = array(); + $baseID = isset($htmlOptions['baseID']) ? $htmlOptions['baseID'] : static::getIdByName($name); + unset($htmlOptions['baseID']); + $id = 0; + foreach ($data as $value => $label) { + $checked = !strcmp($value, $select); + $htmlOptions['value'] = $value; + $htmlOptions['id'] = $baseID . '_' . $id++; + $option = static::radioButton($name, $checked, $htmlOptions); + $label = static::label($label, $htmlOptions['id'], $labelOptions); + $items[] = strtr($template, array('{input}' => $option, '{label}' => $label)); + } + if (empty($container)) { + return implode($separator, $items); + } else { + return static::tag($container, array('id' => $baseID), implode($separator, $items)); + } + } + + /** + * Normalizes the input parameter to be a valid URL. + * + * If the input parameter is an empty string, the currently requested URL will be returned. + * + * If the input parameter is a non-empty string, it is treated as a valid URL and will + * be returned without any change. + * + * If the input parameter is an array, it is treated as a controller route and a list of + * GET parameters, and the {@link CController::createUrl} method will be invoked to + * create a URL. In this case, the first array element refers to the controller route, + * and the rest key-value pairs refer to the additional GET parameters for the URL. + * For example, array('post/list', 'page'=>3) may be used to generate the URL + * /index.php?r=post/list&page=3. + * + * @param mixed $url the parameter to be used to generate a valid URL + * @return string the normalized URL + */ + public static function normalizeUrl($url) + { + if (is_array($url)) { + if (isset($url[0])) { + if (($c = Yii::app()->getController()) !== null) { + $url = $c->createUrl($url[0], array_splice($url, 1)); + } else { + $url = Yii::app()->createUrl($url[0], array_splice($url, 1)); + } + } else { + $url = ''; + } + } + return $url === '' ? Yii::app()->getRequest()->getUrl() : $url; + } + + /** + * Generates an input HTML tag. + * This method generates an input HTML tag based on the given input name and value. + * @param string $type the input type (e.g. 'text', 'radio') + * @param string $name the input name + * @param string $value the input value + * @param array $htmlOptions additional HTML attributes for the HTML tag (see {@link tag}). + * @return string the generated input tag + */ + protected static function inputField($type, $name, $value, $htmlOptions) + { + $htmlOptions['type'] = $type; + $htmlOptions['value'] = $value; + $htmlOptions['name'] = $name; + if (!isset($htmlOptions['id'])) { + $htmlOptions['id'] = static::getIdByName($name); + } elseif ($htmlOptions['id'] === false) { + unset($htmlOptions['id']); + } + return static::tag('input', $htmlOptions); + } + + /** + * Generates the list options. + * @param mixed $selection the selected value(s). This can be either a string for single selection or an array for multiple selections. + * @param array $listData the option data (see {@link listData}) + * @param array $htmlOptions additional HTML attributes. The following two special attributes are recognized: + *
      + *
    • encode: boolean, specifies whether to encode the values. Defaults to true.
    • + *
    • prompt: string, specifies the prompt text shown as the first list option. Its value is empty. Note, the prompt text will NOT be HTML-encoded.
    • + *
    • empty: string, specifies the text corresponding to empty selection. Its value is empty. + * The 'empty' option can also be an array of value-label pairs. + * Each pair will be used to render a list option at the beginning. Note, the text label will NOT be HTML-encoded.
    • + *
    • options: array, specifies additional attributes for each OPTION tag. + * The array keys must be the option values, and the array values are the extra + * OPTION tag attributes in the name-value pairs. For example, + *
      +	 *     array(
      +	 *         'value1'=>array('disabled'=>true, 'label'=>'value 1'),
      +	 *         'value2'=>array('label'=>'value 2'),
      +	 *     );
      +	 * 
      + *
    • + *
    • key: string, specifies the name of key attribute of the selection object(s). + * This is used when the selection is represented in terms of objects. In this case, + * the property named by the key option of the objects will be treated as the actual selection value. + * This option defaults to 'primaryKey', meaning using the 'primaryKey' property value of the objects in the selection. + * This option has been available since version 1.1.3.
    • + *
    + * @return string the generated list options + */ + public static function listOptions($selection, $listData, &$htmlOptions) + { + $raw = isset($htmlOptions['encode']) && !$htmlOptions['encode']; + $content = ''; + if (isset($htmlOptions['prompt'])) { + $content .= '\n"; + unset($htmlOptions['prompt']); + } + if (isset($htmlOptions['empty'])) { + if (!is_array($htmlOptions['empty'])) { + $htmlOptions['empty'] = array('' => $htmlOptions['empty']); + } + foreach ($htmlOptions['empty'] as $value => $label) { + $content .= '\n"; + } + unset($htmlOptions['empty']); + } + + if (isset($htmlOptions['options'])) { + $options = $htmlOptions['options']; + unset($htmlOptions['options']); + } else { + $options = array(); + } + + $key = isset($htmlOptions['key']) ? $htmlOptions['key'] : 'primaryKey'; + if (is_array($selection)) { + foreach ($selection as $i => $item) { + if (is_object($item)) { + $selection[$i] = $item->$key; + } + } + } elseif (is_object($selection)) { + $selection = $selection->$key; + } + + foreach ($listData as $key => $value) { + if (is_array($value)) { + $content .= '\n"; + $dummy = array('options' => $options); + if (isset($htmlOptions['encode'])) { + $dummy['encode'] = $htmlOptions['encode']; + } + $content .= static::listOptions($selection, $value, $dummy); + $content .= '' . "\n"; + } else { + $attributes = array('value' => (string)$key, 'encode' => !$raw); + if (!is_array($selection) && !strcmp($key, $selection) || is_array($selection) && in_array($key, $selection)) { + $attributes['selected'] = 'selected'; + } + if (isset($options[$key])) { + $attributes = array_merge($attributes, $options[$key]); + } + $content .= static::tag('option', $attributes, $raw ? (string)$value : static::encode((string)$value)) . "\n"; + } + } + + unset($htmlOptions['key']); + + return $content; + } + + /** + * Renders the HTML tag attributes. + * Since version 1.1.5, attributes whose value is null will not be rendered. + * Special attributes, such as 'checked', 'disabled', 'readonly', will be rendered + * properly based on their corresponding boolean value. + * @param array $attributes attributes to be rendered + * @return string the rendering result + */ + public static function renderAttributes($attributes) + { + static $specialAttributes = array( + 'async' => 1, + 'autofocus' => 1, + 'autoplay' => 1, + 'checked' => 1, + 'controls' => 1, + 'declare' => 1, + 'default' => 1, + 'defer' => 1, + 'disabled' => 1, + 'formnovalidate' => 1, + 'hidden' => 1, + 'ismap' => 1, + 'loop' => 1, + 'multiple' => 1, + 'muted' => 1, + 'nohref' => 1, + 'noresize' => 1, + 'novalidate' => 1, + 'open' => 1, + 'readonly' => 1, + 'required' => 1, + 'reversed' => 1, + 'scoped' => 1, + 'seamless' => 1, + 'selected' => 1, + 'typemustmatch' => 1, + ); + + if ($attributes === array()) { + return ''; + } + + $html = ''; + if (isset($attributes['encode'])) { + $raw = !$attributes['encode']; + unset($attributes['encode']); + } else { + $raw = false; + } + + foreach ($attributes as $name => $value) { + if (isset($specialAttributes[$name])) { + if ($value) { + $html .= ' ' . $name; + if (static::$renderSpecialAttributesValue) { + $html .= '="' . $name . '"'; + } + } + } elseif ($value !== null) { + $html .= ' ' . $name . '="' . ($raw ? $value : static::encode($value)) . '"'; + } + } + return $html; + } } From d2fcc69b34e27b258d8ac3a516106e4f5c359e63 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Fri, 8 Mar 2013 17:32:19 -0500 Subject: [PATCH 077/117] Html WIP --- framework/util/ArrayHelper.php | 32 ++++++ framework/util/Html.php | 215 ++++++++++++++++++----------------------- 2 files changed, 125 insertions(+), 122 deletions(-) diff --git a/framework/util/ArrayHelper.php b/framework/util/ArrayHelper.php index 8bea4b6..fdcd691 100644 --- a/framework/util/ArrayHelper.php +++ b/framework/util/ArrayHelper.php @@ -7,6 +7,7 @@ namespace yii\util; +use Yii; use yii\base\InvalidParamException; /** @@ -279,4 +280,35 @@ class ArrayHelper $args[] = &$array; call_user_func_array('array_multisort', $args); } + + + /** + * Encodes special characters in an array of strings into HTML entities. + * Both the array keys and values will be encoded if needed. + * If a value is an array, this method will also encode it recursively. + * @param array $data data to be encoded + * @param string $charset the charset that the data is using. If not set, + * [[\yii\base\Application::charset]] will be used. + * @return array the encoded data + * @see http://www.php.net/manual/en/function.htmlspecialchars.php + */ + public static function htmlEncode($data, $charset = null) + { + if ($charset === null) { + $charset = Yii::$app->charset; + } + $d = array(); + foreach ($data as $key => $value) { + if (is_string($key)) { + $key = htmlspecialchars($key, ENT_QUOTES, $charset); + } + if (is_string($value)) { + $value = htmlspecialchars($value, ENT_QUOTES, $charset); + } elseif (is_array($value)) { + $value = static::htmlEncode($value); + } + $d[$key] = $value; + } + return $d; + } } \ No newline at end of file diff --git a/framework/util/Html.php b/framework/util/Html.php index 11b4da6..3a3ee9c 100644 --- a/framework/util/Html.php +++ b/framework/util/Html.php @@ -16,183 +16,154 @@ use Yii; class Html { /** - * @var integer the counter for generating automatic input field names. + * @var boolean whether to close void (empty) elements. Defaults to true. + * @see voidElements */ - public static $count = 0; + public static $closeVoidElements = true; /** - * @var boolean whether to close single tags. Defaults to true. Can be set to false for HTML5. + * @var array list of void elements (element name => 1) + * @see http://www.w3.org/TR/html-markup/syntax.html#void-element */ - public static $closeSingleTags = true; + public static $voidElements = array( + 'area' => 1, + 'base' => 1, + 'br' => 1, + 'col' => 1, + 'command' => 1, + 'embed' => 1, + 'hr' => 1, + 'img' => 1, + 'input' => 1, + 'keygen' => 1, + 'link' => 1, + 'meta' => 1, + 'param' => 1, + 'source' => 1, + 'track' => 1, + 'wbr' => 1, + ); /** * @var boolean whether to render special attributes value. Defaults to true. Can be set to false for HTML5. */ public static $renderSpecialAttributesValue = true; + /** * Encodes special characters into HTML entities. - * The {@link CApplication::charset application charset} will be used for encoding. - * @param string $text data to be encoded - * @return string the encoded data + * The [[yii\base\Application::charset|application charset]] will be used for encoding. + * @param string $content the content to be encoded + * @return string the encoded content + * @see decode * @see http://www.php.net/manual/en/function.htmlspecialchars.php */ - public static function encode($text) + public static function encode($content) { - return htmlspecialchars($text, ENT_QUOTES, Yii::$app->charset); + return htmlspecialchars($content, ENT_QUOTES, Yii::$app->charset); } /** * Decodes special HTML entities back to the corresponding characters. - * This is the opposite of {@link encode()}. - * @param string $text data to be decoded - * @return string the decoded data + * This is the opposite of [[encode()]]. + * @param string $content the content to be decoded + * @return string the decoded content + * @see encode * @see http://www.php.net/manual/en/function.htmlspecialchars-decode.php */ - public static function decode($text) - { - return htmlspecialchars_decode($text, ENT_QUOTES); - } - - /** - * Encodes special characters in an array of strings into HTML entities. - * Both the array keys and values will be encoded if needed. - * If a value is an array, this method will also encode it recursively. - * The {@link CApplication::charset application charset} will be used for encoding. - * @param array $data data to be encoded - * @return array the encoded data - * @see http://www.php.net/manual/en/function.htmlspecialchars.php - */ - public static function encodeArray($data) + public static function decode($content) { - $d = array(); - foreach ($data as $key => $value) { - if (is_string($key)) { - $key = htmlspecialchars($key, ENT_QUOTES, Yii::$app->charset); - } - if (is_string($value)) { - $value = htmlspecialchars($value, ENT_QUOTES, Yii::$app->charset); - } elseif (is_array($value)) { - $value = static::encodeArray($value); - } - $d[$key] = $value; - } - return $d; + return htmlspecialchars_decode($content, ENT_QUOTES); } /** - * Generates an HTML element. - * @param string $tag the tag name - * @param array $htmlOptions the element attributes. The values will be HTML-encoded using {@link encode()}. - * If an 'encode' attribute is given and its value is false, - * the rest of the attribute values will NOT be HTML-encoded. - * Since version 1.1.5, attributes whose value is null will not be rendered. - * @param mixed $content the content to be enclosed between open and close element tags. It will not be HTML-encoded. - * If false, it means there is no body content. - * @param boolean $closeTag whether to generate the close tag. - * @return string the generated HTML element tag + * Generates a complete HTML tag. + * @param string $name the tag name + * @param string $content the content to be enclosed between the start and end tags. It will not be HTML-encoded. + * @param array $attributes the element attributes. The values will be HTML-encoded using [[encode()]]. + * Attributes whose value is null will be ignored and not put in the tag returned. + * @return string the generated HTML tag + * @see beginTag + * @see endTag */ - public static function tag($tag, $htmlOptions = array(), $content = false, $closeTag = true) + public static function tag($name, $content = '', $attributes = array()) { - $html = '<' . $tag . static::renderAttributes($htmlOptions); - if ($content === false) { - return $closeTag && static::$closeSingleTags ? $html . ' />' : $html . '>'; + $html = '<' . $name . static::renderAttributes($attributes); + if (isset(static::$voidElements[strtolower($name)])) { + return $html . (static::$closeVoidElements ? ' />' : '>'); } else { - return $closeTag ? $html . '>' . $content . '' : $html . '>' . $content; + return $html . ">$content"; } } /** - * Generates an open HTML element. - * @param string $tag the tag name - * @param array $htmlOptions the element attributes. The values will be HTML-encoded using {@link encode()}. - * If an 'encode' attribute is given and its value is false, - * the rest of the attribute values will NOT be HTML-encoded. - * Since version 1.1.5, attributes whose value is null will not be rendered. - * @return string the generated HTML element tag + * Generates a start tag. + * @param string $name the tag name + * @param array $attributes the element attributes. The values will be HTML-encoded using [[encode()]]. + * Attributes whose value is null will be ignored and not put in the tag returned. + * @return string the generated start tag + * @see endTag + * @see tag */ - public static function openTag($tag, $htmlOptions = array()) + public static function beginTag($name, $attributes = array()) { - return '<' . $tag . static::renderAttributes($htmlOptions) . '>'; + return '<' . $name . static::renderAttributes($attributes) . '>'; } /** - * Generates a close HTML element. - * @param string $tag the tag name - * @return string the generated HTML element tag + * Generates an end tag. + * @param string $name the tag name + * @return string the generated end tag + * @see beginTag + * @see tag */ - public static function closeTag($tag) + public static function endTag($name) { - return ''; + return !static::$closeVoidElements && isset(static::$voidElements[strtolower($name)]) ? '' : ""; } /** - * Encloses the given string within a CDATA tag. - * @param string $text the string to be enclosed + * Encloses the given content within a CDATA tag. + * @param string $content the content to be enclosed within the CDATA tag * @return string the CDATA tag with the enclosed content. */ - public static function cdata($text) + public static function cdata($content) { - return ''; + return ''; } /** - * Generates a meta tag that can be inserted in the head section of HTML page. - * @param string $content content attribute of the meta tag - * @param string $name name attribute of the meta tag. If null, the attribute will not be generated - * @param string $httpEquiv http-equiv attribute of the meta tag. If null, the attribute will not be generated - * @param array $options other options in name-value pairs (e.g. 'scheme', 'lang') - * @return string the generated meta tag + * Generates a style tag. + * @param string $content the style content + * @param array $attributes the attributes of the style tag. The values will be HTML-encoded using [[encode()]]. + * Attributes whose value is null will be ignored and not put in the tag returned. + * If the attributes does not contain "type", a default one with value "text/css" will be used. + * @return string the generated style tag */ - public static function metaTag($content, $name = null, $httpEquiv = null, $options = array()) + public static function style($content, $attributes = array()) { - if ($name !== null) { - $options['name'] = $name; + if (!isset($attributes['type'])) { + $attributes['type'] = 'text/css'; } - if ($httpEquiv !== null) { - $options['http-equiv'] = $httpEquiv; - } - $options['content'] = $content; - return static::tag('meta', $options); + return static::beginTag('style', $attributes) + . "\n/**/\n" + . static::endTag('style'); } /** - * Generates a link tag that can be inserted in the head section of HTML page. - * Do not confuse this method with {@link link()}. The latter generates a hyperlink. - * @param string $relation rel attribute of the link tag. If null, the attribute will not be generated. - * @param string $type type attribute of the link tag. If null, the attribute will not be generated. - * @param string $href href attribute of the link tag. If null, the attribute will not be generated. - * @param string $media media attribute of the link tag. If null, the attribute will not be generated. - * @param array $options other options in name-value pairs - * @return string the generated link tag - */ - public static function linkTag($relation = null, $type = null, $href = null, $media = null, $options = array()) - { - if ($relation !== null) { - $options['rel'] = $relation; - } - if ($type !== null) { - $options['type'] = $type; - } - if ($href !== null) { - $options['href'] = $href; - } - if ($media !== null) { - $options['media'] = $media; - } - return static::tag('link', $options); - } - - /** - * Encloses the given CSS content with a CSS tag. - * @param string $text the CSS content - * @param string $media the media that this CSS should apply to. - * @return string the CSS properly enclosed + * Generates a script tag. + * @param string $content the script content + * @param array $attributes the attributes of the script tag. The values will be HTML-encoded using [[encode()]]. + * Attributes whose value is null will be ignored and not put in the tag returned. + * If the attributes does not contain "type", a default one with value "text/javascript" will be used. + * @return string the generated script tag */ - public static function css($text, $media = '') + public static function script($content, $attributes = array()) { - if ($media !== '') { - $media = ' media="' . $media . '"'; + if (!isset($attributes['type'])) { + $attributes['type'] = 'text/javascript'; } - return ""; + return static::beginTag('script', $attributes) + . "\n/**/\n" + . static::endTag('script'); } /** From 30d70be071e942b9eb70748b373f0a98f285ff8d Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Sun, 10 Mar 2013 22:00:26 -0400 Subject: [PATCH 078/117] Finished Html helper. --- framework/base/Application.php | 13 +- framework/db/ActiveRelation.php | 16 +- framework/util/ArrayHelper.php | 40 +- framework/util/Html.php | 1308 ++++++++++++++++----------------------- framework/web/Application.php | 47 +- framework/web/Controller.php | 4 + framework/web/Request.php | 39 +- todo.md | 1 + 8 files changed, 663 insertions(+), 805 deletions(-) diff --git a/framework/base/Application.php b/framework/base/Application.php index 3c1a1fb..a60bf90 100644 --- a/framework/base/Application.php +++ b/framework/base/Application.php @@ -382,6 +382,15 @@ class Application extends Module } /** + * Returns the URL manager for this application. + * @return \yii\web\UrlManager the URL manager for this application. + */ + public function getUrlManager() + { + return $this->getComponent('urlManager'); + } + + /** * Returns the internationalization (i18n) component * @return \yii\i18n\I18N the internationalization component */ @@ -411,8 +420,8 @@ class Application extends Module 'i18n' => array( 'class' => 'yii\i18n\I18N', ), - 'securityManager' => array( - 'class' => 'yii\base\SecurityManager', + 'urlManager' => array( + 'class' => 'yii\web\UrlManager', ), )); } diff --git a/framework/db/ActiveRelation.php b/framework/db/ActiveRelation.php index e1793bd..f1b198b 100644 --- a/framework/db/ActiveRelation.php +++ b/framework/db/ActiveRelation.php @@ -55,16 +55,16 @@ class ActiveRelation extends ActiveQuery /** * Specifies the relation associated with the pivot table. * @param string $relationName the relation name. This refers to a relation declared in [[primaryModel]]. - * @param callback $callback a PHP callback for customizing the relation associated with the pivot table. + * @param callable $callable a PHP callback for customizing the relation associated with the pivot table. * Its signature should be `function($query)`, where `$query` is the query to be customized. * @return ActiveRelation the relation object itself. */ - public function via($relationName, $callback = null) + public function via($relationName, $callable = null) { $relation = $this->primaryModel->getRelation($relationName); $this->via = array($relationName, $relation); - if ($callback !== null) { - call_user_func($callback, $relation); + if ($callable !== null) { + call_user_func($callable, $relation); } return $this; } @@ -75,11 +75,11 @@ class ActiveRelation extends ActiveQuery * @param array $link the link between the pivot table and the table associated with [[primaryModel]]. * The keys of the array represent the columns in the pivot table, and the values represent the columns * in the [[primaryModel]] table. - * @param callback $callback a PHP callback for customizing the relation associated with the pivot table. + * @param callable $callable a PHP callback for customizing the relation associated with the pivot table. * Its signature should be `function($query)`, where `$query` is the query to be customized. * @return ActiveRelation */ - public function viaTable($tableName, $link, $callback = null) + public function viaTable($tableName, $link, $callable = null) { $relation = new ActiveRelation(array( 'modelClass' => get_class($this->primaryModel), @@ -89,8 +89,8 @@ class ActiveRelation extends ActiveQuery 'asArray' => true, )); $this->via = $relation; - if ($callback !== null) { - call_user_func($callback, $relation); + if ($callable !== null) { + call_user_func($callable, $relation); } return $this; } diff --git a/framework/util/ArrayHelper.php b/framework/util/ArrayHelper.php index fdcd691..ebf4b23 100644 --- a/framework/util/ArrayHelper.php +++ b/framework/util/ArrayHelper.php @@ -281,33 +281,59 @@ class ArrayHelper call_user_func_array('array_multisort', $args); } - /** * Encodes special characters in an array of strings into HTML entities. - * Both the array keys and values will be encoded if needed. + * Both the array keys and values will be encoded. * If a value is an array, this method will also encode it recursively. * @param array $data data to be encoded + * @param boolean $valuesOnly whether to encode array values only. If false, + * both the array keys and array values will be encoded. * @param string $charset the charset that the data is using. If not set, * [[\yii\base\Application::charset]] will be used. * @return array the encoded data * @see http://www.php.net/manual/en/function.htmlspecialchars.php */ - public static function htmlEncode($data, $charset = null) + public static function htmlEncode($data, $valuesOnly = false, $charset = null) { if ($charset === null) { $charset = Yii::$app->charset; } $d = array(); foreach ($data as $key => $value) { - if (is_string($key)) { + if (!$valuesOnly && is_string($key)) { $key = htmlspecialchars($key, ENT_QUOTES, $charset); } if (is_string($value)) { - $value = htmlspecialchars($value, ENT_QUOTES, $charset); + $d[$key] = htmlspecialchars($value, ENT_QUOTES, $charset); + } elseif (is_array($value)) { + $d[$key] = static::htmlEncode($value, $charset); + } + } + return $d; + } + + /** + * Decodes HTML entities into the corresponding characters in an array of strings. + * Both the array keys and values will be decoded. + * If a value is an array, this method will also decode it recursively. + * @param array $data data to be decoded + * @param boolean $valuesOnly whether to decode array values only. If false, + * both the array keys and array values will be decoded. + * @return array the decoded data + * @see http://www.php.net/manual/en/function.htmlspecialchars-decode.php + */ + public static function htmlDecode($data, $valuesOnly = false) + { + $d = array(); + foreach ($data as $key => $value) { + if (!$valuesOnly && is_string($key)) { + $key = htmlspecialchars_decode($key, ENT_QUOTES); + } + if (is_string($value)) { + $d[$key] = htmlspecialchars_decode($value, ENT_QUOTES); } elseif (is_array($value)) { - $value = static::htmlEncode($value); + $d[$key] = static::htmlDecode($value); } - $d[$key] = $value; } return $d; } diff --git a/framework/util/Html.php b/framework/util/Html.php index 3a3ee9c..774a733 100644 --- a/framework/util/Html.php +++ b/framework/util/Html.php @@ -8,8 +8,11 @@ namespace yii\util; use Yii; +use yii\base\InvalidParamException; /** + * Html provides a set of static methods for generating commonly used HTML tags. + * * @author Qiang Xue * @since 2.0 */ @@ -22,6 +25,7 @@ class Html public static $closeVoidElements = true; /** * @var array list of void elements (element name => 1) + * @see closeVoidElements * @see http://www.w3.org/TR/html-markup/syntax.html#void-element */ public static $voidElements = array( @@ -43,9 +47,78 @@ class Html 'wbr' => 1, ); /** - * @var boolean whether to render special attributes value. Defaults to true. Can be set to false for HTML5. - */ - public static $renderSpecialAttributesValue = true; + * @var boolean whether to show the values of boolean attributes in element tags. + * If false, only the attribute names will be generated. + * @see booleanAttributes + */ + public static $showBooleanAttributeValues = true; + /** + * @var array list of boolean attributes. The presence of a boolean attribute on + * an element represents the true value, and the absence of the attribute represents the false value. + * @see showBooleanAttributeValues + * @see http://www.w3.org/TR/html5/infrastructure.html#boolean-attributes + */ + public static $booleanAttributes = array( + 'async' => 1, + 'autofocus' => 1, + 'autoplay' => 1, + 'checked' => 1, + 'controls' => 1, + 'declare' => 1, + 'default' => 1, + 'defer' => 1, + 'disabled' => 1, + 'formnovalidate' => 1, + 'hidden' => 1, + 'ismap' => 1, + 'loop' => 1, + 'multiple' => 1, + 'muted' => 1, + 'nohref' => 1, + 'noresize' => 1, + 'novalidate' => 1, + 'open' => 1, + 'readonly' => 1, + 'required' => 1, + 'reversed' => 1, + 'scoped' => 1, + 'seamless' => 1, + 'selected' => 1, + 'typemustmatch' => 1, + ); + /** + * @var array the preferred order of attributes in a tag. This mainly affects the order of the attributes + * that are rendered by [[renderAttributes()]]. + */ + public static $attributeOrder = array( + 'type', + 'id', + 'class', + 'name', + 'value', + + 'href', + 'src', + 'action', + 'method', + + 'selected', + 'checked', + 'readonly', + 'disabled', + + 'size', + 'maxlength', + 'width', + 'height', + 'rows', + 'cols', + + 'alt', + 'title', + 'rel', + 'media', + ); /** @@ -78,6 +151,7 @@ class Html * Generates a complete HTML tag. * @param string $name the tag name * @param string $content the content to be enclosed between the start and end tags. It will not be HTML-encoded. + * If this is coming from end users, you should consider [[encode()]] it to prevent XSS attacks. * @param array $attributes the element attributes. The values will be HTML-encoded using [[encode()]]. * Attributes whose value is null will be ignored and not put in the tag returned. * @return string the generated HTML tag @@ -117,7 +191,7 @@ class Html */ public static function endTag($name) { - return !static::$closeVoidElements && isset(static::$voidElements[strtolower($name)]) ? '' : ""; + return ""; } /** @@ -143,9 +217,7 @@ class Html if (!isset($attributes['type'])) { $attributes['type'] = 'text/css'; } - return static::beginTag('style', $attributes) - . "\n/**/\n" - . static::endTag('style'); + return static::tag('style', "\n/**/\n", $attributes); } /** @@ -161,110 +233,77 @@ class Html if (!isset($attributes['type'])) { $attributes['type'] = 'text/javascript'; } - return static::beginTag('script', $attributes) - . "\n/**/\n" - . static::endTag('script'); + return static::tag('script', "\n/**/\n", $attributes); } /** - * Registers a 'refresh' meta tag. - * This method can be invoked anywhere in a view. It will register a 'refresh' - * meta tag with {@link CClientScript} so that the page can be refreshed in - * the specified seconds. - * @param integer $seconds the number of seconds to wait before refreshing the page - * @param string $url the URL to which the page should be redirected to. If empty, it means the current page. - * @since 1.1.1 - */ - public static function refresh($seconds, $url = '') - { - $content = "$seconds"; - if ($url !== '') { - $content .= ';' . static::normalizeUrl($url); - } - Yii::app()->clientScript->registerMetaTag($content, null, 'refresh'); - } - - /** - * Links to the specified CSS file. - * @param string $url the CSS URL - * @param string $media the media that this CSS should apply to. - * @return string the CSS link. + * Generates a link tag that refers to an external CSS file. + * @param array|string $url the URL of the external CSS file. This parameter will be processed by [[url()]]. + * @param array $attributes the attributes of the link tag. The values will be HTML-encoded using [[encode()]]. + * Attributes whose value is null will be ignored and not put in the tag returned. + * @return string the generated link tag + * @see url */ - public static function cssFile($url, $media = '') + public static function cssFile($url, $attributes = array()) { - return CHtml::linkTag('stylesheet', 'text/css', $url, $media !== '' ? $media : null); + $attributes['rel'] = 'stylesheet'; + $attributes['type'] = 'text/css'; + $attributes['href'] = static::url($url); + return static::tag('link', '', $attributes); } /** - * Encloses the given JavaScript within a script tag. - * @param string $text the JavaScript to be enclosed - * @return string the enclosed JavaScript + * Generates a script tag that refers to an external JavaScript file. + * @param string $url the URL of the external JavaScript file. This parameter will be processed by [[url()]]. + * @param array $attributes the attributes of the script tag. The values will be HTML-encoded using [[encode()]]. + * Attributes whose value is null will be ignored and not put in the tag returned. + * @return string the generated script tag + * @see url */ - public static function script($text) + public static function jsFile($url, $attributes = array()) { - return ""; + $attributes['type'] = 'text/javascript'; + $attributes['src'] = static::url($url); + return static::tag('script', '', $attributes); } /** - * Includes a JavaScript file. - * @param string $url URL for the JavaScript file - * @return string the JavaScript file tag + * Generates a form start tag. + * @param array|string $action the form action URL. This parameter will be processed by [[url()]]. + * @param string $method form method, either "post" or "get" (case-insensitive) + * @param array $attributes the attributes of the form tag. The values will be HTML-encoded using [[encode()]]. + * Attributes whose value is null will be ignored and not put in the tag returned. + * @return string the generated form start tag. + * @see endForm */ - public static function scriptFile($url) + public static function beginForm($action = '', $method = 'post', $attributes = array()) { - return ''; - } + $attributes['action'] = $url = static::url($action); + $attributes['method'] = $method; - /** - * Generates an opening form tag. - * This is a shortcut to {@link beginForm}. - * @param mixed $action the form action URL (see {@link normalizeUrl} for details about this parameter.) - * @param string $method form method (e.g. post, get) - * @param array $htmlOptions additional HTML attributes (see {@link tag}). - * @return string the generated form tag. - */ - public static function form($action = '', $method = 'post', $htmlOptions = array()) - { - return static::beginForm($action, $method, $htmlOptions); - } + $form = static::beginTag('form', $attributes); - /** - * Generates an opening form tag. - * Note, only the open tag is generated. A close tag should be placed manually - * at the end of the form. - * @param mixed $action the form action URL (see {@link normalizeUrl} for details about this parameter.) - * @param string $method form method (e.g. post, get) - * @param array $htmlOptions additional HTML attributes (see {@link tag}). - * @return string the generated form tag. - * @see endForm - */ - public static function beginForm($action = '', $method = 'post', $htmlOptions = array()) - { - $htmlOptions['action'] = $url = static::normalizeUrl($action); - $htmlOptions['method'] = $method; - $form = static::tag('form', $htmlOptions, false, false); + // query parameters in the action are ignored for GET method + // we use hidden fields to add them back $hiddens = array(); if (!strcasecmp($method, 'get') && ($pos = strpos($url, '?')) !== false) { foreach (explode('&', substr($url, $pos + 1)) as $pair) { if (($pos = strpos($pair, '=')) !== false) { - $hiddens[] = static::hiddenField(urldecode(substr($pair, 0, $pos)), urldecode(substr($pair, $pos + 1)), array('id' => false)); + $hiddens[] = static::hiddenInput(urldecode(substr($pair, 0, $pos)), urldecode(substr($pair, $pos + 1))); } else { - $hiddens[] = static::hiddenField(urldecode($pair), '', array('id' => false)); + $hiddens[] = static::hiddenInput(urldecode($pair), ''); } } } - $request = Yii::app()->request; - if ($request->enableCsrfValidation && !strcasecmp($method, 'post')) { - $hiddens[] = static::hiddenField($request->csrfTokenName, $request->getCsrfToken(), array('id' => false)); - } if ($hiddens !== array()) { - $form .= "\n" . static::tag('div', array('style' => 'display:none'), implode("\n", $hiddens)); + $form .= "\n" . implode("\n", $hiddens); } + return $form; } /** - * Generates a closing form tag. + * Generates a form end tag. * @return string the generated tag * @see beginForm */ @@ -275,854 +314,599 @@ class Html /** * Generates a hyperlink tag. - * @param string $text link body. It will NOT be HTML-encoded. Therefore you can pass in HTML code such as an image tag. - * @param mixed $url a URL or an action route that can be used to create a URL. - * See {@link normalizeUrl} for more details about how to specify this parameter. - * @param array $htmlOptions additional HTML attributes. Besides normal HTML attributes, a few special - * attributes are also recognized (see {@link clientChange} and {@link tag} for more details.) + * @param string $text link body. It will NOT be HTML-encoded. Therefore you can pass in HTML code + * such as an image tag. If this is is coming from end users, you should consider [[encode()]] + * it to prevent XSS attacks. + * @param array|string|null $url the URL for the hyperlink tag. This parameter will be processed by [[url()]] + * and will be used for the "href" attribute of the tag. If this parameter is null, the "href" attribute + * will not be generated. + * @param array $attributes the attributes of the hyperlink tag. The values will be HTML-encoded using [[encode()]]. + * Attributes whose value is null will be ignored and not put in the tag returned. * @return string the generated hyperlink - * @see normalizeUrl - * @see clientChange + * @see url */ - public static function link($text, $url = '#', $htmlOptions = array()) + public static function a($text, $url = null, $attributes = array()) { - if ($url !== '') { - $htmlOptions['href'] = static::normalizeUrl($url); + if ($url !== null) { + $attributes['href'] = static::url($url); } - static::clientChange('click', $htmlOptions); - return static::tag('a', $htmlOptions, $text); + return static::tag('a', $text, $attributes); } /** - * Generates a mailto link. - * @param string $text link body. It will NOT be HTML-encoded. Therefore you can pass in HTML code such as an image tag. - * @param string $email email address. If this is empty, the first parameter (link body) will be treated as the email address. - * @param array $htmlOptions additional HTML attributes. Besides normal HTML attributes, a few special - * attributes are also recognized (see {@link clientChange} and {@link tag} for more details.) + * Generates a mailto hyperlink. + * @param string $text link body. It will NOT be HTML-encoded. Therefore you can pass in HTML code + * such as an image tag. If this is is coming from end users, you should consider [[encode()]] + * it to prevent XSS attacks. + * @param string $email email address. If this is null, the first parameter (link body) will be treated + * as the email address and used. + * @param array $attributes the attributes of the hyperlink tag. The values will be HTML-encoded using [[encode()]]. + * Attributes whose value is null will be ignored and not put in the tag returned. * @return string the generated mailto link - * @see clientChange */ - public static function mailto($text, $email = '', $htmlOptions = array()) + public static function mailto($text, $email = null, $attributes = array()) { - if ($email === '') { - $email = $text; - } - return static::link($text, 'mailto:' . $email, $htmlOptions); + return static::a($text, 'mailto:' . ($email === null ? $text : $email), $attributes); } /** * Generates an image tag. - * @param string $src the image URL - * @param string $alt the alternative text display - * @param array $htmlOptions additional HTML attributes (see {@link tag}). + * @param string $src the image URL. This parameter will be processed by [[url()]]. + * @param array $attributes the attributes of the image tag. The values will be HTML-encoded using [[encode()]]. + * Attributes whose value is null will be ignored and not put in the tag returned. * @return string the generated image tag */ - public static function image($src, $alt = '', $htmlOptions = array()) + public static function img($src, $attributes = array()) { - $htmlOptions['src'] = $src; - $htmlOptions['alt'] = $alt; - return static::tag('img', $htmlOptions); + $attributes['src'] = static::url($src); + if (!isset($attributes['alt'])) { + $attributes['alt'] = ''; + } + return static::tag('img', null, $attributes); } /** - * Generates a button. - * @param string $label the button label - * @param array $htmlOptions additional HTML attributes. Besides normal HTML attributes, a few special - * attributes are also recognized (see {@link clientChange} and {@link tag} for more details.) - * @return string the generated button tag - * @see clientChange + * Generates a label tag. + * @param string $content label text. It will NOT be HTML-encoded. Therefore you can pass in HTML code + * such as an image tag. If this is is coming from end users, you should consider [[encode()]] + * it to prevent XSS attacks. + * @param string $for the ID of the HTML element that this label is associated with. + * If this is null, the "for" attribute will not be generated. + * @param array $attributes the attributes of the label tag. The values will be HTML-encoded using [[encode()]]. + * Attributes whose value is null will be ignored and not put in the tag returned. + * @return string the generated label tag */ - public static function button($name, $label = 'button', $htmlOptions = array()) + public static function label($content, $for = null, $attributes = array()) { - if (!isset($htmlOptions['name'])) { - if (!array_key_exists('name', $htmlOptions)) { - $htmlOptions['name'] = static::ID_PREFIX . static::$count++; - } - } - if (!isset($htmlOptions['type'])) { - $htmlOptions['type'] = 'button'; - } - if (!isset($htmlOptions['value'])) { - $htmlOptions['value'] = $label; - } - static::clientChange('click', $htmlOptions); - return static::tag('input', $htmlOptions); + $attributes['for'] = $for; + return static::tag('label', $content, $attributes); } /** - * Generates a button using HTML button tag. - * This method is similar to {@link button} except that it generates a 'button' - * tag instead of 'input' tag. - * @param string $label the button label. Note that this value will be directly inserted in the button element - * without being HTML-encoded. - * @param array $htmlOptions additional HTML attributes. Besides normal HTML attributes, a few special - * attributes are also recognized (see {@link clientChange} and {@link tag} for more details.) + * Generates a button tag. + * @param string $name the name attribute. If it is null, the name attribute will not be generated. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param string $content the content enclosed within the button tag. It will NOT be HTML-encoded. + * Therefore you can pass in HTML code such as an image tag. If this is is coming from end users, + * you should consider [[encode()]] it to prevent XSS attacks. + * @param array $attributes the attributes of the button tag. The values will be HTML-encoded using [[encode()]]. + * Attributes whose value is null will be ignored and not put in the tag returned. + * If the attributes does not contain "type", a default one with value "button" will be used. * @return string the generated button tag - * @see clientChange */ - public static function htmlButton($label = 'button', $htmlOptions = array()) + public static function button($name = null, $value = null, $content = 'Button', $attributes = array()) { - if (!isset($htmlOptions['name'])) { - $htmlOptions['name'] = static::ID_PREFIX . static::$count++; - } - if (!isset($htmlOptions['type'])) { - $htmlOptions['type'] = 'button'; + $attributes['name'] = $name; + $attributes['value'] = $value; + if (!isset($attributes['type'])) { + $attributes['type'] = 'button'; } - static::clientChange('click', $htmlOptions); - return static::tag('button', $htmlOptions, $label); + return static::tag('button', $content, $attributes); } /** - * Generates a submit button. - * @param string $label the button label - * @param array $htmlOptions additional HTML attributes. Besides normal HTML attributes, a few special - * attributes are also recognized (see {@link clientChange} and {@link tag} for more details.) - * @return string the generated button tag - * @see clientChange + * Generates a submit button tag. + * @param string $name the name attribute. If it is null, the name attribute will not be generated. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param string $content the content enclosed within the button tag. It will NOT be HTML-encoded. + * Therefore you can pass in HTML code such as an image tag. If this is is coming from end users, + * you should consider [[encode()]] it to prevent XSS attacks. + * @param array $attributes the attributes of the button tag. The values will be HTML-encoded using [[encode()]]. + * Attributes whose value is null will be ignored and not put in the tag returned. + * @return string the generated submit button tag */ - public static function submitButton($label = 'submit', $htmlOptions = array()) + public static function submitButton($name = null, $value = null, $content = 'Submit', $attributes = array()) { - $htmlOptions['type'] = 'submit'; - return static::button($label, $htmlOptions); + $attributes['type'] = 'submit'; + return static::button($name, $value, $content, $attributes); } /** - * Generates a reset button. - * @param string $label the button label - * @param array $htmlOptions additional HTML attributes. Besides normal HTML attributes, a few special - * attributes are also recognized (see {@link clientChange} and {@link tag} for more details.) - * @return string the generated button tag - * @see clientChange + * Generates a reset button tag. + * @param string $name the name attribute. If it is null, the name attribute will not be generated. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param string $content the content enclosed within the button tag. It will NOT be HTML-encoded. + * Therefore you can pass in HTML code such as an image tag. If this is is coming from end users, + * you should consider [[encode()]] it to prevent XSS attacks. + * @param array $attributes the attributes of the button tag. The values will be HTML-encoded using [[encode()]]. + * Attributes whose value is null will be ignored and not put in the tag returned. + * @return string the generated reset button tag */ - public static function resetButton($label = 'reset', $htmlOptions = array()) + public static function resetButton($name = null, $value = null, $content = 'Reset', $attributes = array()) { - $htmlOptions['type'] = 'reset'; - return static::button($label, $htmlOptions); + $attributes['type'] = 'reset'; + return static::button($name, $value, $content, $attributes); } /** - * Generates an image submit button. - * @param string $src the image URL - * @param array $htmlOptions additional HTML attributes. Besides normal HTML attributes, a few special - * attributes are also recognized (see {@link clientChange} and {@link tag} for more details.) + * Generates an input type of the given type. + * @param string $type the type attribute. + * @param string $name the name attribute. If it is null, the name attribute will not be generated. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param array $attributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. + * Attributes whose value is null will be ignored and not put in the tag returned. + * @return string the generated input tag + */ + public static function input($type, $name = null, $value = null, $attributes = array()) + { + $attributes['type'] = $type; + $attributes['name'] = $name; + $attributes['value'] = $value; + return static::tag('input', null, $attributes); + } + + /** + * Generates an input button. + * @param string $name the name attribute. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param array $attributes the attributes of the button tag. The values will be HTML-encoded using [[encode()]]. + * Attributes whose value is null will be ignored and not put in the tag returned. * @return string the generated button tag - * @see clientChange */ - public static function imageButton($src, $htmlOptions = array()) + public static function buttonInput($name, $value = 'Button', $attributes = array()) { - $htmlOptions['src'] = $src; - $htmlOptions['type'] = 'image'; - return static::button('submit', $htmlOptions); + return static::input('button', $name, $value, $attributes); } /** - * Generates a link submit button. - * @param string $label the button label - * @param array $htmlOptions additional HTML attributes. Besides normal HTML attributes, a few special - * attributes are also recognized (see {@link clientChange} and {@link tag} for more details.) + * Generates a submit input button. + * @param string $name the name attribute. If it is null, the name attribute will not be generated. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param array $attributes the attributes of the button tag. The values will be HTML-encoded using [[encode()]]. + * Attributes whose value is null will be ignored and not put in the tag returned. * @return string the generated button tag - * @see clientChange */ - public static function linkButton($label = 'submit', $htmlOptions = array()) + public static function submitInput($name = null, $value = 'Submit', $attributes = array()) { - if (!isset($htmlOptions['submit'])) { - $htmlOptions['submit'] = isset($htmlOptions['href']) ? $htmlOptions['href'] : ''; - } - return static::link($label, '#', $htmlOptions); + return static::input('submit', $name, $value, $attributes); } /** - * Generates a label tag. - * @param string $label label text. Note, you should HTML-encode the text if needed. - * @param string $for the ID of the HTML element that this label is associated with. - * If this is false, the 'for' attribute for the label tag will not be rendered. - * @param array $htmlOptions additional HTML attributes. - * The following HTML option is recognized: - *
      - *
    • required: if this is set and is true, the label will be styled - * with CSS class 'required' (customizable with CHtml::$requiredCss), - * and be decorated with {@link CHtml::beforeRequiredLabel} and - * {@link CHtml::afterRequiredLabel}.
    • - *
    - * @return string the generated label tag + * Generates a reset input button. + * @param string $name the name attribute. If it is null, the name attribute will not be generated. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param array $attributes the attributes of the button tag. The values will be HTML-encoded using [[encode()]]. + * Attributes whose value is null will be ignored and not put in the tag returned. + * @return string the generated button tag */ - public static function label($label, $for, $htmlOptions = array()) + public static function resetInput($name = null, $value = 'Reset', $attributes = array()) { - if ($for === false) { - unset($htmlOptions['for']); - } else { - $htmlOptions['for'] = $for; - } - if (isset($htmlOptions['required'])) { - if ($htmlOptions['required']) { - if (isset($htmlOptions['class'])) { - $htmlOptions['class'] .= ' ' . static::$requiredCss; - } else { - $htmlOptions['class'] = static::$requiredCss; - } - $label = static::$beforeRequiredLabel . $label . static::$afterRequiredLabel; - } - unset($htmlOptions['required']); - } - return static::tag('label', $htmlOptions, $label); + return static::input('reset', $name, $value, $attributes); } /** - * Generates a text field input. - * @param string $name the input name - * @param string $value the input value - * @param array $htmlOptions additional HTML attributes. Besides normal HTML attributes, a few special - * attributes are also recognized (see {@link clientChange} and {@link tag} for more details.) - * @return string the generated input field - * @see clientChange - * @see inputField + * Generates a text input field. + * @param string $name the name attribute. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param array $attributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. + * Attributes whose value is null will be ignored and not put in the tag returned. + * @return string the generated button tag */ - public static function textField($name, $value = '', $htmlOptions = array()) + public static function textInput($name, $value = null, $attributes = array()) { - static::clientChange('change', $htmlOptions); - return static::inputField('text', $name, $value, $htmlOptions); + return static::input('text', $name, $value, $attributes); } /** - * Generates a hidden input. - * @param string $name the input name - * @param string $value the input value - * @param array $htmlOptions additional HTML attributes (see {@link tag}). - * @return string the generated input field - * @see inputField + * Generates a hidden input field. + * @param string $name the name attribute. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param array $attributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. + * Attributes whose value is null will be ignored and not put in the tag returned. + * @return string the generated button tag */ - public static function hiddenField($name, $value = '', $htmlOptions = array()) + public static function hiddenInput($name, $value = null, $attributes = array()) { - return static::inputField('hidden', $name, $value, $htmlOptions); + return static::input('hidden', $name, $value, $attributes); } /** - * Generates a password field input. - * @param string $name the input name - * @param string $value the input value - * @param array $htmlOptions additional HTML attributes. Besides normal HTML attributes, a few special - * attributes are also recognized (see {@link clientChange} and {@link tag} for more details.) - * @return string the generated input field - * @see clientChange - * @see inputField + * Generates a password input field. + * @param string $name the name attribute. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param array $attributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. + * Attributes whose value is null will be ignored and not put in the tag returned. + * @return string the generated button tag */ - public static function passwordField($name, $value = '', $htmlOptions = array()) + public static function passwordInput($name, $value = null, $attributes = array()) { - static::clientChange('change', $htmlOptions); - return static::inputField('password', $name, $value, $htmlOptions); + return static::input('password', $name, $value, $attributes); } /** - * Generates a file input. - * Note, you have to set the enclosing form's 'enctype' attribute to be 'multipart/form-data'. - * After the form is submitted, the uploaded file information can be obtained via $_FILES[$name] (see - * PHP documentation). - * @param string $name the input name - * @param string $value the input value - * @param array $htmlOptions additional HTML attributes (see {@link tag}). - * @return string the generated input field - * @see inputField + * Generates a file input field. + * To use a file input field, you should set the enclosing form's "enctype" attribute to + * be "multipart/form-data". After the form is submitted, the uploaded file information + * can be obtained via $_FILES[$name] (see PHP documentation). + * @param string $name the name attribute. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param array $attributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. + * Attributes whose value is null will be ignored and not put in the tag returned. + * @return string the generated button tag */ - public static function fileField($name, $value = '', $htmlOptions = array()) + public static function fileInput($name, $value = null, $attributes = array()) { - return static::inputField('file', $name, $value, $htmlOptions); + return static::input('file', $name, $value, $attributes); } /** * Generates a text area input. * @param string $name the input name - * @param string $value the input value - * @param array $htmlOptions additional HTML attributes. Besides normal HTML attributes, a few special - * attributes are also recognized (see {@link clientChange} and {@link tag} for more details.) - * @return string the generated text area - * @see clientChange - * @see inputField + * @param string $value the input value. Note that it will be encoded using [[encode()]]. + * @param array $attributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. + * Attributes whose value is null will be ignored and not put in the tag returned. + * @return string the generated text area tag */ - public static function textArea($name, $value = '', $htmlOptions = array()) + public static function textarea($name, $value = '', $attributes = array()) { - $htmlOptions['name'] = $name; - if (!isset($htmlOptions['id'])) { - $htmlOptions['id'] = static::getIdByName($name); - } elseif ($htmlOptions['id'] === false) { - unset($htmlOptions['id']); - } - static::clientChange('change', $htmlOptions); - return static::tag('textarea', $htmlOptions, isset($htmlOptions['encode']) && !$htmlOptions['encode'] ? $value : static::encode($value)); + $attributes['name'] = $name; + return static::tag('textarea', static::encode($value), $attributes); } /** - * Generates a radio button. - * @param string $name the input name - * @param boolean $checked whether the radio button is checked - * @param array $htmlOptions additional HTML attributes. Besides normal HTML attributes, a few special - * attributes are also recognized (see {@link clientChange} and {@link tag} for more details.) - * Since version 1.1.2, a special option named 'uncheckValue' is available that can be used to specify - * the value returned when the radio button is not checked. When set, a hidden field is rendered so that - * when the radio button is not checked, we can still obtain the posted uncheck value. - * If 'uncheckValue' is not set or set to NULL, the hidden field will not be rendered. - * @return string the generated radio button - * @see clientChange - * @see inputField + * Generates a radio button input. + * @param string $name the name attribute. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param boolean $checked whether the radio button should be checked. + * @param array $attributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. + * Attributes whose value is null will be ignored and not put in the tag returned. The following attribute + * will be specially handled and not put in the resulting tag: + * + * - uncheck: string, the value associated with the uncheck state of the radio button. When this attribute + * is present, a hidden input will be generated so that if the radio button is not checked and is submitted, + * the value of this attribute will still be submitted to the server via the hidden input. + * + * @return string the generated radio button tag */ - public static function radioButton($name, $checked = false, $htmlOptions = array()) + public static function radio($name, $value = '1', $checked = false, $attributes = array()) { - if ($checked) { - $htmlOptions['checked'] = 'checked'; - } else { - unset($htmlOptions['checked']); - } - $value = isset($htmlOptions['value']) ? $htmlOptions['value'] : 1; - static::clientChange('click', $htmlOptions); - - if (array_key_exists('uncheckValue', $htmlOptions)) { - $uncheck = $htmlOptions['uncheckValue']; - unset($htmlOptions['uncheckValue']); - } else { - $uncheck = null; - } - - if ($uncheck !== null) { + $attributes['checked'] = $checked; + if (isset($attributes['uncheck'])) { // add a hidden field so that if the radio button is not selected, it still submits a value - if (isset($htmlOptions['id']) && $htmlOptions['id'] !== false) { - $uncheckOptions = array('id' => static::ID_PREFIX . $htmlOptions['id']); - } else { - $uncheckOptions = array('id' => false); - } - $hidden = static::hiddenField($name, $uncheck, $uncheckOptions); + $hidden = static::hiddenInput($name, $attributes['uncheck']); + unset($attributes['uncheck']); } else { $hidden = ''; } - - // add a hidden field so that if the radio button is not selected, it still submits a value - return $hidden . static::inputField('radio', $name, $value, $htmlOptions); + return $hidden . static::input('radio', $name, $value, $attributes); } /** - * Generates a check box. - * @param string $name the input name - * @param boolean $checked whether the check box is checked - * @param array $htmlOptions additional HTML attributes. Besides normal HTML attributes, a few special - * attributes are also recognized (see {@link clientChange} and {@link tag} for more details.) - * Since version 1.1.2, a special option named 'uncheckValue' is available that can be used to specify - * the value returned when the checkbox is not checked. When set, a hidden field is rendered so that - * when the checkbox is not checked, we can still obtain the posted uncheck value. - * If 'uncheckValue' is not set or set to NULL, the hidden field will not be rendered. - * @return string the generated check box - * @see clientChange - * @see inputField + * Generates a checkbox input. + * @param string $name the name attribute. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param boolean $checked whether the checkbox should be checked. + * @param array $attributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. + * Attributes whose value is null will be ignored and not put in the tag returned. The following attribute + * will be specially handled and not put in the resulting tag: + * + * - uncheck: string, the value associated with the uncheck state of the checkbox. When this attribute + * is present, a hidden input will be generated so that if the checkbox is not checked and is submitted, + * the value of this attribute will still be submitted to the server via the hidden input. + * + * @return string the generated checkbox tag */ - public static function checkBox($name, $checked = false, $htmlOptions = array()) + public static function checkbox($name, $value = '1', $checked = false, $attributes = array()) { - if ($checked) { - $htmlOptions['checked'] = 'checked'; - } else { - unset($htmlOptions['checked']); - } - $value = isset($htmlOptions['value']) ? $htmlOptions['value'] : 1; - static::clientChange('click', $htmlOptions); - - if (array_key_exists('uncheckValue', $htmlOptions)) { - $uncheck = $htmlOptions['uncheckValue']; - unset($htmlOptions['uncheckValue']); - } else { - $uncheck = null; - } - - if ($uncheck !== null) { - // add a hidden field so that if the check box is not checked, it still submits a value - if (isset($htmlOptions['id']) && $htmlOptions['id'] !== false) { - $uncheckOptions = array('id' => static::ID_PREFIX . $htmlOptions['id']); - } else { - $uncheckOptions = array('id' => false); - } - $hidden = static::hiddenField($name, $uncheck, $uncheckOptions); + $attributes['checked'] = $checked; + if (isset($attributes['uncheck'])) { + // add a hidden field so that if the checkbox is not selected, it still submits a value + $hidden = static::hiddenInput($name, $attributes['uncheck']); + unset($attributes['uncheck']); } else { $hidden = ''; } - - // add a hidden field so that if the check box is not checked, it still submits a value - return $hidden . static::inputField('checkbox', $name, $value, $htmlOptions); + return $hidden . static::input('checkbox', $name, $value, $attributes); } /** - * Generates a drop down list. + * Generates a drop-down list. * @param string $name the input name - * @param string $select the selected value - * @param array $data data for generating the list options (value=>display). - * You may use {@link listData} to generate this data. - * Please refer to {@link listOptions} on how this data is used to generate the list options. - * Note, the values and labels will be automatically HTML-encoded by this method. - * @param array $htmlOptions additional HTML attributes. Besides normal HTML attributes, a few special - * attributes are recognized. See {@link clientChange} and {@link tag} for more details. - * In addition, the following options are also supported specifically for dropdown list: - *
      - *
    • encode: boolean, specifies whether to encode the values. Defaults to true.
    • - *
    • prompt: string, specifies the prompt text shown as the first list option. Its value is empty. Note, the prompt text will NOT be HTML-encoded.
    • - *
    • empty: string, specifies the text corresponding to empty selection. Its value is empty. - * The 'empty' option can also be an array of value-label pairs. - * Each pair will be used to render a list option at the beginning. Note, the text label will NOT be HTML-encoded.
    • - *
    • options: array, specifies additional attributes for each OPTION tag. - * The array keys must be the option values, and the array values are the extra - * OPTION tag attributes in the name-value pairs. For example, - *
      -	 *     array(
      -	 *         'value1'=>array('disabled'=>true, 'label'=>'value 1'),
      -	 *         'value2'=>array('label'=>'value 2'),
      -	 *     );
      -	 * 
      - *
    • - *
    - * Since 1.1.13, a special option named 'unselectValue' is available. It can be used to set the value - * that will be returned when no option is selected in multiple mode. When set, a hidden field is - * rendered so that if no option is selected in multiple mode, we can still obtain the posted - * unselect value. If 'unselectValue' is not set or set to NULL, the hidden field will not be rendered. - * @return string the generated drop down list - * @see clientChange - * @see inputField - * @see listData + * @param array $items the option data items. The array keys are option values, and the array values + * are the corresponding option labels. The array can also be nested (i.e. some array values are arrays too). + * For each sub-array, an option group will be generated whose label is the key associated with the sub-array. + * If you have a list of data models, you may convert them into the format described above using + * [[\yii\util\ArrayHelper::map()]]. + * + * Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in + * the labels will also be HTML-encoded. + * @param string $selection the selected value + * @param array $attributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. + * Attributes whose value is null will be ignored and not put in the tag returned. The following attribute + * will be specially handled and not put in the resulting tag: + * + * - prompt: string, a prompt text to be displayed as the first option; + * - options: array, the attributes for the option tags. The array keys must be valid option values, + * and the array values are the extra attributes for the corresponding option tags. For example, + * + * ~~~ + * array( + * 'value1' => array('disabled' => true), + * 'value2' => array('label' => 'value 2'), + * ); + * ~~~ + * + * - groups: array, the attributes for the optgroup tags. The structure of this is similar to that of 'options', + * except that the array keys represent the optgroup labels specified in $items. + * @return string the generated drop-down list tag */ - public static function dropDownList($name, $select, $data, $htmlOptions = array()) + public static function dropDownList($name, $items, $selection = null, $attributes = array()) { - $htmlOptions['name'] = $name; - - if (!isset($htmlOptions['id'])) { - $htmlOptions['id'] = static::getIdByName($name); - } elseif ($htmlOptions['id'] === false) { - unset($htmlOptions['id']); - } - - static::clientChange('change', $htmlOptions); - $options = "\n" . static::listOptions($select, $data, $htmlOptions); - $hidden = ''; - - if (isset($htmlOptions['multiple'])) { - if (substr($htmlOptions['name'], -2) !== '[]') { - $htmlOptions['name'] .= '[]'; - } - - if (isset($htmlOptions['unselectValue'])) { - $hiddenOptions = isset($htmlOptions['id']) ? array('id' => static::ID_PREFIX . $htmlOptions['id']) : array('id' => false); - $hidden = static::hiddenField(substr($htmlOptions['name'], 0, -2), $htmlOptions['unselectValue'], $hiddenOptions); - unset($htmlOptions['unselectValue']); - } - } - // add a hidden field so that if the option is not selected, it still submits a value - return $hidden . static::tag('select', $htmlOptions, $options); + $attributes['name'] = $name; + $options = static::renderOptions($items, $selection, $attributes); + return static::tag('select', "\n" . $options . "\n", $attributes); } /** * Generates a list box. * @param string $name the input name - * @param mixed $select the selected value(s). This can be either a string for single selection or an array for multiple selections. - * @param array $data data for generating the list options (value=>display) - * You may use {@link listData} to generate this data. - * Please refer to {@link listOptions} on how this data is used to generate the list options. - * Note, the values and labels will be automatically HTML-encoded by this method. - * @param array $htmlOptions additional HTML attributes. Besides normal HTML attributes, a few special - * attributes are also recognized. See {@link clientChange} and {@link tag} for more details. - * In addition, the following options are also supported specifically for list box: - *
      - *
    • encode: boolean, specifies whether to encode the values. Defaults to true.
    • - *
    • prompt: string, specifies the prompt text shown as the first list option. Its value is empty. Note, the prompt text will NOT be HTML-encoded.
    • - *
    • empty: string, specifies the text corresponding to empty selection. Its value is empty. - * The 'empty' option can also be an array of value-label pairs. - * Each pair will be used to render a list option at the beginning. Note, the text label will NOT be HTML-encoded.
    • - *
    • options: array, specifies additional attributes for each OPTION tag. - * The array keys must be the option values, and the array values are the extra - * OPTION tag attributes in the name-value pairs. For example, - *
      -	 *     array(
      -	 *         'value1'=>array('disabled'=>true, 'label'=>'value 1'),
      -	 *         'value2'=>array('label'=>'value 2'),
      -	 *     );
      -	 * 
      - *
    • - *
    - * @return string the generated list box - * @see clientChange - * @see inputField - * @see listData - */ - public static function listBox($name, $select, $data, $htmlOptions = array()) - { - if (!isset($htmlOptions['size'])) { - $htmlOptions['size'] = 4; + * @param array $items the option data items. The array keys are option values, and the array values + * are the corresponding option labels. The array can also be nested (i.e. some array values are arrays too). + * For each sub-array, an option group will be generated whose label is the key associated with the sub-array. + * If you have a list of data models, you may convert them into the format described above using + * [[\yii\util\ArrayHelper::map()]]. + * + * Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in + * the labels will also be HTML-encoded. + * @param string|array $selection the selected value(s) + * @param array $attributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. + * Attributes whose value is null will be ignored and not put in the tag returned. The following attribute + * will be specially handled and not put in the resulting tag: + * + * - prompt: string, a prompt text to be displayed as the first option; + * - options: array, the attributes for the option tags. The array keys must be valid option values, + * and the array values are the extra attributes for the corresponding option tags. For example, + * + * ~~~ + * array( + * 'value1' => array('disabled' => true), + * 'value2' => array('label' => 'value 2'), + * ); + * ~~~ + * + * - groups: array, the attributes for the optgroup tags. The structure of this is similar to that of 'options', + * except that the array keys represent the optgroup labels specified in $items. + * - unselect: string, the value that will be submitted when no option is selected. + * When this attribute is set, a hidden field will be generated so that if no option is selected in multiple + * mode, we can still obtain the posted unselect value. + * @return string the generated list box tag + */ + public static function listBox($name, $items, $selection = null, $attributes = array()) + { + if (!isset($attributes['size'])) { + $attributes['size'] = 4; + } + if (isset($attributes['unselect'])) { + // add a hidden field so that if the list box has no option being selected, it still submits a value + $hidden = static::hiddenInput($name, $attributes['unselect']); + unset($attributes['unselect']); + } else { + $hidden = ''; } - if (isset($htmlOptions['multiple'])) { - if (substr($name, -2) !== '[]') { - $name .= '[]'; - } + if (isset($attributes['multiple']) && $attributes['multiple'] && substr($name, -2) !== '[]') { + $name .= '[]'; } - return static::dropDownList($name, $select, $data, $htmlOptions); + $attributes['name'] = $name; + $options = static::renderOptions($items, $selection, $attributes); + return $hidden . static::tag('select', "\n" . $options . "\n", $attributes); } /** - * Generates a check box list. - * A check box list allows multiple selection, like {@link listBox}. - * As a result, the corresponding POST value is an array. - * @param string $name name of the check box list. You can use this name to retrieve - * the selected value(s) once the form is submitted. - * @param mixed $select selection of the check boxes. This can be either a string - * for single selection or an array for multiple selections. - * @param array $data value-label pairs used to generate the check box list. - * Note, the values will be automatically HTML-encoded, while the labels will not. - * @param array $htmlOptions additional HTML options. The options will be applied to - * each checkbox input. The following special options are recognized: - *
      - *
    • template: string, specifies how each checkbox is rendered. Defaults - * to "{input} {label}", where "{input}" will be replaced by the generated - * check box input tag while "{label}" be replaced by the corresponding check box label.
    • - *
    • separator: string, specifies the string that separates the generated check boxes.
    • - *
    • checkAll: string, specifies the label for the "check all" checkbox. - * If this option is specified, a 'check all' checkbox will be displayed. Clicking on - * this checkbox will cause all checkboxes checked or unchecked.
    • - *
    • checkAllLast: boolean, specifies whether the 'check all' checkbox should be - * displayed at the end of the checkbox list. If this option is not set (default) - * or is false, the 'check all' checkbox will be displayed at the beginning of - * the checkbox list.
    • - *
    • labelOptions: array, specifies the additional HTML attributes to be rendered - * for every label tag in the list.
    • - *
    • container: string, specifies the checkboxes enclosing tag. Defaults to 'span'. - * If the value is an empty string, no enclosing tag will be generated
    • - *
    • baseID: string, specifies the base ID prefix to be used for checkboxes in the list. - * This option is available since version 1.1.13.
    • - *
    - * @return string the generated check box list + * Generates a list of checkboxes. + * A checkbox list allows multiple selection, like [[listBox()]]. + * As a result, the corresponding submitted value is an array. + * @param string $name the name attribute of each checkbox. + * @param array $items the data item used to generate the checkboxes. + * The array keys are the labels, while the array values are the corresponding checkbox values. + * Note that the labels will NOT be HTML-encoded, while the values will. + * @param string|array $selection the selected value(s). + * @param callable $formatter a callback that can be used to customize the generation of the HTML code + * corresponding to a single checkbox. The signature of this callback must be: + * + * ~~~ + * function ($index, $label, $name, $value, $checked) + * ~~~ + * + * where $index is the zero-based index of the checkbox in the whole list; $label + * is the label for the checkbox; and $name, $value and $checked represent the name, + * value and the checked status of the checkbox input. + * @return string the generated checkbox list */ - public static function checkBoxList($name, $select, $data, $htmlOptions = array()) + public static function checkboxList($name, $items, $selection = null, $formatter = null) { - $template = isset($htmlOptions['template']) ? $htmlOptions['template'] : '{input} {label}'; - $separator = isset($htmlOptions['separator']) ? $htmlOptions['separator'] : "
    \n"; - $container = isset($htmlOptions['container']) ? $htmlOptions['container'] : 'span'; - unset($htmlOptions['template'], $htmlOptions['separator'], $htmlOptions['container']); - if (substr($name, -2) !== '[]') { $name .= '[]'; } - if (isset($htmlOptions['checkAll'])) { - $checkAllLabel = $htmlOptions['checkAll']; - $checkAllLast = isset($htmlOptions['checkAllLast']) && $htmlOptions['checkAllLast']; - } - unset($htmlOptions['checkAll'], $htmlOptions['checkAllLast']); - - $labelOptions = isset($htmlOptions['labelOptions']) ? $htmlOptions['labelOptions'] : array(); - unset($htmlOptions['labelOptions']); - - $items = array(); - $baseID = isset($htmlOptions['baseID']) ? $htmlOptions['baseID'] : static::getIdByName($name); - unset($htmlOptions['baseID']); - $id = 0; - $checkAll = true; - - foreach ($data as $value => $label) { - $checked = !is_array($select) && !strcmp($value, $select) || is_array($select) && in_array($value, $select); - $checkAll = $checkAll && $checked; - $htmlOptions['value'] = $value; - $htmlOptions['id'] = $baseID . '_' . $id++; - $option = static::checkBox($name, $checked, $htmlOptions); - $label = static::label($label, $htmlOptions['id'], $labelOptions); - $items[] = strtr($template, array('{input}' => $option, '{label}' => $label)); - } - - if (isset($checkAllLabel)) { - $htmlOptions['value'] = 1; - $htmlOptions['id'] = $id = $baseID . '_all'; - $option = static::checkBox($id, $checkAll, $htmlOptions); - $label = static::label($checkAllLabel, $id, $labelOptions); - $item = strtr($template, array('{input}' => $option, '{label}' => $label)); - if ($checkAllLast) { - $items[] = $item; + $lines = array(); + $index = 0; + foreach ($items as $value => $label) { + $checked = $selection !== null && + (!is_array($selection) && !strcmp($value, $selection) + || is_array($selection) && in_array($value, $selection)); + if ($formatter !== null) { + $lines[] = call_user_func($formatter, $index, $label, $name, $value, $checked); } else { - array_unshift($items, $item); + $lines[] = static::label(static::checkbox($name, $value, $checked) . ' ' . $label); } - $name = strtr($name, array('[' => '\\[', ']' => '\\]')); - $js = <<getClientScript(); - $cs->registerCoreScript('jquery'); - $cs->registerScript($id, $js); + $index++; } - if (empty($container)) { - return implode($separator, $items); - } else { - return static::tag($container, array('id' => $baseID), implode($separator, $items)); - } + return implode("\n", $lines); } /** - * Generates a radio button list. - * A radio button list is like a {@link checkBoxList check box list}, except that - * it only allows single selection. - * @param string $name name of the radio button list. You can use this name to retrieve - * the selected value(s) once the form is submitted. - * @param string $select selection of the radio buttons. - * @param array $data value-label pairs used to generate the radio button list. - * Note, the values will be automatically HTML-encoded, while the labels will not. - * @param array $htmlOptions additional HTML options. The options will be applied to - * each radio button input. The following special options are recognized: - *
      - *
    • template: string, specifies how each radio button is rendered. Defaults - * to "{input} {label}", where "{input}" will be replaced by the generated - * radio button input tag while "{label}" will be replaced by the corresponding radio button label.
    • - *
    • separator: string, specifies the string that separates the generated radio buttons. Defaults to new line (
      ).
    • - *
    • labelOptions: array, specifies the additional HTML attributes to be rendered - * for every label tag in the list.
    • - *
    • container: string, specifies the radio buttons enclosing tag. Defaults to 'span'. - * If the value is an empty string, no enclosing tag will be generated
    • - *
    • baseID: string, specifies the base ID prefix to be used for radio buttons in the list. - * This option is available since version 1.1.13.
    • - *
    - * @return string the generated radio button list - */ - public static function radioButtonList($name, $select, $data, $htmlOptions = array()) - { - $template = isset($htmlOptions['template']) ? $htmlOptions['template'] : '{input} {label}'; - $separator = isset($htmlOptions['separator']) ? $htmlOptions['separator'] : "
    \n"; - $container = isset($htmlOptions['container']) ? $htmlOptions['container'] : 'span'; - unset($htmlOptions['template'], $htmlOptions['separator'], $htmlOptions['container']); - - $labelOptions = isset($htmlOptions['labelOptions']) ? $htmlOptions['labelOptions'] : array(); - unset($htmlOptions['labelOptions']); - - $items = array(); - $baseID = isset($htmlOptions['baseID']) ? $htmlOptions['baseID'] : static::getIdByName($name); - unset($htmlOptions['baseID']); - $id = 0; - foreach ($data as $value => $label) { - $checked = !strcmp($value, $select); - $htmlOptions['value'] = $value; - $htmlOptions['id'] = $baseID . '_' . $id++; - $option = static::radioButton($name, $checked, $htmlOptions); - $label = static::label($label, $htmlOptions['id'], $labelOptions); - $items[] = strtr($template, array('{input}' => $option, '{label}' => $label)); - } - if (empty($container)) { - return implode($separator, $items); - } else { - return static::tag($container, array('id' => $baseID), implode($separator, $items)); - } - } - - /** - * Normalizes the input parameter to be a valid URL. - * - * If the input parameter is an empty string, the currently requested URL will be returned. - * - * If the input parameter is a non-empty string, it is treated as a valid URL and will - * be returned without any change. + * Generates a list of radio buttons. + * A radio button list is like a checkbox list, except that it only allows single selection. + * @param string $name the name attribute of each radio button. + * @param array $items the data item used to generate the radio buttons. + * The array keys are the labels, while the array values are the corresponding radio button values. + * Note that the labels will NOT be HTML-encoded, while the values will. + * @param string|array $selection the selected value(s). + * @param callable $formatter a callback that can be used to customize the generation of the HTML code + * corresponding to a single radio button. The signature of this callback must be: * - * If the input parameter is an array, it is treated as a controller route and a list of - * GET parameters, and the {@link CController::createUrl} method will be invoked to - * create a URL. In this case, the first array element refers to the controller route, - * and the rest key-value pairs refer to the additional GET parameters for the URL. - * For example, array('post/list', 'page'=>3) may be used to generate the URL - * /index.php?r=post/list&page=3. + * ~~~ + * function ($index, $label, $name, $value, $checked) + * ~~~ * - * @param mixed $url the parameter to be used to generate a valid URL - * @return string the normalized URL + * where $index is the zero-based index of the radio button in the whole list; $label + * is the label for the radio button; and $name, $value and $checked represent the name, + * value and the checked status of the radio button input. + * @return string the generated radio button list */ - public static function normalizeUrl($url) + public static function radioList($name, $items, $selection = null, $formatter = null) { - if (is_array($url)) { - if (isset($url[0])) { - if (($c = Yii::app()->getController()) !== null) { - $url = $c->createUrl($url[0], array_splice($url, 1)); - } else { - $url = Yii::app()->createUrl($url[0], array_splice($url, 1)); - } + $lines = array(); + $index = 0; + foreach ($items as $value => $label) { + $checked = $selection !== null && + (!is_array($selection) && !strcmp($value, $selection) + || is_array($selection) && in_array($value, $selection)); + if ($formatter !== null) { + $lines[] = call_user_func($formatter, $index, $label, $name, $value, $checked); } else { - $url = ''; + $lines[] = static::label(static::radio($name, $value, $checked) . ' ' . $label); } + $index++; } - return $url === '' ? Yii::app()->getRequest()->getUrl() : $url; - } - /** - * Generates an input HTML tag. - * This method generates an input HTML tag based on the given input name and value. - * @param string $type the input type (e.g. 'text', 'radio') - * @param string $name the input name - * @param string $value the input value - * @param array $htmlOptions additional HTML attributes for the HTML tag (see {@link tag}). - * @return string the generated input tag - */ - protected static function inputField($type, $name, $value, $htmlOptions) - { - $htmlOptions['type'] = $type; - $htmlOptions['value'] = $value; - $htmlOptions['name'] = $name; - if (!isset($htmlOptions['id'])) { - $htmlOptions['id'] = static::getIdByName($name); - } elseif ($htmlOptions['id'] === false) { - unset($htmlOptions['id']); - } - return static::tag('input', $htmlOptions); + return implode("\n", $lines); } /** - * Generates the list options. - * @param mixed $selection the selected value(s). This can be either a string for single selection or an array for multiple selections. - * @param array $listData the option data (see {@link listData}) - * @param array $htmlOptions additional HTML attributes. The following two special attributes are recognized: - *
      - *
    • encode: boolean, specifies whether to encode the values. Defaults to true.
    • - *
    • prompt: string, specifies the prompt text shown as the first list option. Its value is empty. Note, the prompt text will NOT be HTML-encoded.
    • - *
    • empty: string, specifies the text corresponding to empty selection. Its value is empty. - * The 'empty' option can also be an array of value-label pairs. - * Each pair will be used to render a list option at the beginning. Note, the text label will NOT be HTML-encoded.
    • - *
    • options: array, specifies additional attributes for each OPTION tag. - * The array keys must be the option values, and the array values are the extra - * OPTION tag attributes in the name-value pairs. For example, - *
      -	 *     array(
      -	 *         'value1'=>array('disabled'=>true, 'label'=>'value 1'),
      -	 *         'value2'=>array('label'=>'value 2'),
      -	 *     );
      -	 * 
      - *
    • - *
    • key: string, specifies the name of key attribute of the selection object(s). - * This is used when the selection is represented in terms of objects. In this case, - * the property named by the key option of the objects will be treated as the actual selection value. - * This option defaults to 'primaryKey', meaning using the 'primaryKey' property value of the objects in the selection. - * This option has been available since version 1.1.3.
    • - *
    + * Renders the option tags that can be used by [[dropDownList()]] and [[listBox()]]. + * @param array $items the option data items. The array keys are option values, and the array values + * are the corresponding option labels. The array can also be nested (i.e. some array values are arrays too). + * For each sub-array, an option group will be generated whose label is the key associated with the sub-array. + * If you have a list of data models, you may convert them into the format described above using + * [[\yii\util\ArrayHelper::map()]]. + * + * Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in + * the labels will also be HTML-encoded. + * @param string|array $selection the selected value(s). This can be either a string for single selection + * or an array for multiple selections. + * @param array $attributes the attributes parameter that is passed to the [[dropDownList()]] or [[listBox()]] call. + * This method will take out these elements, if any: "prompt", "options" and "groups". See more details + * in [[dropDownList()]] for the explanation of these elements. + * * @return string the generated list options */ - public static function listOptions($selection, $listData, &$htmlOptions) + public static function renderOptions($items, $selection = null, &$attributes = array()) { - $raw = isset($htmlOptions['encode']) && !$htmlOptions['encode']; - $content = ''; - if (isset($htmlOptions['prompt'])) { - $content .= '\n"; - unset($htmlOptions['prompt']); - } - if (isset($htmlOptions['empty'])) { - if (!is_array($htmlOptions['empty'])) { - $htmlOptions['empty'] = array('' => $htmlOptions['empty']); - } - foreach ($htmlOptions['empty'] as $value => $label) { - $content .= '\n"; - } - unset($htmlOptions['empty']); + $lines = array(); + if (isset($attributes['prompt'])) { + $prompt = strtr(static::encode($attributes['prompt']), ' ', ' '); + $lines[] = static::tag('option', $prompt, array('value' => '')); } - if (isset($htmlOptions['options'])) { - $options = $htmlOptions['options']; - unset($htmlOptions['options']); - } else { - $options = array(); - } + $options = isset($attributes['options']) ? $attributes['options'] : array(); + $groups = isset($attributes['groups']) ? $attributes['groups'] : array(); + unset($attributes['prompt'], $attributes['options'], $attributes['groups']); - $key = isset($htmlOptions['key']) ? $htmlOptions['key'] : 'primaryKey'; - if (is_array($selection)) { - foreach ($selection as $i => $item) { - if (is_object($item)) { - $selection[$i] = $item->$key; - } - } - } elseif (is_object($selection)) { - $selection = $selection->$key; - } - - foreach ($listData as $key => $value) { + foreach ($items as $key => $value) { if (is_array($value)) { - $content .= '\n"; - $dummy = array('options' => $options); - if (isset($htmlOptions['encode'])) { - $dummy['encode'] = $htmlOptions['encode']; - } - $content .= static::listOptions($selection, $value, $dummy); - $content .= '' . "\n"; + $groupAttrs = isset($groups[$key]) ? $groups[$key] : array(); + $groupAttrs['label'] = $key; + $attrs = array('options' => $options, 'groups' => $groups); + $content = static::renderOptions($selection, $value, $attrs); + $lines[] = static::tag('optgroup', "\n" . $content . "\n", $groupAttrs); } else { - $attributes = array('value' => (string)$key, 'encode' => !$raw); - if (!is_array($selection) && !strcmp($key, $selection) || is_array($selection) && in_array($key, $selection)) { - $attributes['selected'] = 'selected'; - } - if (isset($options[$key])) { - $attributes = array_merge($attributes, $options[$key]); - } - $content .= static::tag('option', $attributes, $raw ? (string)$value : static::encode((string)$value)) . "\n"; + $attrs = isset($options[$key]) ? $options[$key] : array(); + $attrs['value'] = $key; + $attrs['selected'] = $selection !== null && + (!is_array($selection) && !strcmp($key, $selection) + || is_array($selection) && in_array($key, $selection)); + $lines[] = static::tag('option', strtr(static::encode($value), ' ', ' '), $attrs); } } - unset($htmlOptions['key']); - - return $content; + return implode("\n", $lines); } /** * Renders the HTML tag attributes. - * Since version 1.1.5, attributes whose value is null will not be rendered. - * Special attributes, such as 'checked', 'disabled', 'readonly', will be rendered - * properly based on their corresponding boolean value. - * @param array $attributes attributes to be rendered - * @return string the rendering result + * Boolean attributes such as s 'checked', 'disabled', 'readonly', will be handled specially + * according to [[booleanAttributes]] and [[showBooleanAttributeValues]]. + * @param array $attributes attributes to be rendered. The attribute values will be HTML-encoded using [[encode()]]. + * Attributes whose value is null will be ignored and not put in the rendering result. + * @return string the rendering result. If the attributes are not empty, they will be rendered + * into a string with a leading white space (such that it can be directly appended to the tag name + * in a tag. If there is no attribute, an empty string will be returned. */ public static function renderAttributes($attributes) { - static $specialAttributes = array( - 'async' => 1, - 'autofocus' => 1, - 'autoplay' => 1, - 'checked' => 1, - 'controls' => 1, - 'declare' => 1, - 'default' => 1, - 'defer' => 1, - 'disabled' => 1, - 'formnovalidate' => 1, - 'hidden' => 1, - 'ismap' => 1, - 'loop' => 1, - 'multiple' => 1, - 'muted' => 1, - 'nohref' => 1, - 'noresize' => 1, - 'novalidate' => 1, - 'open' => 1, - 'readonly' => 1, - 'required' => 1, - 'reversed' => 1, - 'scoped' => 1, - 'seamless' => 1, - 'selected' => 1, - 'typemustmatch' => 1, - ); - - if ($attributes === array()) { - return ''; + if (count($attributes) > 1) { + $sorted = array(); + foreach (static::$attributeOrder as $name) { + if (isset($attributes[$name])) { + $sorted[$name] = $attributes[$name]; + } + } + $attributes = array_merge($sorted, $attributes); } $html = ''; - if (isset($attributes['encode'])) { - $raw = !$attributes['encode']; - unset($attributes['encode']); - } else { - $raw = false; - } - foreach ($attributes as $name => $value) { - if (isset($specialAttributes[$name])) { - if ($value) { - $html .= ' ' . $name; - if (static::$renderSpecialAttributesValue) { - $html .= '="' . $name . '"'; - } + if (isset(static::$booleanAttributes[strtolower($name)])) { + if ($value || strcasecmp($name, $value) === 0) { + $html .= static::$showBooleanAttributeValues ? " $name=\"$name\"" : " $name"; } } elseif ($value !== null) { - $html .= ' ' . $name . '="' . ($raw ? $value : static::encode($value)) . '"'; + $html .= " $name=\"" . static::encode($value) . '"'; } } - return $html; } + + /** + * Normalizes the input parameter to be a valid URL. + * + * 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 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')`. + * + * @param array|string $url the parameter to be used to generate a valid URL + * @return string the normalized URL + * @throws InvalidParamException if the parameter is invalid. + */ + public static function url($url) + { + if (is_array($url)) { + if (isset($url[0])) { + return Yii::$app->createUrl($url[0], array_splice($url, 1)); + } else { + throw new InvalidParamException('The array specifying a URL must contain at least one element.'); + } + } elseif ($url === '') { + return Yii::$app->getRequest()->getUrl(); + } else { + return Yii::getAlias($url); + } + } } diff --git a/framework/web/Application.php b/framework/web/Application.php index d2a758a..0b9ce06 100644 --- a/framework/web/Application.php +++ b/framework/web/Application.php @@ -6,6 +6,7 @@ */ namespace yii\web; +use yii\base\InvalidParamException; /** * Application is the base class for all application classes. @@ -62,11 +63,48 @@ class Application extends \yii\base\Application } /** - * @return UrlManager + * 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 slash. If the route is an empty string, it stands for + * the route of the currently active [[controller]]. If the route is not empty, it stands for + * an action ID of the [[controller]]. + * + * 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 getUrlManager() + public function createUrl($route, $params = array()) { - return $this->getComponent('urlManager'); + if (strpos($route, '/') === false) { + // a relative route + if ($this->controller !== null) { + $route = $route === '' ? $this->controller->route : $this->controller->uniqueId . '/' . $route; + } else { + throw new InvalidParamException('No active controller exists for resolving a relative route.'); + } + } + 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); } /** @@ -86,9 +124,6 @@ class Application extends \yii\base\Application 'session' => array( 'class' => 'yii\web\Session', ), - 'urlManager' => array( - 'class' => 'yii\web\UrlManager', - ), )); } } diff --git a/framework/web/Controller.php b/framework/web/Controller.php index 2779c35..9420034 100644 --- a/framework/web/Controller.php +++ b/framework/web/Controller.php @@ -16,4 +16,8 @@ namespace yii\web; */ class Controller extends \yii\base\Controller { + public function createUrl($route, $params = array()) + { + + } } \ No newline at end of file diff --git a/framework/web/Request.php b/framework/web/Request.php index 65a7f30..c7899cf 100644 --- a/framework/web/Request.php +++ b/framework/web/Request.php @@ -368,7 +368,7 @@ class Request extends \yii\base\Request */ protected function resolvePathInfo() { - $pathInfo = $this->getRequestUri(); + $pathInfo = $this->getUrl(); if (($pos = strpos($pathInfo, '?')) !== false) { $pathInfo = substr($pathInfo, 0, $pos); @@ -407,42 +407,41 @@ class Request extends \yii\base\Request } /** - * Returns the currently requested URL. - * This is a shortcut to the concatenation of [[hostInfo]] and [[requestUri]]. - * @return string the currently requested URL. + * Returns the currently requested absolute URL. + * This is a shortcut to the concatenation of [[hostInfo]] and [[url]]. + * @return string the currently requested absolute URL. */ - public function getUrl() + public function getAbsoluteUrl() { - return $this->getHostInfo() . $this->getRequestUri(); + return $this->getHostInfo() . $this->getUrl(); } - private $_requestUri; + private $_url; /** - * Returns the portion after [[hostInfo]] for the currently requested URL. - * This refers to the portion that is after the [[hostInfo]] part. It includes the [[queryString]] part if any. - * The implementation of this method referenced Zend_Controller_Request_Http in Zend Framework. - * @return string the request URI portion for the currently requested URL. - * Note that the URI returned is URL-encoded. - * @throws InvalidConfigException if the request URI cannot be determined due to unusual server configuration + * Returns the currently requested relative URL. + * This refers to the portion of the URL that is after the [[hostInfo]] part. + * It includes the [[queryString]] part if any. + * @return string the currently requested relative URL. Note that the URI returned is URL-encoded. + * @throws InvalidConfigException if the URL cannot be determined due to unusual server configuration */ - public function getRequestUri() + public function getUrl() { - if ($this->_requestUri === null) { - $this->_requestUri = $this->resolveRequestUri(); + if ($this->_url === null) { + $this->_url = $this->resolveRequestUri(); } - return $this->_requestUri; + return $this->_url; } /** - * Sets the currently requested URI. + * Sets the currently requested relative URL. * The URI must refer to the portion that is after [[hostInfo]]. * Note that the URI should be URL-encoded. * @param string $value the request URI to be set */ - public function setRequestUri($value) + public function setUrl($value) { - $this->_requestUri = $value; + $this->_url = $value; } /** diff --git a/todo.md b/todo.md index c80da5a..03b9f70 100644 --- a/todo.md +++ b/todo.md @@ -18,6 +18,7 @@ * backend-specific unit tests * dependency unit tests - validators + * Refactor validators to add validateValue() for every validator, if possible. Check if value is an array. * FileValidator: depends on CUploadedFile * CaptchaValidator: depends on CaptchaAction * DateValidator: should we use CDateTimeParser, or simply use strtotime()? From 03e212bcf671e28ecc36445017f5d214bcf5405d Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Mon, 11 Mar 2013 11:41:46 -0400 Subject: [PATCH 079/117] Added HtmlTest. --- framework/util/Html.php | 78 ++++++--- framework/web/Application.php | 10 +- tests/unit/framework/util/ArrayHelperTest.php | 2 +- tests/unit/framework/util/HtmlTest.php | 234 ++++++++++++++++++++++++++ tests/unit/runtime/.gitignore | 1 + 5 files changed, 297 insertions(+), 28 deletions(-) create mode 100644 tests/unit/framework/util/HtmlTest.php create mode 100644 tests/unit/runtime/.gitignore diff --git a/framework/util/Html.php b/framework/util/Html.php index 774a733..8c29047 100644 --- a/framework/util/Html.php +++ b/framework/util/Html.php @@ -217,7 +217,7 @@ class Html if (!isset($attributes['type'])) { $attributes['type'] = 'text/css'; } - return static::tag('style', "\n/**/\n", $attributes); + return static::tag('style', "/**/", $attributes); } /** @@ -233,7 +233,7 @@ class Html if (!isset($attributes['type'])) { $attributes['type'] = 'text/javascript'; } - return static::tag('script', "\n/**/\n", $attributes); + return static::tag('script', "/**/", $attributes); } /** @@ -278,23 +278,25 @@ class Html */ public static function beginForm($action = '', $method = 'post', $attributes = array()) { - $attributes['action'] = $url = static::url($action); - $attributes['method'] = $method; - - $form = static::beginTag('form', $attributes); + $action = static::url($action); // query parameters in the action are ignored for GET method // we use hidden fields to add them back $hiddens = array(); - if (!strcasecmp($method, 'get') && ($pos = strpos($url, '?')) !== false) { - foreach (explode('&', substr($url, $pos + 1)) as $pair) { - if (($pos = strpos($pair, '=')) !== false) { - $hiddens[] = static::hiddenInput(urldecode(substr($pair, 0, $pos)), urldecode(substr($pair, $pos + 1))); + if (!strcasecmp($method, 'get') && ($pos = strpos($action, '?')) !== false) { + foreach (explode('&', substr($action, $pos + 1)) as $pair) { + if (($pos1 = strpos($pair, '=')) !== false) { + $hiddens[] = static::hiddenInput(urldecode(substr($pair, 0, $pos1)), urldecode(substr($pair, $pos1 + 1))); } else { $hiddens[] = static::hiddenInput(urldecode($pair), ''); } } + $action = substr($action, 0, $pos); } + + $attributes['action'] = $action; + $attributes['method'] = $method; + $form = static::beginTag('form', $attributes); if ($hiddens !== array()) { $form .= "\n" . implode("\n", $hiddens); } @@ -696,17 +698,20 @@ class Html if (!isset($attributes['size'])) { $attributes['size'] = 4; } + if (isset($attributes['multiple']) && $attributes['multiple'] && substr($name, -2) !== '[]') { + $name .= '[]'; + } + $attributes['name'] = $name; if (isset($attributes['unselect'])) { // add a hidden field so that if the list box has no option being selected, it still submits a value + if (substr($name, -2) === '[]') { + $name = substr($name, 0, -2); + } $hidden = static::hiddenInput($name, $attributes['unselect']); unset($attributes['unselect']); } else { $hidden = ''; } - if (isset($attributes['multiple']) && $attributes['multiple'] && substr($name, -2) !== '[]') { - $name .= '[]'; - } - $attributes['name'] = $name; $options = static::renderOptions($items, $selection, $attributes); return $hidden . static::tag('select', "\n" . $options . "\n", $attributes); } @@ -720,8 +725,13 @@ class Html * The array keys are the labels, while the array values are the corresponding checkbox values. * Note that the labels will NOT be HTML-encoded, while the values will. * @param string|array $selection the selected value(s). - * @param callable $formatter a callback that can be used to customize the generation of the HTML code - * corresponding to a single checkbox. The signature of this callback must be: + * @param array $options options (name => config) for the checkbox list. The following options are supported: + * + * - unselect: string, the value that should be submitted when none of the checkboxes is selected. + * By setting this option, a hidden input will be generated. + * - separator: string, the HTML code that separates items. + * - item: callable, a callback that can be used to customize the generation of the HTML code + * corresponding to a single item in $items. The signature of this callback must be: * * ~~~ * function ($index, $label, $name, $value, $checked) @@ -732,12 +742,13 @@ class Html * value and the checked status of the checkbox input. * @return string the generated checkbox list */ - public static function checkboxList($name, $items, $selection = null, $formatter = null) + public static function checkboxList($name, $items, $selection = null, $options = array()) { if (substr($name, -2) !== '[]') { $name .= '[]'; } + $formatter = isset($options['item']) ? $options['item'] : null; $lines = array(); $index = 0; foreach ($items as $value => $label) { @@ -752,7 +763,16 @@ class Html $index++; } - return implode("\n", $lines); + if (isset($options['unselect'])) { + // add a hidden field so that if the list box has no option being selected, it still submits a value + $name2 = substr($name, -2) === '[]' ? substr($name, 0, -2) : $name; + $hidden = static::hiddenInput($name2, $options['unselect']); + } else { + $hidden = ''; + } + $separator = isset($options['separator']) ? $options['separator'] : "\n"; + + return $hidden . implode($separator, $lines); } /** @@ -763,8 +783,13 @@ class Html * The array keys are the labels, while the array values are the corresponding radio button values. * Note that the labels will NOT be HTML-encoded, while the values will. * @param string|array $selection the selected value(s). - * @param callable $formatter a callback that can be used to customize the generation of the HTML code - * corresponding to a single radio button. The signature of this callback must be: + * @param array $options options (name => config) for the radio button list. The following options are supported: + * + * - unselect: string, the value that should be submitted when none of the radio buttons is selected. + * By setting this option, a hidden input will be generated. + * - separator: string, the HTML code that separates items. + * - item: callable, a callback that can be used to customize the generation of the HTML code + * corresponding to a single item in $items. The signature of this callback must be: * * ~~~ * function ($index, $label, $name, $value, $checked) @@ -775,8 +800,9 @@ class Html * value and the checked status of the radio button input. * @return string the generated radio button list */ - public static function radioList($name, $items, $selection = null, $formatter = null) + public static function radioList($name, $items, $selection = null, $options = array()) { + $formatter = isset($options['item']) ? $options['item'] : null; $lines = array(); $index = 0; foreach ($items as $value => $label) { @@ -791,7 +817,15 @@ class Html $index++; } - return implode("\n", $lines); + $separator = isset($options['separator']) ? $options['separator'] : "\n"; + if (isset($options['unselect'])) { + // add a hidden field so that if the list box has no option being selected, it still submits a value + $hidden = static::hiddenInput($name, $options['unselect']); + } else { + $hidden = ''; + } + + return $hidden . implode($separator, $lines); } /** diff --git a/framework/web/Application.php b/framework/web/Application.php index 0b9ce06..61e84a3 100644 --- a/framework/web/Application.php +++ b/framework/web/Application.php @@ -66,9 +66,9 @@ 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 slash. If the route is an empty string, it stands for - * the route of the currently active [[controller]]. If the route is not empty, it stands for - * an action ID of the [[controller]]. + * 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. @@ -81,12 +81,12 @@ class Application extends \yii\base\Application */ public function createUrl($route, $params = array()) { - if (strpos($route, '/') === false) { + 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('No active controller exists for resolving a relative route.'); + throw new InvalidParamException('Relative route cannot be handled because there is no active controller.'); } } return $this->getUrlManager()->createUrl($route, $params); diff --git a/tests/unit/framework/util/ArrayHelperTest.php b/tests/unit/framework/util/ArrayHelperTest.php index a713381..a36ce68 100644 --- a/tests/unit/framework/util/ArrayHelperTest.php +++ b/tests/unit/framework/util/ArrayHelperTest.php @@ -1,6 +1,6 @@ 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; + } + + 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() + { + } + + public function testSubmitInput() + { + } + + public function testResetInput() + { + } + + public function testTextInput() + { + } + + public function testHiddenInput() + { + } + + public function testPasswordInput() + { + } + + public function testFileInput() + { + } + + public function testTextarea() + { + } + + public function testRadio() + { + } + + public function testCheckbox() + { + } + + public function testDropDownList() + { + } + + public function testListBox() + { + } + + public function testCheckboxList() + { + } + + public function testRadioList() + { + } + + public function testRenderOptions() + { + } + + public function testRenderAttributes() + { + } + + public function testUrl() + { + } +} diff --git a/tests/unit/runtime/.gitignore b/tests/unit/runtime/.gitignore new file mode 100644 index 0000000..72e8ffc --- /dev/null +++ b/tests/unit/runtime/.gitignore @@ -0,0 +1 @@ +* From a5ababe4c63d8f22ca2b0b776cf5aacb9045e2dd Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Mon, 11 Mar 2013 15:03:00 -0400 Subject: [PATCH 080/117] Finished HtmlTest. --- framework/util/ArrayHelper.php | 4 +- framework/util/Html.php | 15 +-- framework/web/Controller.php | 4 - framework/web/Pagination.php | 4 +- framework/widgets/ActiveForm.php | 6 + tests/unit/framework/util/HtmlTest.php | 201 ++++++++++++++++++++++++++++++++- todo.md | 5 - 7 files changed, 218 insertions(+), 21 deletions(-) create mode 100644 framework/widgets/ActiveForm.php diff --git a/framework/util/ArrayHelper.php b/framework/util/ArrayHelper.php index ebf4b23..447d034 100644 --- a/framework/util/ArrayHelper.php +++ b/framework/util/ArrayHelper.php @@ -293,7 +293,7 @@ class ArrayHelper * @return array the encoded data * @see http://www.php.net/manual/en/function.htmlspecialchars.php */ - public static function htmlEncode($data, $valuesOnly = false, $charset = null) + public static function htmlEncode($data, $valuesOnly = true, $charset = null) { if ($charset === null) { $charset = Yii::$app->charset; @@ -322,7 +322,7 @@ class ArrayHelper * @return array the decoded data * @see http://www.php.net/manual/en/function.htmlspecialchars-decode.php */ - public static function htmlDecode($data, $valuesOnly = false) + public static function htmlDecode($data, $valuesOnly = true) { $d = array(); foreach ($data as $key => $value) { diff --git a/framework/util/Html.php b/framework/util/Html.php index 8c29047..92c8f02 100644 --- a/framework/util/Html.php +++ b/framework/util/Html.php @@ -106,6 +106,7 @@ class Html 'checked', 'readonly', 'disabled', + 'multiple', 'size', 'maxlength', @@ -652,7 +653,7 @@ class Html * except that the array keys represent the optgroup labels specified in $items. * @return string the generated drop-down list tag */ - public static function dropDownList($name, $items, $selection = null, $attributes = array()) + public static function dropDownList($name, $items = array(), $selection = null, $attributes = array()) { $attributes['name'] = $name; $options = static::renderOptions($items, $selection, $attributes); @@ -693,7 +694,7 @@ class Html * mode, we can still obtain the posted unselect value. * @return string the generated list box tag */ - public static function listBox($name, $items, $selection = null, $attributes = array()) + public static function listBox($name, $items = array(), $selection = null, $attributes = array()) { if (!isset($attributes['size'])) { $attributes['size'] = 4; @@ -742,7 +743,7 @@ class Html * value and the checked status of the checkbox input. * @return string the generated checkbox list */ - public static function checkboxList($name, $items, $selection = null, $options = array()) + public static function checkboxList($name, $items = array(), $selection = null, $options = array()) { if (substr($name, -2) !== '[]') { $name .= '[]'; @@ -800,7 +801,7 @@ class Html * value and the checked status of the radio button input. * @return string the generated radio button list */ - public static function radioList($name, $items, $selection = null, $options = array()) + public static function radioList($name, $items = array(), $selection = null, $options = array()) { $formatter = isset($options['item']) ? $options['item'] : null; $lines = array(); @@ -850,7 +851,7 @@ class Html { $lines = array(); if (isset($attributes['prompt'])) { - $prompt = strtr(static::encode($attributes['prompt']), ' ', ' '); + $prompt = str_replace(' ', ' ', static::encode($attributes['prompt'])); $lines[] = static::tag('option', $prompt, array('value' => '')); } @@ -863,7 +864,7 @@ class Html $groupAttrs = isset($groups[$key]) ? $groups[$key] : array(); $groupAttrs['label'] = $key; $attrs = array('options' => $options, 'groups' => $groups); - $content = static::renderOptions($selection, $value, $attrs); + $content = static::renderOptions($value, $selection, $attrs); $lines[] = static::tag('optgroup', "\n" . $content . "\n", $groupAttrs); } else { $attrs = isset($options[$key]) ? $options[$key] : array(); @@ -871,7 +872,7 @@ class Html $attrs['selected'] = $selection !== null && (!is_array($selection) && !strcmp($key, $selection) || is_array($selection) && in_array($key, $selection)); - $lines[] = static::tag('option', strtr(static::encode($value), ' ', ' '), $attrs); + $lines[] = static::tag('option', str_replace(' ', ' ', static::encode($value)), $attrs); } } diff --git a/framework/web/Controller.php b/framework/web/Controller.php index 9420034..2779c35 100644 --- a/framework/web/Controller.php +++ b/framework/web/Controller.php @@ -16,8 +16,4 @@ namespace yii\web; */ class Controller extends \yii\base\Controller { - public function createUrl($route, $params = array()) - { - - } } \ No newline at end of file diff --git a/framework/web/Pagination.php b/framework/web/Pagination.php index 84585d6..1d41c0c 100644 --- a/framework/web/Pagination.php +++ b/framework/web/Pagination.php @@ -14,7 +14,7 @@ use Yii; * * When data needs to be rendered in multiple pages, Pagination can be used to * represent information such as [[itemCount|total item count]], [[pageSize|page size]], - * [[page|current page]], etc. These information can be passed to [[yii\web\widgets\Pager|pagers]] + * [[page|current page]], etc. These information can be passed to [[yii\widgets\Pager|pagers]] * to render pagination buttons or links. * * The following example shows how to create a pagination object and feed it @@ -47,7 +47,7 @@ use Yii; * } * * // display pagination - * $this->widget('yii\web\widgets\LinkPager', array( + * $this->widget('yii\widgets\LinkPager', array( * 'pages' => $pages, * )); * ~~~ diff --git a/framework/widgets/ActiveForm.php b/framework/widgets/ActiveForm.php new file mode 100644 index 0000000..77fd9fd --- /dev/null +++ b/framework/widgets/ActiveForm.php @@ -0,0 +1,6 @@ +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() @@ -166,69 +171,263 @@ class HtmlTest extends \yii\test\TestCase 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', null, true, array('class' => 'a'))); + $this->assertEquals('', Html::radio('test', null, true, array('class' => 'a' ,'uncheck' => '0'))); } public function testCheckbox() { + $this->assertEquals('', Html::checkbox('test')); + $this->assertEquals('', Html::checkbox('test', null, true, array('class' => 'a'))); + $this->assertEquals('', Html::checkbox('test', null, true, array('class' => 'a' ,'uncheck' => '0'))); } public function testDropDownList() { + $this->assertEquals("", Html::dropDownList('test')); + $this->assertEquals("", Html::dropDownList('test', $this->getDataItems())); + $this->assertEquals("", Html::dropDownList('test', $this->getDataItems(), 'value2')); } public function testListBox() { + $expected = << + + +EOD; + $this->assertEquals($expected, Html::listBox('test')); + $expected = << + + + +EOD; + $this->assertEquals($expected, Html::listBox('test', $this->getDataItems(), null, array('size' => 5))); + $expected = << + + + +EOD; + $this->assertEquals($expected, Html::listBox('test', $this->getDataItems2(), null)); + $expected = << + + + +EOD; + $this->assertEquals($expected, Html::listBox('test', $this->getDataItems(), 'value2')); + $expected = << + + + +EOD; + $this->assertEquals($expected, Html::listBox('test', $this->getDataItems(), array('value1', 'value2'))); + + $expected = << + + +EOD; + $this->assertEquals($expected, Html::listBox('test', array(), null, 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', $this->getDataItems(), array('value2'))); + + $expected = << text1<> + +EOD; + $this->assertEquals($expected, Html::checkboxList('test', $this->getDataItems2(), array('value2'))); + + $expected = <<
    + +EOD; + $this->assertEquals($expected, Html::checkboxList('test', $this->getDataItems(), array('value2'), array( + 'separator' => "
    \n", + 'unselect' => '0', + ))); + + $expected = <<text1 + +EOD; + $this->assertEquals($expected, Html::checkboxList('test', $this->getDataItems(), array('value2'), array( + 'item' => function ($index, $label, $name, $value, $checked) { + return Html::label($label . ' ' . Html::checkbox($name, $value, $checked)); + } + ))); } public function testRadioList() { + $this->assertEquals('', Html::radioList('test')); + + $expected = << text1 + +EOD; + $this->assertEquals($expected, Html::radioList('test', $this->getDataItems(), array('value2'))); + + $expected = << text1<> + +EOD; + $this->assertEquals($expected, Html::radioList('test', $this->getDataItems2(), array('value2'))); + + $expected = <<
    + +EOD; + $this->assertEquals($expected, Html::radioList('test', $this->getDataItems(), array('value2'), array( + 'separator' => "
    \n", + 'unselect' => '0', + ))); + + $expected = <<text1 + +EOD; + $this->assertEquals($expected, Html::radioList('test', $this->getDataItems(), array('value2'), array( + 'item' => function ($index, $label, $name, $value, $checked) { + return Html::label($label . ' ' . Html::radio($name, $value, $checked)); + } + ))); } public function testRenderOptions() { + $this->assertEquals('', Html::renderOptions(array())); + + $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::renderOptions($data, array('value111', 'value1'), $attributes)); } public function testRenderAttributes() { + $this->assertEquals('', Html::renderAttributes(array())); + $this->assertEquals(' name="test" value="1<>"', Html::renderAttributes(array('name' => 'test', 'empty' => null, 'value' => '1<>'))); + Html::$showBooleanAttributeValues = false; + $this->assertEquals(' checked disabled', Html::renderAttributes(array('checked' => 'checked', 'disabled' => true, 'hidden' => false))); + Html::$showBooleanAttributeValues = true; + } + + protected function getDataItems() + { + return array( + 'value1' => 'text1', + 'value2' => 'text2', + ); } - public function testUrl() + protected function getDataItems2() { + return array( + 'value1<>' => 'text1<>', + 'value 2' => 'text 2', + ); } } diff --git a/todo.md b/todo.md index 03b9f70..4d5343a 100644 --- a/todo.md +++ b/todo.md @@ -32,8 +32,6 @@ memo * module - Module should be able to define its own configuration including routes. Application should be able to overwrite it. * application - * security - - backport 1.1 changes - built-in console commands + api doc builder * support for markdown syntax @@ -46,7 +44,6 @@ memo * parsing?? * make dates/date patterns uniform application-wide including JUI, formats etc. - helpers - * array * image * string * file @@ -59,8 +56,6 @@ memo * move generation API out of gii, provide yiic commands to use it. Use same templates for gii/yiic. * i18n variant of templates * allow to generate module-specific CRUD -- markup and HTML helpers - * use HTML5 instead of XHTML - assets * ability to manage scripts order (store these in a vector?) * http://ryanbigg.com/guides/asset_pipeline.html, http://guides.rubyonrails.org/asset_pipeline.html, use content hash instead of mtime + directory hash. From 2422a134f007c2a17db6d62099cf43c262b1fda7 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Wed, 13 Mar 2013 16:21:13 -0400 Subject: [PATCH 081/117] Refactored Html. --- framework/util/Html.php | 322 +++++++++++++++++---------------- tests/unit/framework/util/HtmlTest.php | 66 ++++--- 2 files changed, 194 insertions(+), 194 deletions(-) diff --git a/framework/util/Html.php b/framework/util/Html.php index 92c8f02..bdbdcd2 100644 --- a/framework/util/Html.php +++ b/framework/util/Html.php @@ -153,15 +153,15 @@ class Html * @param string $name the tag name * @param string $content the content to be enclosed between the start and end tags. It will not be HTML-encoded. * If this is coming from end users, you should consider [[encode()]] it to prevent XSS attacks. - * @param array $attributes the element attributes. The values will be HTML-encoded using [[encode()]]. + * @param array $tagAttributes the element attributes. The values will be HTML-encoded using [[encode()]]. * Attributes whose value is null will be ignored and not put in the tag returned. * @return string the generated HTML tag * @see beginTag * @see endTag */ - public static function tag($name, $content = '', $attributes = array()) + public static function tag($name, $content = '', $tagAttributes = array()) { - $html = '<' . $name . static::renderAttributes($attributes); + $html = '<' . $name . static::renderTagAttributes($tagAttributes); if (isset(static::$voidElements[strtolower($name)])) { return $html . (static::$closeVoidElements ? ' />' : '>'); } else { @@ -172,15 +172,15 @@ class Html /** * Generates a start tag. * @param string $name the tag name - * @param array $attributes the element attributes. The values will be HTML-encoded using [[encode()]]. + * @param array $tagAttributes the element attributes. The values will be HTML-encoded using [[encode()]]. * Attributes whose value is null will be ignored and not put in the tag returned. * @return string the generated start tag * @see endTag * @see tag */ - public static function beginTag($name, $attributes = array()) + public static function beginTag($name, $tagAttributes = array()) { - return '<' . $name . static::renderAttributes($attributes) . '>'; + return '<' . $name . static::renderTagAttributes($tagAttributes) . '>'; } /** @@ -208,76 +208,76 @@ class Html /** * Generates a style tag. * @param string $content the style content - * @param array $attributes the attributes of the style tag. The values will be HTML-encoded using [[encode()]]. + * @param array $tagAttributes the attributes of the style tag. The values will be HTML-encoded using [[encode()]]. * Attributes whose value is null will be ignored and not put in the tag returned. * If the attributes does not contain "type", a default one with value "text/css" will be used. * @return string the generated style tag */ - public static function style($content, $attributes = array()) + public static function style($content, $tagAttributes = array()) { - if (!isset($attributes['type'])) { - $attributes['type'] = 'text/css'; + if (!isset($tagAttributes['type'])) { + $tagAttributes['type'] = 'text/css'; } - return static::tag('style', "/**/", $attributes); + return static::tag('style', "/**/", $tagAttributes); } /** * Generates a script tag. * @param string $content the script content - * @param array $attributes the attributes of the script tag. The values will be HTML-encoded using [[encode()]]. + * @param array $tagAttributes the attributes of the script tag. The values will be HTML-encoded using [[encode()]]. * Attributes whose value is null will be ignored and not put in the tag returned. * If the attributes does not contain "type", a default one with value "text/javascript" will be used. * @return string the generated script tag */ - public static function script($content, $attributes = array()) + public static function script($content, $tagAttributes = array()) { - if (!isset($attributes['type'])) { - $attributes['type'] = 'text/javascript'; + if (!isset($tagAttributes['type'])) { + $tagAttributes['type'] = 'text/javascript'; } - return static::tag('script', "/**/", $attributes); + return static::tag('script', "/**/", $tagAttributes); } /** * Generates a link tag that refers to an external CSS file. * @param array|string $url the URL of the external CSS file. This parameter will be processed by [[url()]]. - * @param array $attributes the attributes of the link tag. The values will be HTML-encoded using [[encode()]]. + * @param array $tagAttributes the attributes of the link tag. The values will be HTML-encoded using [[encode()]]. * Attributes whose value is null will be ignored and not put in the tag returned. * @return string the generated link tag * @see url */ - public static function cssFile($url, $attributes = array()) + public static function cssFile($url, $tagAttributes = array()) { - $attributes['rel'] = 'stylesheet'; - $attributes['type'] = 'text/css'; - $attributes['href'] = static::url($url); - return static::tag('link', '', $attributes); + $tagAttributes['rel'] = 'stylesheet'; + $tagAttributes['type'] = 'text/css'; + $tagAttributes['href'] = static::url($url); + return static::tag('link', '', $tagAttributes); } /** * Generates a script tag that refers to an external JavaScript file. * @param string $url the URL of the external JavaScript file. This parameter will be processed by [[url()]]. - * @param array $attributes the attributes of the script tag. The values will be HTML-encoded using [[encode()]]. + * @param array $tagAttributes the attributes of the script tag. The values will be HTML-encoded using [[encode()]]. * Attributes whose value is null will be ignored and not put in the tag returned. * @return string the generated script tag * @see url */ - public static function jsFile($url, $attributes = array()) + public static function jsFile($url, $tagAttributes = array()) { - $attributes['type'] = 'text/javascript'; - $attributes['src'] = static::url($url); - return static::tag('script', '', $attributes); + $tagAttributes['type'] = 'text/javascript'; + $tagAttributes['src'] = static::url($url); + return static::tag('script', '', $tagAttributes); } /** * Generates a form start tag. * @param array|string $action the form action URL. This parameter will be processed by [[url()]]. * @param string $method form method, either "post" or "get" (case-insensitive) - * @param array $attributes the attributes of the form tag. The values will be HTML-encoded using [[encode()]]. + * @param array $tagAttributes the attributes of the form tag. The values will be HTML-encoded using [[encode()]]. * Attributes whose value is null will be ignored and not put in the tag returned. * @return string the generated form start tag. * @see endForm */ - public static function beginForm($action = '', $method = 'post', $attributes = array()) + public static function beginForm($action = '', $method = 'post', $tagAttributes = array()) { $action = static::url($action); @@ -295,9 +295,9 @@ class Html $action = substr($action, 0, $pos); } - $attributes['action'] = $action; - $attributes['method'] = $method; - $form = static::beginTag('form', $attributes); + $tagAttributes['action'] = $action; + $tagAttributes['method'] = $method; + $form = static::beginTag('form', $tagAttributes); if ($hiddens !== array()) { $form .= "\n" . implode("\n", $hiddens); } @@ -323,17 +323,17 @@ class Html * @param array|string|null $url the URL for the hyperlink tag. This parameter will be processed by [[url()]] * and will be used for the "href" attribute of the tag. If this parameter is null, the "href" attribute * will not be generated. - * @param array $attributes the attributes of the hyperlink tag. The values will be HTML-encoded using [[encode()]]. + * @param array $tagAttributes the attributes of the hyperlink tag. The values will be HTML-encoded using [[encode()]]. * Attributes whose value is null will be ignored and not put in the tag returned. * @return string the generated hyperlink * @see url */ - public static function a($text, $url = null, $attributes = array()) + public static function a($text, $url = null, $tagAttributes = array()) { if ($url !== null) { - $attributes['href'] = static::url($url); + $tagAttributes['href'] = static::url($url); } - return static::tag('a', $text, $attributes); + return static::tag('a', $text, $tagAttributes); } /** @@ -343,29 +343,29 @@ class Html * it to prevent XSS attacks. * @param string $email email address. If this is null, the first parameter (link body) will be treated * as the email address and used. - * @param array $attributes the attributes of the hyperlink tag. The values will be HTML-encoded using [[encode()]]. + * @param array $tagAttributes the attributes of the hyperlink tag. The values will be HTML-encoded using [[encode()]]. * Attributes whose value is null will be ignored and not put in the tag returned. * @return string the generated mailto link */ - public static function mailto($text, $email = null, $attributes = array()) + public static function mailto($text, $email = null, $tagAttributes = array()) { - return static::a($text, 'mailto:' . ($email === null ? $text : $email), $attributes); + return static::a($text, 'mailto:' . ($email === null ? $text : $email), $tagAttributes); } /** * Generates an image tag. * @param string $src the image URL. This parameter will be processed by [[url()]]. - * @param array $attributes the attributes of the image tag. The values will be HTML-encoded using [[encode()]]. + * @param array $tagAttributes the attributes of the image tag. The values will be HTML-encoded using [[encode()]]. * Attributes whose value is null will be ignored and not put in the tag returned. * @return string the generated image tag */ - public static function img($src, $attributes = array()) + public static function img($src, $tagAttributes = array()) { - $attributes['src'] = static::url($src); - if (!isset($attributes['alt'])) { - $attributes['alt'] = ''; + $tagAttributes['src'] = static::url($src); + if (!isset($tagAttributes['alt'])) { + $tagAttributes['alt'] = ''; } - return static::tag('img', null, $attributes); + return static::tag('img', null, $tagAttributes); } /** @@ -375,14 +375,14 @@ class Html * it to prevent XSS attacks. * @param string $for the ID of the HTML element that this label is associated with. * If this is null, the "for" attribute will not be generated. - * @param array $attributes the attributes of the label tag. The values will be HTML-encoded using [[encode()]]. + * @param array $tagAttributes the attributes of the label tag. The values will be HTML-encoded using [[encode()]]. * Attributes whose value is null will be ignored and not put in the tag returned. * @return string the generated label tag */ - public static function label($content, $for = null, $attributes = array()) + public static function label($content, $for = null, $tagAttributes = array()) { - $attributes['for'] = $for; - return static::tag('label', $content, $attributes); + $tagAttributes['for'] = $for; + return static::tag('label', $content, $tagAttributes); } /** @@ -392,19 +392,19 @@ class Html * @param string $content the content enclosed within the button tag. It will NOT be HTML-encoded. * Therefore you can pass in HTML code such as an image tag. If this is is coming from end users, * you should consider [[encode()]] it to prevent XSS attacks. - * @param array $attributes the attributes of the button tag. The values will be HTML-encoded using [[encode()]]. + * @param array $tagAttributes the attributes of the button tag. The values will be HTML-encoded using [[encode()]]. * Attributes whose value is null will be ignored and not put in the tag returned. * If the attributes does not contain "type", a default one with value "button" will be used. * @return string the generated button tag */ - public static function button($name = null, $value = null, $content = 'Button', $attributes = array()) + public static function button($name = null, $value = null, $content = 'Button', $tagAttributes = array()) { - $attributes['name'] = $name; - $attributes['value'] = $value; - if (!isset($attributes['type'])) { - $attributes['type'] = 'button'; + $tagAttributes['name'] = $name; + $tagAttributes['value'] = $value; + if (!isset($tagAttributes['type'])) { + $tagAttributes['type'] = 'button'; } - return static::tag('button', $content, $attributes); + return static::tag('button', $content, $tagAttributes); } /** @@ -414,14 +414,14 @@ class Html * @param string $content the content enclosed within the button tag. It will NOT be HTML-encoded. * Therefore you can pass in HTML code such as an image tag. If this is is coming from end users, * you should consider [[encode()]] it to prevent XSS attacks. - * @param array $attributes the attributes of the button tag. The values will be HTML-encoded using [[encode()]]. + * @param array $tagAttributes the attributes of the button tag. The values will be HTML-encoded using [[encode()]]. * Attributes whose value is null will be ignored and not put in the tag returned. * @return string the generated submit button tag */ - public static function submitButton($name = null, $value = null, $content = 'Submit', $attributes = array()) + public static function submitButton($name = null, $value = null, $content = 'Submit', $tagAttributes = array()) { - $attributes['type'] = 'submit'; - return static::button($name, $value, $content, $attributes); + $tagAttributes['type'] = 'submit'; + return static::button($name, $value, $content, $tagAttributes); } /** @@ -431,14 +431,14 @@ class Html * @param string $content the content enclosed within the button tag. It will NOT be HTML-encoded. * Therefore you can pass in HTML code such as an image tag. If this is is coming from end users, * you should consider [[encode()]] it to prevent XSS attacks. - * @param array $attributes the attributes of the button tag. The values will be HTML-encoded using [[encode()]]. + * @param array $tagAttributes the attributes of the button tag. The values will be HTML-encoded using [[encode()]]. * Attributes whose value is null will be ignored and not put in the tag returned. * @return string the generated reset button tag */ - public static function resetButton($name = null, $value = null, $content = 'Reset', $attributes = array()) + public static function resetButton($name = null, $value = null, $content = 'Reset', $tagAttributes = array()) { - $attributes['type'] = 'reset'; - return static::button($name, $value, $content, $attributes); + $tagAttributes['type'] = 'reset'; + return static::button($name, $value, $content, $tagAttributes); } /** @@ -446,94 +446,94 @@ class Html * @param string $type the type attribute. * @param string $name the name attribute. If it is null, the name attribute will not be generated. * @param string $value the value attribute. If it is null, the value attribute will not be generated. - * @param array $attributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. + * @param array $tagAttributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. * Attributes whose value is null will be ignored and not put in the tag returned. * @return string the generated input tag */ - public static function input($type, $name = null, $value = null, $attributes = array()) + public static function input($type, $name = null, $value = null, $tagAttributes = array()) { - $attributes['type'] = $type; - $attributes['name'] = $name; - $attributes['value'] = $value; - return static::tag('input', null, $attributes); + $tagAttributes['type'] = $type; + $tagAttributes['name'] = $name; + $tagAttributes['value'] = $value; + return static::tag('input', null, $tagAttributes); } /** * Generates an input button. * @param string $name the name attribute. * @param string $value the value attribute. If it is null, the value attribute will not be generated. - * @param array $attributes the attributes of the button tag. The values will be HTML-encoded using [[encode()]]. + * @param array $tagAttributes the attributes of the button tag. The values will be HTML-encoded using [[encode()]]. * Attributes whose value is null will be ignored and not put in the tag returned. * @return string the generated button tag */ - public static function buttonInput($name, $value = 'Button', $attributes = array()) + public static function buttonInput($name, $value = 'Button', $tagAttributes = array()) { - return static::input('button', $name, $value, $attributes); + return static::input('button', $name, $value, $tagAttributes); } /** * Generates a submit input button. * @param string $name the name attribute. If it is null, the name attribute will not be generated. * @param string $value the value attribute. If it is null, the value attribute will not be generated. - * @param array $attributes the attributes of the button tag. The values will be HTML-encoded using [[encode()]]. + * @param array $tagAttributes the attributes of the button tag. The values will be HTML-encoded using [[encode()]]. * Attributes whose value is null will be ignored and not put in the tag returned. * @return string the generated button tag */ - public static function submitInput($name = null, $value = 'Submit', $attributes = array()) + public static function submitInput($name = null, $value = 'Submit', $tagAttributes = array()) { - return static::input('submit', $name, $value, $attributes); + return static::input('submit', $name, $value, $tagAttributes); } /** * Generates a reset input button. * @param string $name the name attribute. If it is null, the name attribute will not be generated. * @param string $value the value attribute. If it is null, the value attribute will not be generated. - * @param array $attributes the attributes of the button tag. The values will be HTML-encoded using [[encode()]]. + * @param array $tagAttributes the attributes of the button tag. The values will be HTML-encoded using [[encode()]]. * Attributes whose value is null will be ignored and not put in the tag returned. * @return string the generated button tag */ - public static function resetInput($name = null, $value = 'Reset', $attributes = array()) + public static function resetInput($name = null, $value = 'Reset', $tagAttributes = array()) { - return static::input('reset', $name, $value, $attributes); + return static::input('reset', $name, $value, $tagAttributes); } /** * Generates a text input field. * @param string $name the name attribute. * @param string $value the value attribute. If it is null, the value attribute will not be generated. - * @param array $attributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. + * @param array $tagAttributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. * Attributes whose value is null will be ignored and not put in the tag returned. * @return string the generated button tag */ - public static function textInput($name, $value = null, $attributes = array()) + public static function textInput($name, $value = null, $tagAttributes = array()) { - return static::input('text', $name, $value, $attributes); + return static::input('text', $name, $value, $tagAttributes); } /** * Generates a hidden input field. * @param string $name the name attribute. * @param string $value the value attribute. If it is null, the value attribute will not be generated. - * @param array $attributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. + * @param array $tagAttributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. * Attributes whose value is null will be ignored and not put in the tag returned. * @return string the generated button tag */ - public static function hiddenInput($name, $value = null, $attributes = array()) + public static function hiddenInput($name, $value = null, $tagAttributes = array()) { - return static::input('hidden', $name, $value, $attributes); + return static::input('hidden', $name, $value, $tagAttributes); } /** * Generates a password input field. * @param string $name the name attribute. * @param string $value the value attribute. If it is null, the value attribute will not be generated. - * @param array $attributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. + * @param array $tagAttributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. * Attributes whose value is null will be ignored and not put in the tag returned. * @return string the generated button tag */ - public static function passwordInput($name, $value = null, $attributes = array()) + public static function passwordInput($name, $value = null, $tagAttributes = array()) { - return static::input('password', $name, $value, $attributes); + return static::input('password', $name, $value, $tagAttributes); } /** @@ -543,36 +543,36 @@ class Html * can be obtained via $_FILES[$name] (see PHP documentation). * @param string $name the name attribute. * @param string $value the value attribute. If it is null, the value attribute will not be generated. - * @param array $attributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. + * @param array $tagAttributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. * Attributes whose value is null will be ignored and not put in the tag returned. * @return string the generated button tag */ - public static function fileInput($name, $value = null, $attributes = array()) + public static function fileInput($name, $value = null, $tagAttributes = array()) { - return static::input('file', $name, $value, $attributes); + return static::input('file', $name, $value, $tagAttributes); } /** * Generates a text area input. * @param string $name the input name * @param string $value the input value. Note that it will be encoded using [[encode()]]. - * @param array $attributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. + * @param array $tagAttributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. * Attributes whose value is null will be ignored and not put in the tag returned. * @return string the generated text area tag */ - public static function textarea($name, $value = '', $attributes = array()) + public static function textarea($name, $value = '', $tagAttributes = array()) { - $attributes['name'] = $name; - return static::tag('textarea', static::encode($value), $attributes); + $tagAttributes['name'] = $name; + return static::tag('textarea', static::encode($value), $tagAttributes); } /** * Generates a radio button input. * @param string $name the name attribute. - * @param string $value the value attribute. If it is null, the value attribute will not be generated. * @param boolean $checked whether the radio button should be checked. - * @param array $attributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. - * Attributes whose value is null will be ignored and not put in the tag returned. The following attribute + * @param string $value the value attribute. If it is null, the value attribute will not be rendered. + * @param array $tagAttributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. + * Attributes whose value is null will be ignored and not put in the tag returned. The following attributes * will be specially handled and not put in the resulting tag: * * - uncheck: string, the value associated with the uncheck state of the radio button. When this attribute @@ -581,26 +581,27 @@ class Html * * @return string the generated radio button tag */ - public static function radio($name, $value = '1', $checked = false, $attributes = array()) + public static function radio($name, $checked = false, $value = '1', $tagAttributes = array()) { - $attributes['checked'] = $checked; - if (isset($attributes['uncheck'])) { + $tagAttributes['checked'] = $checked; + $tagAttributes['value'] = $value; + if (isset($tagAttributes['uncheck'])) { // add a hidden field so that if the radio button is not selected, it still submits a value - $hidden = static::hiddenInput($name, $attributes['uncheck']); - unset($attributes['uncheck']); + $hidden = static::hiddenInput($name, $tagAttributes['uncheck']); + unset($tagAttributes['uncheck']); } else { $hidden = ''; } - return $hidden . static::input('radio', $name, $value, $attributes); + return $hidden . static::input('radio', $name, $value, $tagAttributes); } /** * Generates a checkbox input. * @param string $name the name attribute. - * @param string $value the value attribute. If it is null, the value attribute will not be generated. * @param boolean $checked whether the checkbox should be checked. - * @param array $attributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. - * Attributes whose value is null will be ignored and not put in the tag returned. The following attribute + * @param string $value the value attribute. If it is null, the value attribute will not be rendered. + * @param array $tagAttributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. + * Attributes whose value is null will be ignored and not put in the tag returned. The following attributes * will be specially handled and not put in the resulting tag: * * - uncheck: string, the value associated with the uncheck state of the checkbox. When this attribute @@ -609,22 +610,24 @@ class Html * * @return string the generated checkbox tag */ - public static function checkbox($name, $value = '1', $checked = false, $attributes = array()) + public static function checkbox($name, $checked = false, $value = '1', $tagAttributes = array()) { - $attributes['checked'] = $checked; - if (isset($attributes['uncheck'])) { + $tagAttributes['checked'] = $checked; + $tagAttributes['value'] = $value; + if (isset($tagAttributes['uncheck'])) { // add a hidden field so that if the checkbox is not selected, it still submits a value - $hidden = static::hiddenInput($name, $attributes['uncheck']); - unset($attributes['uncheck']); + $hidden = static::hiddenInput($name, $tagAttributes['uncheck']); + unset($tagAttributes['uncheck']); } else { $hidden = ''; } - return $hidden . static::input('checkbox', $name, $value, $attributes); + return $hidden . static::input('checkbox', $name, $value, $tagAttributes); } /** * Generates a drop-down list. * @param string $name the input name + * @param string $selection the selected value * @param array $items the option data items. The array keys are option values, and the array values * are the corresponding option labels. The array can also be nested (i.e. some array values are arrays too). * For each sub-array, an option group will be generated whose label is the key associated with the sub-array. @@ -633,9 +636,8 @@ class Html * * Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in * the labels will also be HTML-encoded. - * @param string $selection the selected value - * @param array $attributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. - * Attributes whose value is null will be ignored and not put in the tag returned. The following attribute + * @param array $tagAttributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. + * Attributes whose value is null will be ignored and not put in the tag returned. The following attributes * will be specially handled and not put in the resulting tag: * * - prompt: string, a prompt text to be displayed as the first option; @@ -653,16 +655,17 @@ class Html * except that the array keys represent the optgroup labels specified in $items. * @return string the generated drop-down list tag */ - public static function dropDownList($name, $items = array(), $selection = null, $attributes = array()) + public static function dropDownList($name, $selection = null, $items = array(), $tagAttributes = array()) { - $attributes['name'] = $name; - $options = static::renderOptions($items, $selection, $attributes); - return static::tag('select', "\n" . $options . "\n", $attributes); + $tagAttributes['name'] = $name; + $options = static::renderSelectOptions($selection, $items, $tagAttributes); + return static::tag('select', "\n" . $options . "\n", $tagAttributes); } /** * Generates a list box. * @param string $name the input name + * @param string|array $selection the selected value(s) * @param array $items the option data items. The array keys are option values, and the array values * are the corresponding option labels. The array can also be nested (i.e. some array values are arrays too). * For each sub-array, an option group will be generated whose label is the key associated with the sub-array. @@ -671,9 +674,8 @@ class Html * * Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in * the labels will also be HTML-encoded. - * @param string|array $selection the selected value(s) - * @param array $attributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. - * Attributes whose value is null will be ignored and not put in the tag returned. The following attribute + * @param array $tagAttributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. + * Attributes whose value is null will be ignored and not put in the tag returned. The following attributes * will be specially handled and not put in the resulting tag: * * - prompt: string, a prompt text to be displayed as the first option; @@ -694,27 +696,27 @@ class Html * mode, we can still obtain the posted unselect value. * @return string the generated list box tag */ - public static function listBox($name, $items = array(), $selection = null, $attributes = array()) + public static function listBox($name, $selection = null, $items = array(), $tagAttributes = array()) { - if (!isset($attributes['size'])) { - $attributes['size'] = 4; + if (!isset($tagAttributes['size'])) { + $tagAttributes['size'] = 4; } - if (isset($attributes['multiple']) && $attributes['multiple'] && substr($name, -2) !== '[]') { + if (isset($tagAttributes['multiple']) && $tagAttributes['multiple'] && substr($name, -2) !== '[]') { $name .= '[]'; } - $attributes['name'] = $name; - if (isset($attributes['unselect'])) { + $tagAttributes['name'] = $name; + if (isset($tagAttributes['unselect'])) { // add a hidden field so that if the list box has no option being selected, it still submits a value if (substr($name, -2) === '[]') { $name = substr($name, 0, -2); } - $hidden = static::hiddenInput($name, $attributes['unselect']); - unset($attributes['unselect']); + $hidden = static::hiddenInput($name, $tagAttributes['unselect']); + unset($tagAttributes['unselect']); } else { $hidden = ''; } - $options = static::renderOptions($items, $selection, $attributes); - return $hidden . static::tag('select', "\n" . $options . "\n", $attributes); + $options = static::renderSelectOptions($selection, $items, $tagAttributes); + return $hidden . static::tag('select', "\n" . $options . "\n", $tagAttributes); } /** @@ -722,10 +724,10 @@ class Html * A checkbox list allows multiple selection, like [[listBox()]]. * As a result, the corresponding submitted value is an array. * @param string $name the name attribute of each checkbox. + * @param string|array $selection the selected value(s). * @param array $items the data item used to generate the checkboxes. * The array keys are the labels, while the array values are the corresponding checkbox values. * Note that the labels will NOT be HTML-encoded, while the values will. - * @param string|array $selection the selected value(s). * @param array $options options (name => config) for the checkbox list. The following options are supported: * * - unselect: string, the value that should be submitted when none of the checkboxes is selected. @@ -735,7 +737,7 @@ class Html * corresponding to a single item in $items. The signature of this callback must be: * * ~~~ - * function ($index, $label, $name, $value, $checked) + * function ($index, $label, $name, $checked, $value) * ~~~ * * where $index is the zero-based index of the checkbox in the whole list; $label @@ -743,7 +745,7 @@ class Html * value and the checked status of the checkbox input. * @return string the generated checkbox list */ - public static function checkboxList($name, $items = array(), $selection = null, $options = array()) + public static function checkboxList($name, $selection = null, $items = array(), $options = array()) { if (substr($name, -2) !== '[]') { $name .= '[]'; @@ -757,9 +759,9 @@ class Html (!is_array($selection) && !strcmp($value, $selection) || is_array($selection) && in_array($value, $selection)); if ($formatter !== null) { - $lines[] = call_user_func($formatter, $index, $label, $name, $value, $checked); + $lines[] = call_user_func($formatter, $index, $label, $name, $checked, $value); } else { - $lines[] = static::label(static::checkbox($name, $value, $checked) . ' ' . $label); + $lines[] = static::label(static::checkbox($name, $checked, $value) . ' ' . $label); } $index++; } @@ -780,10 +782,10 @@ class Html * Generates a list of radio buttons. * A radio button list is like a checkbox list, except that it only allows single selection. * @param string $name the name attribute of each radio button. + * @param string|array $selection the selected value(s). * @param array $items the data item used to generate the radio buttons. * The array keys are the labels, while the array values are the corresponding radio button values. * Note that the labels will NOT be HTML-encoded, while the values will. - * @param string|array $selection the selected value(s). * @param array $options options (name => config) for the radio button list. The following options are supported: * * - unselect: string, the value that should be submitted when none of the radio buttons is selected. @@ -793,7 +795,7 @@ class Html * corresponding to a single item in $items. The signature of this callback must be: * * ~~~ - * function ($index, $label, $name, $value, $checked) + * function ($index, $label, $name, $checked, $value) * ~~~ * * where $index is the zero-based index of the radio button in the whole list; $label @@ -801,7 +803,7 @@ class Html * value and the checked status of the radio button input. * @return string the generated radio button list */ - public static function radioList($name, $items = array(), $selection = null, $options = array()) + public static function radioList($name, $selection = null, $items = array(), $options = array()) { $formatter = isset($options['item']) ? $options['item'] : null; $lines = array(); @@ -811,9 +813,9 @@ class Html (!is_array($selection) && !strcmp($value, $selection) || is_array($selection) && in_array($value, $selection)); if ($formatter !== null) { - $lines[] = call_user_func($formatter, $index, $label, $name, $value, $checked); + $lines[] = call_user_func($formatter, $index, $label, $name, $checked, $value); } else { - $lines[] = static::label(static::radio($name, $value, $checked) . ' ' . $label); + $lines[] = static::label(static::radio($name, $checked, $value) . ' ' . $label); } $index++; } @@ -831,6 +833,8 @@ class Html /** * Renders the option tags that can be used by [[dropDownList()]] and [[listBox()]]. + * @param string|array $selection the selected value(s). This can be either a string for single selection + * or an array for multiple selections. * @param array $items the option data items. The array keys are option values, and the array values * are the corresponding option labels. The array can also be nested (i.e. some array values are arrays too). * For each sub-array, an option group will be generated whose label is the key associated with the sub-array. @@ -839,32 +843,30 @@ class Html * * Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in * the labels will also be HTML-encoded. - * @param string|array $selection the selected value(s). This can be either a string for single selection - * or an array for multiple selections. - * @param array $attributes the attributes parameter that is passed to the [[dropDownList()]] or [[listBox()]] call. + * @param array $tagAttributes the attributes parameter that is passed to the [[dropDownList()]] or [[listBox()]] call. * This method will take out these elements, if any: "prompt", "options" and "groups". See more details * in [[dropDownList()]] for the explanation of these elements. * * @return string the generated list options */ - public static function renderOptions($items, $selection = null, &$attributes = array()) + public static function renderSelectOptions($selection, $items, &$tagAttributes = array()) { $lines = array(); - if (isset($attributes['prompt'])) { - $prompt = str_replace(' ', ' ', static::encode($attributes['prompt'])); + if (isset($tagAttributes['prompt'])) { + $prompt = str_replace(' ', ' ', static::encode($tagAttributes['prompt'])); $lines[] = static::tag('option', $prompt, array('value' => '')); } - $options = isset($attributes['options']) ? $attributes['options'] : array(); - $groups = isset($attributes['groups']) ? $attributes['groups'] : array(); - unset($attributes['prompt'], $attributes['options'], $attributes['groups']); + $options = isset($tagAttributes['options']) ? $tagAttributes['options'] : array(); + $groups = isset($tagAttributes['groups']) ? $tagAttributes['groups'] : array(); + unset($tagAttributes['prompt'], $tagAttributes['options'], $tagAttributes['groups']); foreach ($items as $key => $value) { if (is_array($value)) { $groupAttrs = isset($groups[$key]) ? $groups[$key] : array(); $groupAttrs['label'] = $key; $attrs = array('options' => $options, 'groups' => $groups); - $content = static::renderOptions($value, $selection, $attrs); + $content = static::renderSelectOptions($selection, $value, $attrs); $lines[] = static::tag('optgroup', "\n" . $content . "\n", $groupAttrs); } else { $attrs = isset($options[$key]) ? $options[$key] : array(); @@ -883,26 +885,26 @@ class Html * Renders the HTML tag attributes. * Boolean attributes such as s 'checked', 'disabled', 'readonly', will be handled specially * according to [[booleanAttributes]] and [[showBooleanAttributeValues]]. - * @param array $attributes attributes to be rendered. The attribute values will be HTML-encoded using [[encode()]]. + * @param array $tagAttributes attributes to be rendered. The attribute values will be HTML-encoded using [[encode()]]. * Attributes whose value is null will be ignored and not put in the rendering result. * @return string the rendering result. If the attributes are not empty, they will be rendered * into a string with a leading white space (such that it can be directly appended to the tag name * in a tag. If there is no attribute, an empty string will be returned. */ - public static function renderAttributes($attributes) + public static function renderTagAttributes($tagAttributes) { - if (count($attributes) > 1) { + if (count($tagAttributes) > 1) { $sorted = array(); foreach (static::$attributeOrder as $name) { - if (isset($attributes[$name])) { - $sorted[$name] = $attributes[$name]; + if (isset($tagAttributes[$name])) { + $sorted[$name] = $tagAttributes[$name]; } } - $attributes = array_merge($sorted, $attributes); + $tagAttributes = array_merge($sorted, $tagAttributes); } $html = ''; - foreach ($attributes as $name => $value) { + foreach ($tagAttributes as $name => $value) { if (isset(static::$booleanAttributes[strtolower($name)])) { if ($value || strcasecmp($name, $value) === 0) { $html .= static::$showBooleanAttributeValues ? " $name=\"$name\"" : " $name"; diff --git a/tests/unit/framework/util/HtmlTest.php b/tests/unit/framework/util/HtmlTest.php index 43991c4..35ae24c 100644 --- a/tests/unit/framework/util/HtmlTest.php +++ b/tests/unit/framework/util/HtmlTest.php @@ -220,22 +220,22 @@ class HtmlTest extends \yii\test\TestCase public function testRadio() { $this->assertEquals('', Html::radio('test')); - $this->assertEquals('', Html::radio('test', null, true, array('class' => 'a'))); - $this->assertEquals('', Html::radio('test', null, true, array('class' => 'a' ,'uncheck' => '0'))); + $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', null, true, array('class' => 'a'))); - $this->assertEquals('', Html::checkbox('test', null, true, array('class' => 'a' ,'uncheck' => '0'))); + $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() { $this->assertEquals("", Html::dropDownList('test')); - $this->assertEquals("", Html::dropDownList('test', $this->getDataItems())); - $this->assertEquals("", Html::dropDownList('test', $this->getDataItems(), 'value2')); + $this->assertEquals("", Html::dropDownList('test', null, $this->getDataItems())); + $this->assertEquals("", Html::dropDownList('test', 'value2', $this->getDataItems())); } public function testListBox() @@ -252,41 +252,41 @@ EOD; EOD; - $this->assertEquals($expected, Html::listBox('test', $this->getDataItems(), null, array('size' => 5))); + $this->assertEquals($expected, Html::listBox('test', null, $this->getDataItems(), array('size' => 5))); $expected = << EOD; - $this->assertEquals($expected, Html::listBox('test', $this->getDataItems2(), null)); + $this->assertEquals($expected, Html::listBox('test', null, $this->getDataItems2())); $expected = << EOD; - $this->assertEquals($expected, Html::listBox('test', $this->getDataItems(), 'value2')); + $this->assertEquals($expected, Html::listBox('test', 'value2', $this->getDataItems())); $expected = << EOD; - $this->assertEquals($expected, Html::listBox('test', $this->getDataItems(), array('value1', 'value2'))); + $this->assertEquals($expected, Html::listBox('test', array('value1', 'value2'), $this->getDataItems())); $expected = << EOD; - $this->assertEquals($expected, Html::listBox('test', array(), null, array('multiple' => true))); + $this->assertEquals($expected, Html::listBox('test', null, array(), array('multiple' => true))); $expected = << EOD; - $this->assertEquals($expected, Html::listBox('test', array(), '', array('unselect' => '0'))); + $this->assertEquals($expected, Html::listBox('test', '', array(), array('unselect' => '0'))); } public function testCheckboxList() @@ -297,30 +297,30 @@ EOD; EOD; - $this->assertEquals($expected, Html::checkboxList('test', $this->getDataItems(), array('value2'))); + $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems())); $expected = << text1<> EOD; - $this->assertEquals($expected, Html::checkboxList('test', $this->getDataItems2(), array('value2'))); + $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems2())); $expected = <<
    EOD; - $this->assertEquals($expected, Html::checkboxList('test', $this->getDataItems(), array('value2'), array( + $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems(), array( 'separator' => "
    \n", 'unselect' => '0', ))); $expected = <<text1 - +0 +1 EOD; - $this->assertEquals($expected, Html::checkboxList('test', $this->getDataItems(), array('value2'), array( - 'item' => function ($index, $label, $name, $value, $checked) { - return Html::label($label . ' ' . Html::checkbox($name, $value, $checked)); + $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)); } ))); } @@ -333,38 +333,36 @@ EOD; EOD; - $this->assertEquals($expected, Html::radioList('test', $this->getDataItems(), array('value2'))); + $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems())); $expected = << text1<> EOD; - $this->assertEquals($expected, Html::radioList('test', $this->getDataItems2(), array('value2'))); + $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems2())); $expected = <<
    EOD; - $this->assertEquals($expected, Html::radioList('test', $this->getDataItems(), array('value2'), array( + $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems(), array( 'separator' => "
    \n", 'unselect' => '0', ))); $expected = <<text1 - +0 +1 EOD; - $this->assertEquals($expected, Html::radioList('test', $this->getDataItems(), array('value2'), array( - 'item' => function ($index, $label, $name, $value, $checked) { - return Html::label($label . ' ' . Html::radio($name, $value, $checked)); + $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() { - $this->assertEquals('', Html::renderOptions(array())); - $data = array( 'value1' => 'label1', 'group1' => array( @@ -403,15 +401,15 @@ EOD; 'group12' => array('class' => 'group'), ), ); - $this->assertEquals($expected, Html::renderOptions($data, array('value111', 'value1'), $attributes)); + $this->assertEquals($expected, Html::renderSelectOptions(array('value111', 'value1'), $data, $attributes)); } public function testRenderAttributes() { - $this->assertEquals('', Html::renderAttributes(array())); - $this->assertEquals(' name="test" value="1<>"', Html::renderAttributes(array('name' => 'test', 'empty' => null, 'value' => '1<>'))); + $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::renderAttributes(array('checked' => 'checked', 'disabled' => true, 'hidden' => false))); + $this->assertEquals(' checked disabled', Html::renderTagAttributes(array('checked' => 'checked', 'disabled' => true, 'hidden' => false))); Html::$showBooleanAttributeValues = true; } From 5227d2db7dcf94f022247cbeb41385d2e0896516 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Wed, 13 Mar 2013 16:46:52 -0400 Subject: [PATCH 082/117] refactoring. --- framework/util/Html.php | 389 ++++++++++++++++++--------------- tests/unit/framework/util/HtmlTest.php | 23 +- 2 files changed, 228 insertions(+), 184 deletions(-) diff --git a/framework/util/Html.php b/framework/util/Html.php index bdbdcd2..ebba40a 100644 --- a/framework/util/Html.php +++ b/framework/util/Html.php @@ -12,7 +12,7 @@ use yii\base\InvalidParamException; /** * Html provides a set of static methods for generating commonly used HTML tags. - * + * * @author Qiang Xue * @since 2.0 */ @@ -121,7 +121,6 @@ class Html 'media', ); - /** * Encodes special characters into HTML entities. * The [[yii\base\Application::charset|application charset]] will be used for encoding. @@ -153,15 +152,16 @@ class Html * @param string $name the tag name * @param string $content the content to be enclosed between the start and end tags. It will not be HTML-encoded. * If this is coming from end users, you should consider [[encode()]] it to prevent XSS attacks. - * @param array $tagAttributes the element attributes. The values will be HTML-encoded using [[encode()]]. - * Attributes whose value is null will be ignored and not put in the tag returned. + * @param array $options the tag options in terms of name-value pairs. These be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. * @return string the generated HTML tag * @see beginTag * @see endTag */ - public static function tag($name, $content = '', $tagAttributes = array()) + public static function tag($name, $content = '', $options = array()) { - $html = '<' . $name . static::renderTagAttributes($tagAttributes); + $html = '<' . $name . static::renderTagAttributes($options); if (isset(static::$voidElements[strtolower($name)])) { return $html . (static::$closeVoidElements ? ' />' : '>'); } else { @@ -172,15 +172,16 @@ class Html /** * Generates a start tag. * @param string $name the tag name - * @param array $tagAttributes the element attributes. The values will be HTML-encoded using [[encode()]]. - * Attributes whose value is null will be ignored and not put in the tag returned. + * @param array $options the tag options in terms of name-value pairs. These be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. * @return string the generated start tag * @see endTag * @see tag */ - public static function beginTag($name, $tagAttributes = array()) + public static function beginTag($name, $options = array()) { - return '<' . $name . static::renderTagAttributes($tagAttributes) . '>'; + return '<' . $name . static::renderTagAttributes($options) . '>'; } /** @@ -208,76 +209,81 @@ class Html /** * Generates a style tag. * @param string $content the style content - * @param array $tagAttributes the attributes of the style tag. The values will be HTML-encoded using [[encode()]]. - * Attributes whose value is null will be ignored and not put in the tag returned. - * If the attributes does not contain "type", a default one with value "text/css" will be used. + * @param array $options the tag options in terms of name-value pairs. These be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * If the options does not contain "type", a "type" attribute with value "text/css" will be used. * @return string the generated style tag */ - public static function style($content, $tagAttributes = array()) + public static function style($content, $options = array()) { - if (!isset($tagAttributes['type'])) { - $tagAttributes['type'] = 'text/css'; + if (!isset($options['type'])) { + $options['type'] = 'text/css'; } - return static::tag('style', "/**/", $tagAttributes); + return static::tag('style', "/**/", $options); } /** * Generates a script tag. * @param string $content the script content - * @param array $tagAttributes the attributes of the script tag. The values will be HTML-encoded using [[encode()]]. - * Attributes whose value is null will be ignored and not put in the tag returned. - * If the attributes does not contain "type", a default one with value "text/javascript" will be used. + * @param array $options the tag options in terms of name-value pairs. These be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * If the options does not contain "type", a "type" attribute with value "text/javascript" will be rendered. * @return string the generated script tag */ - public static function script($content, $tagAttributes = array()) + public static function script($content, $options = array()) { - if (!isset($tagAttributes['type'])) { - $tagAttributes['type'] = 'text/javascript'; + if (!isset($options['type'])) { + $options['type'] = 'text/javascript'; } - return static::tag('script', "/**/", $tagAttributes); + return static::tag('script', "/**/", $options); } /** * Generates a link tag that refers to an external CSS file. * @param array|string $url the URL of the external CSS file. This parameter will be processed by [[url()]]. - * @param array $tagAttributes the attributes of the link tag. The values will be HTML-encoded using [[encode()]]. - * Attributes whose value is null will be ignored and not put in the tag returned. + * @param array $options the tag options in terms of name-value pairs. These be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. * @return string the generated link tag * @see url */ - public static function cssFile($url, $tagAttributes = array()) + public static function cssFile($url, $options = array()) { - $tagAttributes['rel'] = 'stylesheet'; - $tagAttributes['type'] = 'text/css'; - $tagAttributes['href'] = static::url($url); - return static::tag('link', '', $tagAttributes); + $options['rel'] = 'stylesheet'; + $options['type'] = 'text/css'; + $options['href'] = static::url($url); + return static::tag('link', '', $options); } /** * Generates a script tag that refers to an external JavaScript file. * @param string $url the URL of the external JavaScript file. This parameter will be processed by [[url()]]. - * @param array $tagAttributes the attributes of the script tag. The values will be HTML-encoded using [[encode()]]. - * Attributes whose value is null will be ignored and not put in the tag returned. + * @param array $options the tag options in terms of name-value pairs. These be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. * @return string the generated script tag * @see url */ - public static function jsFile($url, $tagAttributes = array()) + public static function jsFile($url, $options = array()) { - $tagAttributes['type'] = 'text/javascript'; - $tagAttributes['src'] = static::url($url); - return static::tag('script', '', $tagAttributes); + $options['type'] = 'text/javascript'; + $options['src'] = static::url($url); + return static::tag('script', '', $options); } /** * Generates a form start tag. * @param array|string $action the form action URL. This parameter will be processed by [[url()]]. * @param string $method form method, either "post" or "get" (case-insensitive) - * @param array $tagAttributes the attributes of the form tag. The values will be HTML-encoded using [[encode()]]. - * Attributes whose value is null will be ignored and not put in the tag returned. + * @param array $options the tag options in terms of name-value pairs. These be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. * @return string the generated form start tag. * @see endForm */ - public static function beginForm($action = '', $method = 'post', $tagAttributes = array()) + public static function beginForm($action = '', $method = 'post', $options = array()) { $action = static::url($action); @@ -295,9 +301,9 @@ class Html $action = substr($action, 0, $pos); } - $tagAttributes['action'] = $action; - $tagAttributes['method'] = $method; - $form = static::beginTag('form', $tagAttributes); + $options['action'] = $action; + $options['method'] = $method; + $form = static::beginTag('form', $options); if ($hiddens !== array()) { $form .= "\n" . implode("\n", $hiddens); } @@ -323,17 +329,18 @@ class Html * @param array|string|null $url the URL for the hyperlink tag. This parameter will be processed by [[url()]] * and will be used for the "href" attribute of the tag. If this parameter is null, the "href" attribute * will not be generated. - * @param array $tagAttributes the attributes of the hyperlink tag. The values will be HTML-encoded using [[encode()]]. - * Attributes whose value is null will be ignored and not put in the tag returned. + * @param array $options the tag options in terms of name-value pairs. These be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. * @return string the generated hyperlink * @see url */ - public static function a($text, $url = null, $tagAttributes = array()) + public static function a($text, $url = null, $options = array()) { if ($url !== null) { - $tagAttributes['href'] = static::url($url); + $options['href'] = static::url($url); } - return static::tag('a', $text, $tagAttributes); + return static::tag('a', $text, $options); } /** @@ -343,29 +350,31 @@ class Html * it to prevent XSS attacks. * @param string $email email address. If this is null, the first parameter (link body) will be treated * as the email address and used. - * @param array $tagAttributes the attributes of the hyperlink tag. The values will be HTML-encoded using [[encode()]]. - * Attributes whose value is null will be ignored and not put in the tag returned. + * @param array $options the tag options in terms of name-value pairs. These be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. * @return string the generated mailto link */ - public static function mailto($text, $email = null, $tagAttributes = array()) + public static function mailto($text, $email = null, $options = array()) { - return static::a($text, 'mailto:' . ($email === null ? $text : $email), $tagAttributes); + return static::a($text, 'mailto:' . ($email === null ? $text : $email), $options); } /** * Generates an image tag. * @param string $src the image URL. This parameter will be processed by [[url()]]. - * @param array $tagAttributes the attributes of the image tag. The values will be HTML-encoded using [[encode()]]. - * Attributes whose value is null will be ignored and not put in the tag returned. + * @param array $options the tag options in terms of name-value pairs. These be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. * @return string the generated image tag */ - public static function img($src, $tagAttributes = array()) + public static function img($src, $options = array()) { - $tagAttributes['src'] = static::url($src); - if (!isset($tagAttributes['alt'])) { - $tagAttributes['alt'] = ''; + $options['src'] = static::url($src); + if (!isset($options['alt'])) { + $options['alt'] = ''; } - return static::tag('img', null, $tagAttributes); + return static::tag('img', null, $options); } /** @@ -375,14 +384,15 @@ class Html * it to prevent XSS attacks. * @param string $for the ID of the HTML element that this label is associated with. * If this is null, the "for" attribute will not be generated. - * @param array $tagAttributes the attributes of the label tag. The values will be HTML-encoded using [[encode()]]. - * Attributes whose value is null will be ignored and not put in the tag returned. + * @param array $options the tag options in terms of name-value pairs. These be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. * @return string the generated label tag */ - public static function label($content, $for = null, $tagAttributes = array()) + public static function label($content, $for = null, $options = array()) { - $tagAttributes['for'] = $for; - return static::tag('label', $content, $tagAttributes); + $options['for'] = $for; + return static::tag('label', $content, $options); } /** @@ -392,19 +402,20 @@ class Html * @param string $content the content enclosed within the button tag. It will NOT be HTML-encoded. * Therefore you can pass in HTML code such as an image tag. If this is is coming from end users, * you should consider [[encode()]] it to prevent XSS attacks. - * @param array $tagAttributes the attributes of the button tag. The values will be HTML-encoded using [[encode()]]. - * Attributes whose value is null will be ignored and not put in the tag returned. - * If the attributes does not contain "type", a default one with value "button" will be used. + * @param array $options the tag options in terms of name-value pairs. These be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * If the options does not contain "type", a "type" attribute with value "button" will be rendered. * @return string the generated button tag */ - public static function button($name = null, $value = null, $content = 'Button', $tagAttributes = array()) + public static function button($name = null, $value = null, $content = 'Button', $options = array()) { - $tagAttributes['name'] = $name; - $tagAttributes['value'] = $value; - if (!isset($tagAttributes['type'])) { - $tagAttributes['type'] = 'button'; + $options['name'] = $name; + $options['value'] = $value; + if (!isset($options['type'])) { + $options['type'] = 'button'; } - return static::tag('button', $content, $tagAttributes); + return static::tag('button', $content, $options); } /** @@ -414,14 +425,15 @@ class Html * @param string $content the content enclosed within the button tag. It will NOT be HTML-encoded. * Therefore you can pass in HTML code such as an image tag. If this is is coming from end users, * you should consider [[encode()]] it to prevent XSS attacks. - * @param array $tagAttributes the attributes of the button tag. The values will be HTML-encoded using [[encode()]]. - * Attributes whose value is null will be ignored and not put in the tag returned. + * @param array $options the tag options in terms of name-value pairs. These be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. * @return string the generated submit button tag */ - public static function submitButton($name = null, $value = null, $content = 'Submit', $tagAttributes = array()) + public static function submitButton($name = null, $value = null, $content = 'Submit', $options = array()) { - $tagAttributes['type'] = 'submit'; - return static::button($name, $value, $content, $tagAttributes); + $options['type'] = 'submit'; + return static::button($name, $value, $content, $options); } /** @@ -431,14 +443,15 @@ class Html * @param string $content the content enclosed within the button tag. It will NOT be HTML-encoded. * Therefore you can pass in HTML code such as an image tag. If this is is coming from end users, * you should consider [[encode()]] it to prevent XSS attacks. - * @param array $tagAttributes the attributes of the button tag. The values will be HTML-encoded using [[encode()]]. - * Attributes whose value is null will be ignored and not put in the tag returned. + * @param array $options the tag options in terms of name-value pairs. These be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. * @return string the generated reset button tag */ - public static function resetButton($name = null, $value = null, $content = 'Reset', $tagAttributes = array()) + public static function resetButton($name = null, $value = null, $content = 'Reset', $options = array()) { - $tagAttributes['type'] = 'reset'; - return static::button($name, $value, $content, $tagAttributes); + $options['type'] = 'reset'; + return static::button($name, $value, $content, $options); } /** @@ -446,94 +459,100 @@ class Html * @param string $type the type attribute. * @param string $name the name attribute. If it is null, the name attribute will not be generated. * @param string $value the value attribute. If it is null, the value attribute will not be generated. - * @param array $tagAttributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. - * Attributes whose value is null will be ignored and not put in the tag returned. + * @param array $options the tag options in terms of name-value pairs. These be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. * @return string the generated input tag */ - public static function input($type, $name = null, $value = null, $tagAttributes = array()) + public static function input($type, $name = null, $value = null, $options = array()) { - $tagAttributes['type'] = $type; - $tagAttributes['name'] = $name; - $tagAttributes['value'] = $value; - return static::tag('input', null, $tagAttributes); + $options['type'] = $type; + $options['name'] = $name; + $options['value'] = $value; + return static::tag('input', null, $options); } /** * Generates an input button. * @param string $name the name attribute. * @param string $value the value attribute. If it is null, the value attribute will not be generated. - * @param array $tagAttributes the attributes of the button tag. The values will be HTML-encoded using [[encode()]]. - * Attributes whose value is null will be ignored and not put in the tag returned. + * @param array $options the tag options in terms of name-value pairs. These be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. * @return string the generated button tag */ - public static function buttonInput($name, $value = 'Button', $tagAttributes = array()) + public static function buttonInput($name, $value = 'Button', $options = array()) { - return static::input('button', $name, $value, $tagAttributes); + return static::input('button', $name, $value, $options); } /** * Generates a submit input button. * @param string $name the name attribute. If it is null, the name attribute will not be generated. * @param string $value the value attribute. If it is null, the value attribute will not be generated. - * @param array $tagAttributes the attributes of the button tag. The values will be HTML-encoded using [[encode()]]. - * Attributes whose value is null will be ignored and not put in the tag returned. + * @param array $options the tag options in terms of name-value pairs. These be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. * @return string the generated button tag */ - public static function submitInput($name = null, $value = 'Submit', $tagAttributes = array()) + public static function submitInput($name = null, $value = 'Submit', $options = array()) { - return static::input('submit', $name, $value, $tagAttributes); + return static::input('submit', $name, $value, $options); } /** * Generates a reset input button. * @param string $name the name attribute. If it is null, the name attribute will not be generated. * @param string $value the value attribute. If it is null, the value attribute will not be generated. - * @param array $tagAttributes the attributes of the button tag. The values will be HTML-encoded using [[encode()]]. + * @param array $options the attributes of the button tag. The values will be HTML-encoded using [[encode()]]. * Attributes whose value is null will be ignored and not put in the tag returned. * @return string the generated button tag */ - public static function resetInput($name = null, $value = 'Reset', $tagAttributes = array()) + public static function resetInput($name = null, $value = 'Reset', $options = array()) { - return static::input('reset', $name, $value, $tagAttributes); + return static::input('reset', $name, $value, $options); } /** * Generates a text input field. * @param string $name the name attribute. * @param string $value the value attribute. If it is null, the value attribute will not be generated. - * @param array $tagAttributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. - * Attributes whose value is null will be ignored and not put in the tag returned. + * @param array $options the tag options in terms of name-value pairs. These be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. * @return string the generated button tag */ - public static function textInput($name, $value = null, $tagAttributes = array()) + public static function textInput($name, $value = null, $options = array()) { - return static::input('text', $name, $value, $tagAttributes); + return static::input('text', $name, $value, $options); } /** * Generates a hidden input field. * @param string $name the name attribute. * @param string $value the value attribute. If it is null, the value attribute will not be generated. - * @param array $tagAttributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. - * Attributes whose value is null will be ignored and not put in the tag returned. + * @param array $options the tag options in terms of name-value pairs. These be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. * @return string the generated button tag */ - public static function hiddenInput($name, $value = null, $tagAttributes = array()) + public static function hiddenInput($name, $value = null, $options = array()) { - return static::input('hidden', $name, $value, $tagAttributes); + return static::input('hidden', $name, $value, $options); } /** * Generates a password input field. * @param string $name the name attribute. * @param string $value the value attribute. If it is null, the value attribute will not be generated. - * @param array $tagAttributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. - * Attributes whose value is null will be ignored and not put in the tag returned. + * @param array $options the tag options in terms of name-value pairs. These be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. * @return string the generated button tag */ - public static function passwordInput($name, $value = null, $tagAttributes = array()) + public static function passwordInput($name, $value = null, $options = array()) { - return static::input('password', $name, $value, $tagAttributes); + return static::input('password', $name, $value, $options); } /** @@ -543,27 +562,29 @@ class Html * can be obtained via $_FILES[$name] (see PHP documentation). * @param string $name the name attribute. * @param string $value the value attribute. If it is null, the value attribute will not be generated. - * @param array $tagAttributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. - * Attributes whose value is null will be ignored and not put in the tag returned. + * @param array $options the tag options in terms of name-value pairs. These be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. * @return string the generated button tag */ - public static function fileInput($name, $value = null, $tagAttributes = array()) + public static function fileInput($name, $value = null, $options = array()) { - return static::input('file', $name, $value, $tagAttributes); + return static::input('file', $name, $value, $options); } /** * Generates a text area input. * @param string $name the input name * @param string $value the input value. Note that it will be encoded using [[encode()]]. - * @param array $tagAttributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. - * Attributes whose value is null will be ignored and not put in the tag returned. + * @param array $options the tag options in terms of name-value pairs. These be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. * @return string the generated text area tag */ - public static function textarea($name, $value = '', $tagAttributes = array()) + public static function textarea($name, $value = '', $options = array()) { - $tagAttributes['name'] = $name; - return static::tag('textarea', static::encode($value), $tagAttributes); + $options['name'] = $name; + return static::tag('textarea', static::encode($value), $options); } /** @@ -571,28 +592,29 @@ class Html * @param string $name the name attribute. * @param boolean $checked whether the radio button should be checked. * @param string $value the value attribute. If it is null, the value attribute will not be rendered. - * @param array $tagAttributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. - * Attributes whose value is null will be ignored and not put in the tag returned. The following attributes - * will be specially handled and not put in the resulting tag: + * @param array $options the tag options in terms of name-value pairs. The following options are supported: * * - uncheck: string, the value associated with the uncheck state of the radio button. When this attribute * is present, a hidden input will be generated so that if the radio button is not checked and is submitted, * the value of this attribute will still be submitted to the server via the hidden input. * + * The rest of the options will be rendered as the attributes of the resulting tag. The values will + * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. + * * @return string the generated radio button tag */ - public static function radio($name, $checked = false, $value = '1', $tagAttributes = array()) + public static function radio($name, $checked = false, $value = '1', $options = array()) { - $tagAttributes['checked'] = $checked; - $tagAttributes['value'] = $value; - if (isset($tagAttributes['uncheck'])) { + $options['checked'] = $checked; + $options['value'] = $value; + if (isset($options['uncheck'])) { // add a hidden field so that if the radio button is not selected, it still submits a value - $hidden = static::hiddenInput($name, $tagAttributes['uncheck']); - unset($tagAttributes['uncheck']); + $hidden = static::hiddenInput($name, $options['uncheck']); + unset($options['uncheck']); } else { $hidden = ''; } - return $hidden . static::input('radio', $name, $value, $tagAttributes); + return $hidden . static::input('radio', $name, $value, $options); } /** @@ -600,28 +622,29 @@ class Html * @param string $name the name attribute. * @param boolean $checked whether the checkbox should be checked. * @param string $value the value attribute. If it is null, the value attribute will not be rendered. - * @param array $tagAttributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. - * Attributes whose value is null will be ignored and not put in the tag returned. The following attributes - * will be specially handled and not put in the resulting tag: + * @param array $options the tag options in terms of name-value pairs. The following options are supported: * * - uncheck: string, the value associated with the uncheck state of the checkbox. When this attribute * is present, a hidden input will be generated so that if the checkbox is not checked and is submitted, * the value of this attribute will still be submitted to the server via the hidden input. * + * The rest of the options will be rendered as the attributes of the resulting tag. The values will + * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. + * * @return string the generated checkbox tag */ - public static function checkbox($name, $checked = false, $value = '1', $tagAttributes = array()) + public static function checkbox($name, $checked = false, $value = '1', $options = array()) { - $tagAttributes['checked'] = $checked; - $tagAttributes['value'] = $value; - if (isset($tagAttributes['uncheck'])) { + $options['checked'] = $checked; + $options['value'] = $value; + if (isset($options['uncheck'])) { // add a hidden field so that if the checkbox is not selected, it still submits a value - $hidden = static::hiddenInput($name, $tagAttributes['uncheck']); - unset($tagAttributes['uncheck']); + $hidden = static::hiddenInput($name, $options['uncheck']); + unset($options['uncheck']); } else { $hidden = ''; } - return $hidden . static::input('checkbox', $name, $value, $tagAttributes); + return $hidden . static::input('checkbox', $name, $value, $options); } /** @@ -636,12 +659,10 @@ class Html * * Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in * the labels will also be HTML-encoded. - * @param array $tagAttributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. - * Attributes whose value is null will be ignored and not put in the tag returned. The following attributes - * will be specially handled and not put in the resulting tag: + * @param array $options the tag options in terms of name-value pairs. The following options are supported: * * - prompt: string, a prompt text to be displayed as the first option; - * - options: array, the attributes for the option tags. The array keys must be valid option values, + * - options: array, the attributes for the select option tags. The array keys must be valid option values, * and the array values are the extra attributes for the corresponding option tags. For example, * * ~~~ @@ -653,13 +674,17 @@ class Html * * - groups: array, the attributes for the optgroup tags. The structure of this is similar to that of 'options', * except that the array keys represent the optgroup labels specified in $items. + * + * The rest of the options will be rendered as the attributes of the resulting tag. The values will + * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. + * * @return string the generated drop-down list tag */ - public static function dropDownList($name, $selection = null, $items = array(), $tagAttributes = array()) + public static function dropDownList($name, $selection = null, $items = array(), $options = array()) { - $tagAttributes['name'] = $name; - $options = static::renderSelectOptions($selection, $items, $tagAttributes); - return static::tag('select', "\n" . $options . "\n", $tagAttributes); + $options['name'] = $name; + $selectOptions = static::renderSelectOptions($selection, $items, $options); + return static::tag('select', "\n" . $selectOptions . "\n", $options); } /** @@ -674,12 +699,10 @@ class Html * * Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in * the labels will also be HTML-encoded. - * @param array $tagAttributes the attributes of the input tag. The values will be HTML-encoded using [[encode()]]. - * Attributes whose value is null will be ignored and not put in the tag returned. The following attributes - * will be specially handled and not put in the resulting tag: + * @param array $options the tag options in terms of name-value pairs. The following options are supported: * * - prompt: string, a prompt text to be displayed as the first option; - * - options: array, the attributes for the option tags. The array keys must be valid option values, + * - options: array, the attributes for the select option tags. The array keys must be valid option values, * and the array values are the extra attributes for the corresponding option tags. For example, * * ~~~ @@ -694,29 +717,33 @@ class Html * - unselect: string, the value that will be submitted when no option is selected. * When this attribute is set, a hidden field will be generated so that if no option is selected in multiple * mode, we can still obtain the posted unselect value. + * + * The rest of the options will be rendered as the attributes of the resulting tag. The values will + * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. + * * @return string the generated list box tag */ - public static function listBox($name, $selection = null, $items = array(), $tagAttributes = array()) + public static function listBox($name, $selection = null, $items = array(), $options = array()) { - if (!isset($tagAttributes['size'])) { - $tagAttributes['size'] = 4; + if (!isset($options['size'])) { + $options['size'] = 4; } - if (isset($tagAttributes['multiple']) && $tagAttributes['multiple'] && substr($name, -2) !== '[]') { + if (isset($options['multiple']) && $options['multiple'] && substr($name, -2) !== '[]') { $name .= '[]'; } - $tagAttributes['name'] = $name; - if (isset($tagAttributes['unselect'])) { + $options['name'] = $name; + if (isset($options['unselect'])) { // add a hidden field so that if the list box has no option being selected, it still submits a value if (substr($name, -2) === '[]') { $name = substr($name, 0, -2); } - $hidden = static::hiddenInput($name, $tagAttributes['unselect']); - unset($tagAttributes['unselect']); + $hidden = static::hiddenInput($name, $options['unselect']); + unset($options['unselect']); } else { $hidden = ''; } - $options = static::renderSelectOptions($selection, $items, $tagAttributes); - return $hidden . static::tag('select', "\n" . $options . "\n", $tagAttributes); + $selectOptions = static::renderSelectOptions($selection, $items, $options); + return $hidden . static::tag('select', "\n" . $selectOptions . "\n", $options); } /** @@ -757,7 +784,7 @@ class Html foreach ($items as $value => $label) { $checked = $selection !== null && (!is_array($selection) && !strcmp($value, $selection) - || is_array($selection) && in_array($value, $selection)); + || is_array($selection) && in_array($value, $selection)); if ($formatter !== null) { $lines[] = call_user_func($formatter, $index, $label, $name, $checked, $value); } else { @@ -811,7 +838,7 @@ class Html foreach ($items as $value => $label) { $checked = $selection !== null && (!is_array($selection) && !strcmp($value, $selection) - || is_array($selection) && in_array($value, $selection)); + || is_array($selection) && in_array($value, $selection)); if ($formatter !== null) { $lines[] = call_user_func($formatter, $index, $label, $name, $checked, $value); } else { @@ -843,23 +870,23 @@ class Html * * Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in * the labels will also be HTML-encoded. - * @param array $tagAttributes the attributes parameter that is passed to the [[dropDownList()]] or [[listBox()]] call. + * @param array $tagOptions the $options parameter that is passed to the [[dropDownList()]] or [[listBox()]] call. * This method will take out these elements, if any: "prompt", "options" and "groups". See more details * in [[dropDownList()]] for the explanation of these elements. * * @return string the generated list options */ - public static function renderSelectOptions($selection, $items, &$tagAttributes = array()) + public static function renderSelectOptions($selection, $items, &$tagOptions = array()) { $lines = array(); - if (isset($tagAttributes['prompt'])) { - $prompt = str_replace(' ', ' ', static::encode($tagAttributes['prompt'])); + if (isset($tagOptions['prompt'])) { + $prompt = str_replace(' ', ' ', static::encode($tagOptions['prompt'])); $lines[] = static::tag('option', $prompt, array('value' => '')); } - $options = isset($tagAttributes['options']) ? $tagAttributes['options'] : array(); - $groups = isset($tagAttributes['groups']) ? $tagAttributes['groups'] : array(); - unset($tagAttributes['prompt'], $tagAttributes['options'], $tagAttributes['groups']); + $options = isset($tagOptions['options']) ? $tagOptions['options'] : array(); + $groups = isset($tagOptions['groups']) ? $tagOptions['groups'] : array(); + unset($tagOptions['prompt'], $tagOptions['options'], $tagOptions['groups']); foreach ($items as $key => $value) { if (is_array($value)) { @@ -873,7 +900,7 @@ class Html $attrs['value'] = $key; $attrs['selected'] = $selection !== null && (!is_array($selection) && !strcmp($key, $selection) - || is_array($selection) && in_array($key, $selection)); + || is_array($selection) && in_array($key, $selection)); $lines[] = static::tag('option', str_replace(' ', ' ', static::encode($value)), $attrs); } } @@ -885,26 +912,26 @@ class Html * Renders the HTML tag attributes. * Boolean attributes such as s 'checked', 'disabled', 'readonly', will be handled specially * according to [[booleanAttributes]] and [[showBooleanAttributeValues]]. - * @param array $tagAttributes attributes to be rendered. The attribute values will be HTML-encoded using [[encode()]]. + * @param array $attributes attributes to be rendered. The attribute values will be HTML-encoded using [[encode()]]. * Attributes whose value is null will be ignored and not put in the rendering result. * @return string the rendering result. If the attributes are not empty, they will be rendered * into a string with a leading white space (such that it can be directly appended to the tag name * in a tag. If there is no attribute, an empty string will be returned. */ - public static function renderTagAttributes($tagAttributes) + public static function renderTagAttributes($attributes) { - if (count($tagAttributes) > 1) { + if (count($attributes) > 1) { $sorted = array(); foreach (static::$attributeOrder as $name) { - if (isset($tagAttributes[$name])) { - $sorted[$name] = $tagAttributes[$name]; + if (isset($attributes[$name])) { + $sorted[$name] = $attributes[$name]; } } - $tagAttributes = array_merge($sorted, $tagAttributes); + $attributes = array_merge($sorted, $attributes); } $html = ''; - foreach ($tagAttributes as $name => $value) { + foreach ($attributes as $name => $value) { if (isset(static::$booleanAttributes[strtolower($name)])) { if ($value || strcasecmp($name, $value) === 0) { $html .= static::$showBooleanAttributeValues ? " $name=\"$name\"" : " $name"; diff --git a/tests/unit/framework/util/HtmlTest.php b/tests/unit/framework/util/HtmlTest.php index 35ae24c..a887f29 100644 --- a/tests/unit/framework/util/HtmlTest.php +++ b/tests/unit/framework/util/HtmlTest.php @@ -233,9 +233,26 @@ class HtmlTest extends \yii\test\TestCase public function testDropDownList() { - $this->assertEquals("", Html::dropDownList('test')); - $this->assertEquals("", Html::dropDownList('test', null, $this->getDataItems())); - $this->assertEquals("", Html::dropDownList('test', 'value2', $this->getDataItems())); + $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() From c69e61801149d97992d0ddd6580a483cb668f1d7 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Wed, 13 Mar 2013 17:25:27 -0400 Subject: [PATCH 083/117] ActiveForm WIP --- framework/widgets/ActiveForm.php | 128 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 127 insertions(+), 1 deletion(-) diff --git a/framework/widgets/ActiveForm.php b/framework/widgets/ActiveForm.php index 77fd9fd..2bac53a 100644 --- a/framework/widgets/ActiveForm.php +++ b/framework/widgets/ActiveForm.php @@ -1,6 +1,132 @@ + * @since 2.0 + */ +class ActiveForm extends Widget { + /** + * @var mixed the form action URL (see {@link CHtml::normalizeUrl} for details about this parameter). + * If not set, the current page URL is used. + */ + public $action = ''; + /** + * @var string the form submission method. This should be either 'post' or 'get'. + * Defaults to 'post'. + */ + public $method = 'post'; + /** + * @var string the CSS class name for error messages. Defaults to 'errorMessage'. + * Individual {@link error} call may override this value by specifying the 'class' HTML option. + */ + public $errorMessageCssClass = 'errorMessage'; + /** + * @var array additional HTML attributes that should be rendered for the form tag. + */ + public $htmlOptions = array(); + /** + * @var boolean whether to enable data validation via AJAX. Defaults to false. + * When this property is set true, you should respond to the AJAX validation request on the server side as shown below: + *
    +	 * public function actionCreate()
    +	 * {
    +	 *     $model=new User;
    +	 *     if(isset($_POST['ajax']) && $_POST['ajax']==='user-form')
    +	 *     {
    +	 *         echo CActiveForm::validate($model);
    +	 *         Yii::app()->end();
    +	 *     }
    +	 *     ......
    +	 * }
    +	 * 
    + */ + public $enableAjaxValidation = false; + /** + * @var boolean whether to enable client-side data validation. Defaults to false. + * + * When this property is set true, client-side validation will be performed by validators + * that support it (see {@link CValidator::enableClientValidation} and {@link CValidator::clientValidateAttribute}). + * + * @see error + * @since 1.1.7 + */ + public $enableClientValidation = false; + + + public function errorSummary($model, $options = array()) + { + } + + public function error($model, $attribute, $options = array()) + { + } + + public function label($model, $attribute, $options = array()) + { + } + + public function input($type, $model, $attribute, $options = array()) + { + return ''; + } + + public function textInput($model, $attribute, $options = array()) + { + return $this->input('text', $model, $attribute, $options); + } + + public function hiddenInput($model, $attribute, $options = array()) + { + return $this->input('hidden', $model, $attribute, $options); + } + + public function passwordInput($model, $attribute, $options = array()) + { + return $this->input('password', $model, $attribute, $options); + } + + public function fileInput($model, $attribute, $options = array()) + { + return $this->input('file', $model, $attribute, $options); + } + + public function textarea($model, $attribute, $options = array()) + { + } + + public function radio($model, $attribute, $value = '1', $options = array()) + { + } + + public function checkbox($model, $attribute, $value = '1', $options = array()) + { + } + + public function dropDownList($model, $attribute, $items, $options = array()) + { + } + + public function listBox($model, $attribute, $items, $options = array()) + { + } + + public function checkboxList($model, $attribute, $items, $options = array()) + { + } + public function radioList($model, $attribute, $items, $options = array()) + { + } } From b71e83016fedf1c874f3514346f720db6724feea Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Thu, 14 Mar 2013 20:44:58 -0400 Subject: [PATCH 084/117] ActiveForm WIP --- framework/base/Model.php | 40 ++++++++-------- framework/util/Html.php | 46 +++++++++--------- framework/widgets/ActiveForm.php | 100 +++++++++++++++++++++++++++------------ 3 files changed, 114 insertions(+), 72 deletions(-) diff --git a/framework/base/Model.php b/framework/base/Model.php index 50b1058..b761ada 100644 --- a/framework/base/Model.php +++ b/framework/base/Model.php @@ -420,12 +420,31 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess } /** + * Returns the first error of every attribute in the model. + * @return array the first errors. An empty array will be returned if there is no error. + */ + public function getFirstErrors() + { + if (empty($this->_errors)) { + return array(); + } else { + $errors = array(); + foreach ($this->_errors as $errors) { + if (isset($errors[0])) { + $errors[] = $errors[0]; + } + } + } + return $errors; + } + + /** * Returns the first error of the specified attribute. * @param string $attribute attribute name. * @return string the error message. Null is returned if no error. * @see getErrors */ - public function getError($attribute) + public function getFirstError($attribute) { return isset($this->_errors[$attribute]) ? reset($this->_errors[$attribute]) : null; } @@ -441,25 +460,6 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess } /** - * Adds a list of errors. - * @param array $errors a list of errors. The array keys must be attribute names. - * The array values should be error messages. If an attribute has multiple errors, - * these errors must be given in terms of an array. - */ - public function addErrors($errors) - { - foreach ($errors as $attribute => $error) { - if (is_array($error)) { - foreach ($error as $e) { - $this->_errors[$attribute][] = $e; - } - } else { - $this->_errors[$attribute][] = $error; - } - } - } - - /** * Removes errors for all attributes or a single attribute. * @param string $attribute attribute name. Use null to remove errors for all attribute. */ diff --git a/framework/util/Html.php b/framework/util/Html.php index ebba40a..a7b744b 100644 --- a/framework/util/Html.php +++ b/framework/util/Html.php @@ -152,7 +152,7 @@ class Html * @param string $name the tag name * @param string $content the content to be enclosed between the start and end tags. It will not be HTML-encoded. * If this is coming from end users, you should consider [[encode()]] it to prevent XSS attacks. - * @param array $options the tag options in terms of name-value pairs. These be rendered as + * @param array $options the tag options in terms of name-value pairs. These will be rendered as * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. * If a value is null, the corresponding attribute will not be rendered. * @return string the generated HTML tag @@ -172,7 +172,7 @@ class Html /** * Generates a start tag. * @param string $name the tag name - * @param array $options the tag options in terms of name-value pairs. These be rendered as + * @param array $options the tag options in terms of name-value pairs. These will be rendered as * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. * If a value is null, the corresponding attribute will not be rendered. * @return string the generated start tag @@ -209,7 +209,7 @@ class Html /** * Generates a style tag. * @param string $content the style content - * @param array $options the tag options in terms of name-value pairs. These be rendered as + * @param array $options the tag options in terms of name-value pairs. These will be rendered as * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. * If a value is null, the corresponding attribute will not be rendered. * If the options does not contain "type", a "type" attribute with value "text/css" will be used. @@ -226,7 +226,7 @@ class Html /** * Generates a script tag. * @param string $content the script content - * @param array $options the tag options in terms of name-value pairs. These be rendered as + * @param array $options the tag options in terms of name-value pairs. These will be rendered as * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. * If a value is null, the corresponding attribute will not be rendered. * If the options does not contain "type", a "type" attribute with value "text/javascript" will be rendered. @@ -243,7 +243,7 @@ class Html /** * Generates a link tag that refers to an external CSS file. * @param array|string $url the URL of the external CSS file. This parameter will be processed by [[url()]]. - * @param array $options the tag options in terms of name-value pairs. These be rendered as + * @param array $options the tag options in terms of name-value pairs. These will be rendered as * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. * If a value is null, the corresponding attribute will not be rendered. * @return string the generated link tag @@ -260,7 +260,7 @@ class Html /** * Generates a script tag that refers to an external JavaScript file. * @param string $url the URL of the external JavaScript file. This parameter will be processed by [[url()]]. - * @param array $options the tag options in terms of name-value pairs. These be rendered as + * @param array $options the tag options in terms of name-value pairs. These will be rendered as * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. * If a value is null, the corresponding attribute will not be rendered. * @return string the generated script tag @@ -276,8 +276,8 @@ class Html /** * Generates a form start tag. * @param array|string $action the form action URL. This parameter will be processed by [[url()]]. - * @param string $method form method, either "post" or "get" (case-insensitive) - * @param array $options the tag options in terms of name-value pairs. These be rendered as + * @param string $method the form submission method, either "post" or "get" (case-insensitive) + * @param array $options the tag options in terms of name-value pairs. These will be rendered as * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. * If a value is null, the corresponding attribute will not be rendered. * @return string the generated form start tag. @@ -329,7 +329,7 @@ class Html * @param array|string|null $url the URL for the hyperlink tag. This parameter will be processed by [[url()]] * and will be used for the "href" attribute of the tag. If this parameter is null, the "href" attribute * will not be generated. - * @param array $options the tag options in terms of name-value pairs. These be rendered as + * @param array $options the tag options in terms of name-value pairs. These will be rendered as * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. * If a value is null, the corresponding attribute will not be rendered. * @return string the generated hyperlink @@ -350,7 +350,7 @@ class Html * it to prevent XSS attacks. * @param string $email email address. If this is null, the first parameter (link body) will be treated * as the email address and used. - * @param array $options the tag options in terms of name-value pairs. These be rendered as + * @param array $options the tag options in terms of name-value pairs. These will be rendered as * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. * If a value is null, the corresponding attribute will not be rendered. * @return string the generated mailto link @@ -363,7 +363,7 @@ class Html /** * Generates an image tag. * @param string $src the image URL. This parameter will be processed by [[url()]]. - * @param array $options the tag options in terms of name-value pairs. These be rendered as + * @param array $options the tag options in terms of name-value pairs. These will be rendered as * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. * If a value is null, the corresponding attribute will not be rendered. * @return string the generated image tag @@ -384,7 +384,7 @@ class Html * it to prevent XSS attacks. * @param string $for the ID of the HTML element that this label is associated with. * If this is null, the "for" attribute will not be generated. - * @param array $options the tag options in terms of name-value pairs. These be rendered as + * @param array $options the tag options in terms of name-value pairs. These will be rendered as * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. * If a value is null, the corresponding attribute will not be rendered. * @return string the generated label tag @@ -402,7 +402,7 @@ class Html * @param string $content the content enclosed within the button tag. It will NOT be HTML-encoded. * Therefore you can pass in HTML code such as an image tag. If this is is coming from end users, * you should consider [[encode()]] it to prevent XSS attacks. - * @param array $options the tag options in terms of name-value pairs. These be rendered as + * @param array $options the tag options in terms of name-value pairs. These will be rendered as * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. * If a value is null, the corresponding attribute will not be rendered. * If the options does not contain "type", a "type" attribute with value "button" will be rendered. @@ -425,7 +425,7 @@ class Html * @param string $content the content enclosed within the button tag. It will NOT be HTML-encoded. * Therefore you can pass in HTML code such as an image tag. If this is is coming from end users, * you should consider [[encode()]] it to prevent XSS attacks. - * @param array $options the tag options in terms of name-value pairs. These be rendered as + * @param array $options the tag options in terms of name-value pairs. These will be rendered as * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. * If a value is null, the corresponding attribute will not be rendered. * @return string the generated submit button tag @@ -443,7 +443,7 @@ class Html * @param string $content the content enclosed within the button tag. It will NOT be HTML-encoded. * Therefore you can pass in HTML code such as an image tag. If this is is coming from end users, * you should consider [[encode()]] it to prevent XSS attacks. - * @param array $options the tag options in terms of name-value pairs. These be rendered as + * @param array $options the tag options in terms of name-value pairs. These will be rendered as * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. * If a value is null, the corresponding attribute will not be rendered. * @return string the generated reset button tag @@ -459,7 +459,7 @@ class Html * @param string $type the type attribute. * @param string $name the name attribute. If it is null, the name attribute will not be generated. * @param string $value the value attribute. If it is null, the value attribute will not be generated. - * @param array $options the tag options in terms of name-value pairs. These be rendered as + * @param array $options the tag options in terms of name-value pairs. These will be rendered as * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. * If a value is null, the corresponding attribute will not be rendered. * @return string the generated input tag @@ -476,7 +476,7 @@ class Html * Generates an input button. * @param string $name the name attribute. * @param string $value the value attribute. If it is null, the value attribute will not be generated. - * @param array $options the tag options in terms of name-value pairs. These be rendered as + * @param array $options the tag options in terms of name-value pairs. These will be rendered as * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. * If a value is null, the corresponding attribute will not be rendered. * @return string the generated button tag @@ -490,7 +490,7 @@ class Html * Generates a submit input button. * @param string $name the name attribute. If it is null, the name attribute will not be generated. * @param string $value the value attribute. If it is null, the value attribute will not be generated. - * @param array $options the tag options in terms of name-value pairs. These be rendered as + * @param array $options the tag options in terms of name-value pairs. These will be rendered as * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. * If a value is null, the corresponding attribute will not be rendered. * @return string the generated button tag @@ -517,7 +517,7 @@ class Html * Generates a text input field. * @param string $name the name attribute. * @param string $value the value attribute. If it is null, the value attribute will not be generated. - * @param array $options the tag options in terms of name-value pairs. These be rendered as + * @param array $options the tag options in terms of name-value pairs. These will be rendered as * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. * If a value is null, the corresponding attribute will not be rendered. * @return string the generated button tag @@ -531,7 +531,7 @@ class Html * Generates a hidden input field. * @param string $name the name attribute. * @param string $value the value attribute. If it is null, the value attribute will not be generated. - * @param array $options the tag options in terms of name-value pairs. These be rendered as + * @param array $options the tag options in terms of name-value pairs. These will be rendered as * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. * If a value is null, the corresponding attribute will not be rendered. * @return string the generated button tag @@ -545,7 +545,7 @@ class Html * Generates a password input field. * @param string $name the name attribute. * @param string $value the value attribute. If it is null, the value attribute will not be generated. - * @param array $options the tag options in terms of name-value pairs. These be rendered as + * @param array $options the tag options in terms of name-value pairs. These will be rendered as * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. * If a value is null, the corresponding attribute will not be rendered. * @return string the generated button tag @@ -562,7 +562,7 @@ class Html * can be obtained via $_FILES[$name] (see PHP documentation). * @param string $name the name attribute. * @param string $value the value attribute. If it is null, the value attribute will not be generated. - * @param array $options the tag options in terms of name-value pairs. These be rendered as + * @param array $options the tag options in terms of name-value pairs. These will be rendered as * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. * If a value is null, the corresponding attribute will not be rendered. * @return string the generated button tag @@ -576,7 +576,7 @@ class Html * Generates a text area input. * @param string $name the input name * @param string $value the input value. Note that it will be encoded using [[encode()]]. - * @param array $options the tag options in terms of name-value pairs. These be rendered as + * @param array $options the tag options in terms of name-value pairs. These will be rendered as * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. * If a value is null, the corresponding attribute will not be rendered. * @return string the generated text area tag diff --git a/framework/widgets/ActiveForm.php b/framework/widgets/ActiveForm.php index 2bac53a..4a0f89c 100644 --- a/framework/widgets/ActiveForm.php +++ b/framework/widgets/ActiveForm.php @@ -7,7 +7,11 @@ namespace yii\widgets; +use Yii; use yii\base\Widget; +use yii\base\Model; +use yii\util\Html; +use yii\util\ArrayHelper; /** * ActiveForm ... @@ -18,8 +22,7 @@ use yii\base\Widget; class ActiveForm extends Widget { /** - * @var mixed the form action URL (see {@link CHtml::normalizeUrl} for details about this parameter). - * If not set, the current page URL is used. + * @param array|string $action the form action URL. This parameter will be processed by [[\yii\util\Html::url()]]. */ public $action = ''; /** @@ -28,49 +31,88 @@ class ActiveForm extends Widget */ public $method = 'post'; /** - * @var string the CSS class name for error messages. Defaults to 'errorMessage'. - * Individual {@link error} call may override this value by specifying the 'class' HTML option. + * @var string the default CSS class for the error summary container. + * @see errorSummary() */ - public $errorMessageCssClass = 'errorMessage'; + public $errorSummaryClass = 'yii-error-summary'; /** - * @var array additional HTML attributes that should be rendered for the form tag. + * @var string the default CSS class that indicates an input has error. + * This is */ - public $htmlOptions = array(); - /** - * @var boolean whether to enable data validation via AJAX. Defaults to false. - * When this property is set true, you should respond to the AJAX validation request on the server side as shown below: - *
    -	 * public function actionCreate()
    -	 * {
    -	 *     $model=new User;
    -	 *     if(isset($_POST['ajax']) && $_POST['ajax']==='user-form')
    -	 *     {
    -	 *         echo CActiveForm::validate($model);
    -	 *         Yii::app()->end();
    -	 *     }
    -	 *     ......
    -	 * }
    -	 * 
    - */ - public $enableAjaxValidation = false; + public $errorClass = 'yii-error'; + public $successClass = 'yii-success'; + public $validatingClass = 'yii-validating'; /** * @var boolean whether to enable client-side data validation. Defaults to false. - * * When this property is set true, client-side validation will be performed by validators * that support it (see {@link CValidator::enableClientValidation} and {@link CValidator::clientValidateAttribute}). - * - * @see error - * @since 1.1.7 */ public $enableClientValidation = false; + public $options = array(); + - public function errorSummary($model, $options = array()) + /** + * @param Model|Model[] $models + * @param array $options + * @return string + */ + public function errorSummary($models, $options = array()) { + if (!is_array($models)) { + $models = array($models); + } + + $showAll = isset($options['showAll']) && $options['showAll']); + $lines = array(); + /** @var $model Model */ + foreach ($models as $model) { + if ($showAll) { + foreach ($model->getErrors() as $errors) { + $lines = array_merge($lines, $errors); + } + } else { + $lines = array_merge($lines, $model->getFirstErrors()); + } + } + + $header = isset($options['header']) ? $options['header'] : '

    ' . Yii::t('yii|Please fix the following errors:') . '

    '; + $footer = isset($options['footer']) ? $options['footer'] : ''; + $container = isset($options['container']) ? $options['container'] : 'div'; + unset($options['showAll'], $options['header'], $options['footer'], $options['container']); + + if (!isset($options['class'])) { + $options['class'] = $this->errorSummaryClass; + } + + if ($lines !== array()) { + $content = "
    • " . implode("
    • \n
    • ", ArrayHelper::htmlEncode($lines)) . "
      • "; + return Html::tag($container, $header . $content . $footer, $options); + } else { + $content = "
          "; + $options['style'] = isset($options['style']) ? rtrim($options['style'], ';') . '; display:none' : 'display:none'; + return Html::tag($container, $header . $content . $footer, $options); + } } + /** + * @param Model $model + * @param string $attribute + * @param array $options + * @return string + */ public function error($model, $attribute, $options = array()) { + self::resolveName($model, $attribute); // turn [a][b]attr into attr + $container = isset($options['container']) ? $options['container'] : 'div'; + unset($options['container']); + $error = $model->getFirstError($attribute); + return Html::tag($container, Html::encode($error), $options); + } + + public function resolveAttributeName($name) + { + } public function label($model, $attribute, $options = array()) From b5be2ccf18f0e504bad2eff6d5828c90f8c85487 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Fri, 15 Mar 2013 17:01:55 -0400 Subject: [PATCH 085/117] activeform WIP --- framework/widgets/ActiveForm.php | 134 ++++++++++++++++++++++++++++++++++----- 1 file changed, 119 insertions(+), 15 deletions(-) diff --git a/framework/widgets/ActiveForm.php b/framework/widgets/ActiveForm.php index 4a0f89c..2d47253 100644 --- a/framework/widgets/ActiveForm.php +++ b/framework/widgets/ActiveForm.php @@ -8,6 +8,7 @@ namespace yii\widgets; use Yii; +use yii\base\InvalidParamException; use yii\base\Widget; use yii\base\Model; use yii\util\Html; @@ -35,6 +36,7 @@ class ActiveForm extends Widget * @see errorSummary() */ public $errorSummaryClass = 'yii-error-summary'; + public $errorMessageClass = 'yii-error-message'; /** * @var string the default CSS class that indicates an input has error. * This is @@ -50,7 +52,10 @@ class ActiveForm extends Widget public $enableClientValidation = false; public $options = array(); - + /** + * @var array model-class mapped to name prefix + */ + public $modelMap; /** * @param Model|Model[] $models @@ -63,7 +68,7 @@ class ActiveForm extends Widget $models = array($models); } - $showAll = isset($options['showAll']) && $options['showAll']); + $showAll = isset($options['showAll']) && $options['showAll']; $lines = array(); /** @var $model Model */ foreach ($models as $model) { @@ -78,20 +83,22 @@ class ActiveForm extends Widget $header = isset($options['header']) ? $options['header'] : '

          ' . Yii::t('yii|Please fix the following errors:') . '

          '; $footer = isset($options['footer']) ? $options['footer'] : ''; - $container = isset($options['container']) ? $options['container'] : 'div'; + $tag = isset($options['tag']) ? $options['tag'] : 'div'; unset($options['showAll'], $options['header'], $options['footer'], $options['container']); if (!isset($options['class'])) { $options['class'] = $this->errorSummaryClass; + } else { + $options['class'] .= ' ' . $this->errorSummaryClass; } if ($lines !== array()) { $content = "
          • " . implode("
          • \n
          • ", ArrayHelper::htmlEncode($lines)) . "
            • "; - return Html::tag($container, $header . $content . $footer, $options); + return Html::tag($tag, $header . $content . $footer, $options); } else { $content = "
                "; $options['style'] = isset($options['style']) ? rtrim($options['style'], ';') . '; display:none' : 'display:none'; - return Html::tag($container, $header . $content . $footer, $options); + return Html::tag($tag, $header . $content . $footer, $options); } } @@ -103,25 +110,32 @@ class ActiveForm extends Widget */ public function error($model, $attribute, $options = array()) { - self::resolveName($model, $attribute); // turn [a][b]attr into attr - $container = isset($options['container']) ? $options['container'] : 'div'; - unset($options['container']); + $attribute = $this->normalizeAttributeName($attribute); + $this->getInputName($model, $attribute); + $tag = isset($options['tag']) ? $options['tag'] : 'div'; + unset($options['tag']); $error = $model->getFirstError($attribute); - return Html::tag($container, Html::encode($error), $options); - } - - public function resolveAttributeName($name) - { - + return Html::tag($tag, Html::encode($error), $options); } + /** + * @param Model $model + * @param string $attribute + * @param array $options + * @return string + */ 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); } public function input($type, $model, $attribute, $options = array()) { - return ''; + $value = $this->getAttributeValue($model, $attribute); + $name = $this->getInputName($model, $attribute); + return Html::input($type, $name, $value, $options); } public function textInput($model, $attribute, $options = array()) @@ -146,29 +160,119 @@ class ActiveForm extends Widget public function textarea($model, $attribute, $options = array()) { + $value = $this->getAttributeValue($model, $attribute); + $name = $this->getInputName($model, $attribute); + return Html::textarea($name, $value, $options); } public function radio($model, $attribute, $value = '1', $options = array()) { + $checked = $this->getAttributeValue($model, $attribute); + $name = $this->getInputName($model, $attribute); + if (!array_key_exists('uncheck', $options)) { + $options['unchecked'] = '0'; + } + return Html::radio($name, $checked, $value, $options); } public function checkbox($model, $attribute, $value = '1', $options = array()) { + $checked = $this->getAttributeValue($model, $attribute); + $name = $this->getInputName($model, $attribute); + if (!array_key_exists('uncheck', $options)) { + $options['unchecked'] = '0'; + } + return Html::checkbox($name, $checked, $value, $options); } public function dropDownList($model, $attribute, $items, $options = array()) { + $checked = $this->getAttributeValue($model, $attribute); + $name = $this->getInputName($model, $attribute); + return Html::dropDownList($name, $checked, $items, $options); } public function listBox($model, $attribute, $items, $options = array()) { + $checked = $this->getAttributeValue($model, $attribute); + $name = $this->getInputName($model, $attribute); + if (!array_key_exists('unselect', $options)) { + $options['unselect'] = '0'; + } + return Html::listBox($name, $checked, $items, $options); } public function checkboxList($model, $attribute, $items, $options = array()) { + $checked = $this->getAttributeValue($model, $attribute); + $name = $this->getInputName($model, $attribute); + if (!array_key_exists('unselect', $options)) { + $options['unselect'] = '0'; + } + return Html::checkboxList($name, $checked, $items, $options); } public function radioList($model, $attribute, $items, $options = array()) { + $checked = $this->getAttributeValue($model, $attribute); + $name = $this->getInputName($model, $attribute); + if (!array_key_exists('unselect', $options)) { + $options['unselect'] = '0'; + } + return Html::radioList($name, $checked, $items, $options); + } + + public function getInputName($model, $attribute) + { + $class = get_class($model); + if (isset($this->modelMap[$class])) { + $class = $this->modelMap[$class]; + } elseif (($pos = strrpos($class, '\\')) !== false) { + $class = substr($class, $pos); + } + if (!preg_match('/(^|.*\])(\w+)(\[.*|$)/', $attribute, $matches)) { + throw new InvalidParamException('Attribute name must contain word characters only.'); + } + $prefix = $matches[1]; + $attribute = $matches[2]; + $suffix = $matches[3]; + if ($class === '' && $prefix === '') { + return $attribute . $suffix; + } elseif ($class !== '') { + return $class . $prefix . "[$attribute]" . $suffix; + } else { + throw new InvalidParamException('Model name cannot be mapped to empty for tabular inputs.'); + } + } + + public function getAttributeValue($model, $attribute) + { + if (!preg_match('/(^|.*\])(\w+)(\[.*|$)/', $attribute, $matches)) { + throw new InvalidParamException('Attribute name must contain word characters only.'); + } + $attribute = $matches[2]; + $index = $matches[3]; + if ($index === '') { + return $model->$attribute; + } else { + $value = $model->$attribute; + foreach (explode('][', trim($index, '[]')) as $id) { + if ((is_array($value) || $value instanceof \ArrayAccess) && isset($value[$id])) { + $value = $value[$id]; + } else { + return null; + } + } + return $value; + } + } + + public function normalizeAttributeName($attribute) + { + if (preg_match('/(^|.*\])(\w+)(\[.*|$)/', $attribute, $matches)) { + return $matches[2]; + } else { + throw new InvalidParamException('Attribute name must contain word characters only.'); + } } } From b2b9b4f319551f31d1c41d43fa93635d4f8ac298 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Sat, 16 Mar 2013 10:18:44 -0400 Subject: [PATCH 086/117] test wip --- framework/test/TestCase.php | 4 ++-- framework/test/WebTestCase.php | 25 +++++++++++++++++++++++++ framework/widgets/ActiveForm.php | 2 +- 3 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 framework/test/WebTestCase.php diff --git a/framework/test/TestCase.php b/framework/test/TestCase.php index 3fd92c0..f190e5a 100644 --- a/framework/test/TestCase.php +++ b/framework/test/TestCase.php @@ -10,9 +10,9 @@ namespace yii\test; require_once('PHPUnit/Runner/Version.php'); -spl_autoload_unregister(array('YiiBase','autoload')); +spl_autoload_unregister(array('Yii','autoload')); require_once('PHPUnit/Autoload.php'); -spl_autoload_register(array('YiiBase','autoload')); // put yii's autoloader at the end +spl_autoload_register(array('Yii','autoload')); // put yii's autoloader at the end /** * TestCase is the base class for all test case classes. diff --git a/framework/test/WebTestCase.php b/framework/test/WebTestCase.php new file mode 100644 index 0000000..39162c9 --- /dev/null +++ b/framework/test/WebTestCase.php @@ -0,0 +1,25 @@ + + * @since 2.0 + */ +abstract class WebTestCase extends \PHPUnit_Extensions_SeleniumTestCase +{ +} diff --git a/framework/widgets/ActiveForm.php b/framework/widgets/ActiveForm.php index 4a0f89c..b8c8f2c 100644 --- a/framework/widgets/ActiveForm.php +++ b/framework/widgets/ActiveForm.php @@ -63,7 +63,7 @@ class ActiveForm extends Widget $models = array($models); } - $showAll = isset($options['showAll']) && $options['showAll']); + $showAll = isset($options['showAll']) && $options['showAll']; $lines = array(); /** @var $model Model */ foreach ($models as $model) { From 3b610926f4eb569ea3992f5ed2a8b3e65b615a01 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Sat, 16 Mar 2013 10:21:15 -0400 Subject: [PATCH 087/117] Added web test folders. --- tests/web/app/assets/.gitignore | 1 + tests/web/app/protected/runtime/.gitignore | 1 + 2 files changed, 2 insertions(+) create mode 100644 tests/web/app/assets/.gitignore create mode 100644 tests/web/app/protected/runtime/.gitignore diff --git a/tests/web/app/assets/.gitignore b/tests/web/app/assets/.gitignore new file mode 100644 index 0000000..72e8ffc --- /dev/null +++ b/tests/web/app/assets/.gitignore @@ -0,0 +1 @@ +* diff --git a/tests/web/app/protected/runtime/.gitignore b/tests/web/app/protected/runtime/.gitignore new file mode 100644 index 0000000..72e8ffc --- /dev/null +++ b/tests/web/app/protected/runtime/.gitignore @@ -0,0 +1 @@ +* From 9a4f4f85d26ad796bdb6440fd895240f860a2e8d Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Mon, 18 Mar 2013 08:42:12 -0400 Subject: [PATCH 088/117] wip --- tests/web/app/index.php | 6 +++++ tests/web/app/protected/config/main.php | 3 +++ .../app/protected/controllers/SiteController.php | 30 ++++++++++++++++++++++ tests/web/app/protected/views/site/index.php | 8 ++++++ 4 files changed, 47 insertions(+) create mode 100644 tests/web/app/index.php create mode 100644 tests/web/app/protected/config/main.php create mode 100644 tests/web/app/protected/controllers/SiteController.php create mode 100644 tests/web/app/protected/views/site/index.php diff --git a/tests/web/app/index.php b/tests/web/app/index.php new file mode 100644 index 0000000..4cfa1ab --- /dev/null +++ b/tests/web/app/index.php @@ -0,0 +1,6 @@ +run(); diff --git a/tests/web/app/protected/config/main.php b/tests/web/app/protected/config/main.php new file mode 100644 index 0000000..eed6d54 --- /dev/null +++ b/tests/web/app/protected/config/main.php @@ -0,0 +1,3 @@ + 'item 1', + 'value 2' => 'item 2', + 'value 3' => 'item 3', + ), isset($_POST['test']) ? $_POST['test'] : null, + function ($index, $label, $name, $value, $checked) { + return Html::label( + $label . ' ' . Html::checkbox($name, $value, $checked), + null, array('class' => 'inline checkbox') + ); + }); + echo Html::submitButton(); + echo Html::endForm(); + print_r($_POST); + } +} \ No newline at end of file diff --git a/tests/web/app/protected/views/site/index.php b/tests/web/app/protected/views/site/index.php new file mode 100644 index 0000000..5decb56 --- /dev/null +++ b/tests/web/app/protected/views/site/index.php @@ -0,0 +1,8 @@ + Date: Wed, 20 Mar 2013 22:03:13 -0400 Subject: [PATCH 089/117] refactored View. --- framework/YiiBase.php | 46 +- framework/base/Application.php | 44 +- framework/base/Controller.php | 144 +++++- framework/base/ErrorHandler.php | 9 +- framework/base/Theme.php | 9 +- framework/base/View.php | 509 +++++++-------------- framework/base/Widget.php | 77 +++- framework/caching/FileCache.php | 3 - .../console/controllers/MigrateController.php | 2 +- framework/util/FileHelper.php | 2 +- framework/views/error.php | 10 +- framework/views/exception.php | 16 +- framework/web/Session.php | 2 +- 13 files changed, 419 insertions(+), 454 deletions(-) diff --git a/framework/YiiBase.php b/framework/YiiBase.php index 392dd1c..678856c 100644 --- a/framework/YiiBase.php +++ b/framework/YiiBase.php @@ -4,9 +4,9 @@ * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ - use yii\base\Exception; use yii\base\InvalidConfigException; +use yii\base\InvalidParamException; use yii\logging\Logger; /** @@ -94,7 +94,7 @@ class YiiBase */ public static $objectConfig = array(); - private static $_imported = array(); // alias => class name or directory + private static $_imported = array(); // alias => class name or directory private static $_logger; /** @@ -159,9 +159,7 @@ class YiiBase return self::$_imported[$alias] = $className; } - if (($path = static::getAlias(dirname($alias))) === false) { - throw new Exception('Invalid path alias: ' . $alias); - } + $path = static::getAlias(dirname($alias)); if ($isClass) { if ($forceInclude) { @@ -191,24 +189,30 @@ class YiiBase * * Note, this method does not ensure the existence of the resulting path. * @param string $alias alias + * @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. * @see setAlias */ - public static function getAlias($alias) + public static function getAlias($alias, $throwException = true) { - if (!is_string($alias)) { - return false; - } elseif (isset(self::$aliases[$alias])) { - return self::$aliases[$alias]; - } elseif ($alias === '' || $alias[0] !== '@') { // not an alias - return $alias; - } elseif (($pos = strpos($alias, '/')) !== false) { - $rootAlias = substr($alias, 0, $pos); - if (isset(self::$aliases[$rootAlias])) { - return self::$aliases[$alias] = self::$aliases[$rootAlias] . substr($alias, $pos); + if (is_string($alias)) { + if (isset(self::$aliases[$alias])) { + return self::$aliases[$alias]; + } elseif ($alias === '' || $alias[0] !== '@') { // not an alias + return $alias; + } elseif (($pos = strpos($alias, '/')) !== false || ($pos = strpos($alias, '\\')) !== false) { + $rootAlias = substr($alias, 0, $pos); + if (isset(self::$aliases[$rootAlias])) { + return self::$aliases[$alias] = self::$aliases[$rootAlias] . substr($alias, $pos); + } } } - return false; + if ($throwException) { + throw new InvalidParamException("Invalid path alias: $alias"); + } else { + return false; + } } /** @@ -236,10 +240,8 @@ class YiiBase unset(self::$aliases[$alias]); } elseif ($path[0] !== '@') { self::$aliases[$alias] = rtrim($path, '\\/'); - } elseif (($p = static::getAlias($path)) !== false) { - self::$aliases[$alias] = $p; } else { - throw new Exception('Invalid path: ' . $path); + self::$aliases[$alias] = static::getAlias($path); } } @@ -273,14 +275,14 @@ class YiiBase // namespaced class, e.g. yii\base\Component // convert namespace to path alias, e.g. yii\base\Component to @yii/base/Component $alias = '@' . str_replace('\\', '/', ltrim($className, '\\')); - if (($path = static::getAlias($alias)) !== false) { + if (($path = static::getAlias($alias, false)) !== false) { $classFile = $path . '.php'; } } elseif (($pos = strpos($className, '_')) !== false) { // PEAR-styled class, e.g. PHPUnit_Framework_TestCase // convert class name to path alias, e.g. PHPUnit_Framework_TestCase to @PHPUnit/Framework/TestCase $alias = '@' . str_replace('_', '/', $className); - if (($path = static::getAlias($alias)) !== false) { + if (($path = static::getAlias($alias, false)) !== false) { $classFile = $path . '.php'; } } diff --git a/framework/base/Application.php b/framework/base/Application.php index a60bf90..bbc4601 100644 --- a/framework/base/Application.php +++ b/framework/base/Application.php @@ -160,28 +160,28 @@ class Application extends Module */ public function handleFatalError() { - if(YII_ENABLE_ERROR_HANDLER) { + if (YII_ENABLE_ERROR_HANDLER) { $error = error_get_last(); - if(ErrorException::isFatalErorr($error)) { + if (ErrorException::isFatalErorr($error)) { unset($this->_memoryReserve); $exception = new ErrorException($error['message'], $error['type'], $error['type'], $error['file'], $error['line']); - if(function_exists('xdebug_get_function_stack')) { + if (function_exists('xdebug_get_function_stack')) { $trace = array_slice(array_reverse(xdebug_get_function_stack()), 4, -1); - foreach($trace as &$frame) { - if(!isset($frame['function'])) { + foreach ($trace as &$frame) { + if (!isset($frame['function'])) { $frame['function'] = 'unknown'; } // XDebug < 2.1.1: http://bugs.xdebug.org/view.php?id=695 - if(!isset($frame['type'])) { + if (!isset($frame['type'])) { $frame['type'] = '::'; } // XDebug has a different key name $frame['args'] = array(); - if(isset($frame['params']) && !isset($frame['args'])) { + if (isset($frame['params']) && !isset($frame['args'])) { $frame['args'] = $frame['params']; } } @@ -214,8 +214,8 @@ class Application extends Module $this->beforeRequest(); // Allocating twice more than required to display memory exhausted error // in case of trying to allocate last 1 byte while all memory is taken. - $this->_memoryReserve = str_repeat('x', 1024*256); - register_shutdown_function(array($this,'end'),0,false); + $this->_memoryReserve = str_repeat('x', 1024 * 256); + register_shutdown_function(array($this, 'end'), 0, false); $status = $this->processRequest(); $this->afterRequest(); return $status; @@ -346,15 +346,6 @@ class Application extends Module } /** - * Returns the application theme. - * @return Theme the theme that this application is currently using. - */ - public function getTheme() - { - return $this->getComponent('theme'); - } - - /** * Returns the cache component. * @return \yii\caching\Cache the cache application component. Null if the component is not enabled. */ @@ -373,12 +364,12 @@ class Application extends Module } /** - * Returns the view renderer. - * @return ViewRenderer the view renderer used by this application. + * Returns the view object. + * @return View the view object that is used to render various view files. */ - public function getViewRenderer() + public function getView() { - return $this->getComponent('viewRenderer'); + return $this->getComponent('view'); } /** @@ -423,6 +414,9 @@ class Application extends Module 'urlManager' => array( 'class' => 'yii\web\UrlManager', ), + 'view' => array( + 'class' => 'yii\base\View', + ), )); } @@ -446,8 +440,8 @@ class Application extends Module // in case error appeared in __toString method we can't throw any exception $trace = debug_backtrace(false); array_shift($trace); - foreach($trace as $frame) { - if($frame['function'] == '__toString') { + foreach ($trace as $frame) { + if ($frame['function'] == '__toString') { $this->handleException($exception); } } @@ -481,7 +475,7 @@ class Application extends Module $this->end(1); - } catch(\Exception $e) { + } catch (\Exception $e) { // exception could be thrown in end() or ErrorHandler::handle() $msg = (string)$e; $msg .= "\nPrevious exception:\n"; diff --git a/framework/base/Controller.php b/framework/base/Controller.php index 65200bb..8840cca 100644 --- a/framework/base/Controller.php +++ b/framework/base/Controller.php @@ -8,6 +8,7 @@ namespace yii\base; use Yii; +use yii\util\FileHelper; use yii\util\StringHelper; /** @@ -295,34 +296,42 @@ class Controller extends Component /** * Renders a view and applies layout if available. - * - * @param $view - * @param array $params - * @return string + * @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. + * @return string the rendering result. + * @throws InvalidParamException if the view file or the layout file does not exist. */ public function render($view, $params = array()) { - return $this->createView()->render($view, $params); - } - - public function renderContent($content) - { - return $this->createView()->renderContent($content); + $viewFile = $this->findViewFile($view); + $layoutFile = $this->findLayoutFile(); + return Yii::$app->getView()->render($this, $viewFile, $params, $layoutFile); } + /** + * 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 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 $this->createView()->renderPartial($view, $params); + return $this->renderFile($this->findViewFile($view), $params); } + /** + * Renders a view file. + * @param string $file the view file to be rendered. This can be either a file path or a path alias. + * @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 renderFile($file, $params = array()) { - return $this->createView()->renderFile($file, $params); - } - - public function createView() - { - return new View($this); + return Yii::$app->getView()->render($this, $file, $params); } /** @@ -335,4 +344,105 @@ class Controller extends Component { return $this->module->getViewPath() . DIRECTORY_SEPARATOR . $this->id; } + + /** + * 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 [[viewPath]]. + * + * If the view name does not contain a file extension, it will use the default one `.php`. + * + * @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 + */ + protected function findViewFile($view) + { + if (strncmp($view, '@', 1) === 0) { + // e.g. "@app/views/common" + $file = Yii::getAlias($view); + } elseif (strncmp($view, '/', 1) !== 0) { + // e.g. "index" + $file = $this->getViewPath() . DIRECTORY_SEPARATOR . $view; + } elseif (strncmp($view, '//', 2) !== 0) { + // e.g. "/site/index" + $file = $this->module->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); + } else { + // e.g. "//layouts/main" + $file = Yii::$app->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); + } + if (FileHelper::getExtension($file) === '') { + $file .= '.php'; + } + return $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. + * @throws InvalidParamException if an invalid path alias is used to specify the layout + */ + protected function findLayoutFile() + { + /** @var $module Module */ + if (is_string($this->layout)) { + $module = $this->module; + $view = $this->layout; + } elseif ($this->layout === null) { + $module = $this->module; + while ($module !== null && $module->layout === null) { + $module = $module->module; + } + if ($module !== null && is_string($module->layout)) { + $view = $module->layout; + } + } + + if (!isset($view)) { + return false; + } + + if (strncmp($view, '@', 1) === 0) { + $file = Yii::getAlias($view); + } elseif (strncmp($view, '/', 1) === 0) { + $file = Yii::$app->getLayoutPath() . DIRECTORY_SEPARATOR . $view; + } else { + $file = $module->getLayoutPath() . DIRECTORY_SEPARATOR . $view; + } + + if (FileHelper::getExtension($file) === '') { + $file .= '.php'; + } + return $file; + } } diff --git a/framework/base/ErrorHandler.php b/framework/base/ErrorHandler.php index 40ae6d8..996fa18 100644 --- a/framework/base/ErrorHandler.php +++ b/framework/base/ErrorHandler.php @@ -253,14 +253,9 @@ class ErrorHandler extends Component */ public function renderAsHtml($exception) { - $view = new View($this); - if (!YII_DEBUG || $exception instanceof UserException) { - $viewName = $this->errorView; - } else { - $viewName = $this->exceptionView; - } + $view = new View; $name = !YII_DEBUG || $exception instanceof HttpException ? $this->errorView : $this->exceptionView; - echo $view->render($name, array( + echo $view->render($this, $name, array( 'exception' => $exception, )); } diff --git a/framework/base/Theme.php b/framework/base/Theme.php index 61487d8..c5fd925 100644 --- a/framework/base/Theme.php +++ b/framework/base/Theme.php @@ -40,7 +40,8 @@ class Theme extends Component /** * @var array the mapping between view directories and their corresponding themed versions. * If not set, it will be initialized as a mapping from [[Application::basePath]] to [[basePath]]. - * This property is used by [[apply()]] when a view is trying to apply the theme. + * This property is used by [[applyTo()]] when a view is trying to apply the theme. + * Path aliases can be used when specifying directories. */ public $pathMap; @@ -63,7 +64,9 @@ class Theme extends Component } $paths = array(); foreach ($this->pathMap as $from => $to) { - $paths[FileHelper::normalizePath($from) . DIRECTORY_SEPARATOR] = FileHelper::normalizePath($to) . DIRECTORY_SEPARATOR; + $from = FileHelper::normalizePath(Yii::getAlias($from)); + $to = FileHelper::normalizePath(Yii::getAlias($to)); + $paths[$from . DIRECTORY_SEPARATOR] = $to . DIRECTORY_SEPARATOR; } $this->pathMap = $paths; } @@ -93,7 +96,7 @@ class Theme extends Component * @param string $path the file to be themed * @return string the themed file, or the original file if the themed version is not available. */ - public function apply($path) + public function applyTo($path) { $path = FileHelper::normalizePath($path); foreach ($this->pathMap as $from => $to) { diff --git a/framework/base/View.php b/framework/base/View.php index 2b2e7d1..75a911d 100644 --- a/framework/base/View.php +++ b/framework/base/View.php @@ -8,14 +8,14 @@ namespace yii\base; use Yii; -use yii\util\FileHelper; use yii\base\Application; +use yii\util\FileHelper; /** * View represents a view object in the MVC pattern. - * + * * View provides a set of methods (e.g. [[render()]]) for rendering purpose. - * + * * @author Qiang Xue * @since 2.0 */ @@ -24,133 +24,112 @@ class View extends Component /** * @var object the object that owns this view. This can be a controller, a widget, or any other object. */ - public $owner; - /** - * @var string the layout to be applied when [[render()]] or [[renderContent()]] is called. - * If not set, it will use the [[Module::layout]] of the currently active module. - */ - public $layout; - /** - * @var string the language that the view should be rendered in. If not set, it will use - * the value of [[Application::language]]. - */ - public $language; + public $context; /** - * @var string the language that the original view is in. If not set, it will use - * the value of [[Application::sourceLanguage]]. + * @var mixed custom parameters that are shared among view templates. */ - public $sourceLanguage; - /** - * @var boolean whether to localize the view when possible. Defaults to true. - * Note that when this is true, if a localized view cannot be found, the original view will be rendered. - * No error will be reported. - */ - public $enableI18N = true; + public $params; /** - * @var boolean whether to theme the view when possible. Defaults to true. - * Note that theming will be disabled if [[Application::theme]] is not set. + * @var ViewRenderer|array the view renderer object or the configuration array for + * creating the view renderer. If not set, view files will be treated as normal PHP files. */ - public $enableTheme = true; + public $renderer; /** - * @var mixed custom parameters that are available in the view template + * @var Theme|array the theme object or the configuration array for creating the theme. + * If not set, it means theming is not enabled. */ - public $params; + public $theme; /** * @var Widget[] the widgets that are currently not ended */ - private $_widgetStack = array(); - - /** - * Constructor. - * @param object $owner the owner of this view. This usually is a controller or a 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); - } + private $_widgetStack = array(); - /** - * Renders a view within a layout. - * This method is similar to [[renderPartial()]] except that if a layout is available, - * this method will embed the view result into the layout and then return it. - * @param string $view the view to be rendered. Please refer to [[findViewFile()]] on possible formats of the view name. - * @param array $params the parameters that should be made available in the view. The PHP function `extract()` - * will be called on this variable to extract the variables from this parameter. - * @return string the rendering result - * @throws InvalidConfigException if the view file or layout file cannot be found - * @see findViewFile() - * @see findLayoutFile() - */ - public function render($view, $params = array()) - { - $content = $this->renderPartial($view, $params); - return $this->renderContent($content); - } /** - * Renders a text content within a layout. - * The layout being used is resolved by [[findLayout()]]. - * If no layout is available, the content will be returned back. - * @param string $content the content to be rendered - * @return string the rendering result - * @throws InvalidConfigException if the layout file cannot be found - * @see findLayoutFile() + * Initializes the view component. */ - public function renderContent($content) + public function init() { - $layoutFile = $this->findLayoutFile(); - if ($layoutFile !== false) { - return $this->renderFile($layoutFile, array('content' => $content)); - } else { - return $content; + parent::init(); + if (is_array($this->renderer)) { + $this->renderer = Yii::createObject($this->renderer); + } + if (is_array($this->theme)) { + $this->theme = Yii::createObject($this->theme); } } /** - * Renders a view. + * Renders a view file under a context with an optional layout. + * + * This method is similar to [[renderFile()]] except that it will update [[context]] + * with the provided $context parameter. It will also apply layout to the rendering result + * of the view file if $layoutFile is given. * - * The method first finds the actual view file corresponding to the specified view. - * It then calls [[renderFile()]] to render the view file. The rendering result is returned - * as a string. If the view file does not exist, an exception will be thrown. + * Theming and localization will be performed for the view file and the layout file, if possible. * - * @param string $view the view to be rendered. Please refer to [[findViewFile()]] on possible formats of the view name. - * @param array $params the parameters that should be made available in the view. The PHP function `extract()` - * will be called on this variable to extract the variables from this parameter. + * @param object $context the context object for rendering the file. This could be a controller, a widget, + * or any other object that serves as the rendering context of the view file. In the view file, + * it can be accessed through the [[context]] property. + * @param string $viewFile the view file. This can be a file path or a path alias. + * @param array $params the parameters (name-value pairs) that will be extracted and made available in the view file. + * @param string|boolean $layoutFile the layout file. This can be a file path or a path alias. + * If it is false, it means no layout should be applied. * @return string the rendering result - * @throws InvalidParamException if the view file cannot be found - * @see findViewFile() + * @throws InvalidParamException if the view file or the layout file does not exist. */ - public function renderPartial($view, $params = array()) + public function render($context, $viewFile, $params = array(), $layoutFile = false) { - $file = $this->findViewFile($view); - if ($file !== false) { - return $this->renderFile($file, $params); - } else { - throw new InvalidParamException("Unable to find the view file for view '$view'."); + $oldContext = $this->context; + $this->context = $context; + + $content = $this->renderFile($viewFile, $params); + + if ($layoutFile !== false) { + $content = $this->renderFile($layoutFile, array('content' => $content)); } + + $this->context = $oldContext; + + return $content; } /** * Renders a view file. * - * If a [[ViewRenderer|view renderer]] is installed, this method will try to use the view renderer - * to render the view file. Otherwise, it will simply include the view file, capture its output - * and return it as a string. + * This method renders the specified view file under the existing [[context]]. + * + * If [[theme]] is enabled (not null), it will try to render the themed version of the view file as long + * as it is available. * - * @param string $file the view file. + * The method will call [[FileHelper::localize()]] to localize the view file. + * + * If [[renderer]] is enabled (not null), the method will use it to render the view file. + * Otherwise, it will simply include the view file as a normal PHP file, capture its output and + * return it as a string. + * + * @param string $viewFile the view file. This can be a file path or a path alias. * @param array $params the parameters (name-value pairs) that will be extracted and made available in the view file. * @return string the rendering result + * @throws InvalidParamException if the view file does not exist */ - public function renderFile($file, $params = array()) + public function renderFile($viewFile, $params = array()) { - $renderer = Yii::$app->getViewRenderer(); - if ($renderer !== null) { - return $renderer->render($this, $file, $params); + $viewFile = Yii::getAlias($viewFile); + if (is_file($viewFile)) { + if ($this->theme !== null) { + $viewFile = $this->theme->applyTo($viewFile); + } + $viewFile = FileHelper::localize($viewFile); + } else { + throw new InvalidParamException("The view file does not exist: $viewFile"); + } + + if ($this->renderer !== null) { + return $this->renderer->render($this, $viewFile, $params); } else { - return $this->renderPhpFile($file, $params); + return $this->renderPhpFile($viewFile, $params); } } @@ -161,6 +140,8 @@ class View extends Component * It extracts the given parameters and makes them available in the view file. * The method captures the output of the included view file and returns it as a string. * + * This method should mainly be called by view renderer or [[renderFile()]]. + * * @param string $_file_ the view file. * @param array $_params_ the parameters (name-value pairs) that will be extracted and made available in the view file. * @return string the rendering result @@ -184,7 +165,7 @@ class View extends Component public function createWidget($class, $properties = array()) { $properties['class'] = $class; - return Yii::createObject($properties, $this->owner); + return Yii::createObject($properties, $this->context); } /** @@ -233,7 +214,7 @@ class View extends Component * If you want to capture the rendering result of a widget, you may use * [[createWidget()]] and [[Widget::run()]]. * @return Widget the widget instance - * @throws Exception if [[beginWidget()]] and [[endWidget()]] calls are not properly nested + * @throws InvalidCallException if [[beginWidget()]] and [[endWidget()]] calls are not properly nested */ public function endWidget() { @@ -242,251 +223,99 @@ class View extends Component $widget->run(); return $widget; } else { - throw new Exception("Unmatched beginWidget() and endWidget() calls."); - } - } -// -// /** -// * Begins recording a clip. -// * This method is a shortcut to beginning [[yii\widgets\Clip]] -// * @param string $id the clip ID. -// * @param array $properties initial property values for [[yii\widgets\Clip]] -// */ -// public function beginClip($id, $properties = array()) -// { -// $properties['id'] = $id; -// $this->beginWidget('yii\widgets\Clip', $properties); -// } -// -// /** -// * Ends recording a clip. -// */ -// public function endClip() -// { -// $this->endWidget(); -// } -// -// /** -// * Begins fragment caching. -// * This method will display cached content if it is available. -// * If not, it will start caching and would expect an [[endCache()]] -// * call to end the cache and save the content into cache. -// * A typical usage of fragment caching is as follows, -// * -// * ~~~ -// * if($this->beginCache($id)) { -// * // ...generate content here -// * $this->endCache(); -// * } -// * ~~~ -// * -// * @param string $id a unique ID identifying the fragment to be cached. -// * @param array $properties initial property values for [[yii\widgets\OutputCache]] -// * @return boolean whether we need to generate content for caching. False if cached version is available. -// * @see endCache -// */ -// public function beginCache($id, $properties = array()) -// { -// $properties['id'] = $id; -// $cache = $this->beginWidget('yii\widgets\OutputCache', $properties); -// if ($cache->getIsContentCached()) { -// $this->endCache(); -// return false; -// } else { -// return true; -// } -// } -// -// /** -// * Ends fragment caching. -// * This is an alias to [[endWidget()]] -// * @see beginCache -// */ -// public function endCache() -// { -// $this->endWidget(); -// } -// -// /** -// * Begins the rendering of content that is to be decorated by the specified view. -// * @param mixed $view the name of the view that will be used to decorate the content. The actual view script -// * is resolved via {@link getViewFile}. If this parameter is null (default), -// * the default layout will be used as the decorative view. -// * Note that if the current controller does not belong to -// * any module, the default layout refers to the application's {@link CWebApplication::layout default layout}; -// * If the controller belongs to a module, the default layout refers to the module's -// * {@link CWebModule::layout default layout}. -// * @param array $params the variables (name=>value) to be extracted and made available in the decorative view. -// * @see endContent -// * @see yii\widgets\ContentDecorator -// */ -// public function beginContent($view, $params = array()) -// { -// $this->beginWidget('yii\widgets\ContentDecorator', array( -// 'view' => $view, -// 'params' => $params, -// )); -// } -// -// /** -// * Ends the rendering of content. -// * @see beginContent -// */ -// public function endContent() -// { -// $this->endWidget(); -// } - - /** - * 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 the [[owner]]'s view path. - * If [[owner]] is a widget or a controller, its view path is given by their `viewPath` property. - * If [[owner]] is an object of any other type, its view path is the `view` sub-directory of the directory - * containing the owner class file. - * - * If the view name does not contain a file extension, it will default to `.php`. - * - * If [[enableTheme]] is true and there is an active application them, the method will also - * attempt to use a themed version of the view file, when available. - * - * And if [[enableI18N]] is true, the method will attempt to use a translated version of the view file, - * when available. - * - * @param string $view the view name or path alias. If the view name does not specify - * the view file extension name, it will use `.php` as the extension name. - * @return string the view file path if it exists. False if the view file cannot be found. - * @throws InvalidConfigException if the view file does not exist - */ - public function findViewFile($view) - { - if (FileHelper::getExtension($view) === '') { - $view .= '.php'; - } - if (strncmp($view, '@', 1) === 0) { - // e.g. "@app/views/common" - if (($file = Yii::getAlias($view)) === false) { - throw new InvalidConfigException("Invalid path alias: $view"); - } - } elseif (strncmp($view, '/', 1) !== 0) { - // e.g. "index" - if ($this->owner instanceof Controller || $this->owner instanceof Widget) { - $file = $this->owner->getViewPath() . DIRECTORY_SEPARATOR . $view; - } elseif ($this->owner !== null) { - $class = new \ReflectionClass($this->owner); - $file = dirname($class->getFileName()) . DIRECTORY_SEPARATOR . 'views' . DIRECTORY_SEPARATOR . $view; - } else { - $file = Yii::$app->getViewPath() . DIRECTORY_SEPARATOR . $view; - } - } elseif (strncmp($view, '//', 2) !== 0 && Yii::$app->controller !== null) { - // e.g. "/site/index" - $file = Yii::$app->controller->module->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); - } else { - // e.g. "//layouts/main" - $file = Yii::$app->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); - } - - if (is_file($file)) { - if ($this->enableTheme && ($theme = Yii::$app->getTheme()) !== null) { - $file = $theme->apply($file); - } - return $this->enableI18N ? FileHelper::localize($file, $this->language, $this->sourceLanguage) : $file; - } else { - throw new InvalidConfigException("View file for view '$view' does not exist: $file"); + throw new InvalidCallException("Unmatched beginWidget() and endWidget() calls."); } } - /** - * Finds the layout file that can be applied to the view. - * - * The applicable layout is resolved according to the following rules: - * - * - If [[layout]] is specified as a string, use it as the layout name and search for the layout file - * under the layout path of the currently active module; - * - If [[layout]] is null and [[owner]] is a controller: - * * If the controller's [[Controller::layout|layout]] is a string, use it as the layout name - * and search for the layout file under the layout path of the parent module of the controller; - * * If the controller's [[Controller::layout|layout]] is null, look through its ancestor modules - * and find the first one whose [[Module::layout|layout]] is not null. Use the layout specified - * by that module; - * - Returns false for all other cases. - * - * Like view names, a layout name can take several formats: - * - * - path alias (e.g. "@app/views/layouts/main"); - * - 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; - * - relative path (e.g. "main"): the actual layout layout file will be looked for under the - * [[Module::viewPath|view path]] of the context module determined by the above layout resolution process. - * - * If the layout name does not contain a file extension, it will default to `.php`. - * - * If [[enableTheme]] is true and there is an active application them, the method will also - * attempt to use a themed version of the layout file, when available. - * - * And if [[enableI18N]] is true, the method will attempt to use a translated version of the layout file, - * when available. - * - * @return string|boolean the layout file path, or false if layout is not needed. - * @throws InvalidConfigException if the layout file cannot be found - */ - public function findLayoutFile() - { - /** @var $module Module */ - if (is_string($this->layout)) { - if (Yii::$app->controller) { - $module = Yii::$app->controller->module; - } else { - $module = Yii::$app; - } - $view = $this->layout; - } elseif ($this->owner instanceof Controller) { - if (is_string($this->owner->layout)) { - $module = $this->owner->module; - $view = $this->owner->layout; - } elseif ($this->owner->layout === null) { - $module = $this->owner->module; - while ($module !== null && $module->layout === null) { - $module = $module->module; - } - if ($module !== null && is_string($module->layout)) { - $view = $module->layout; - } - } - } - - if (!isset($view)) { - return false; - } - - if (FileHelper::getExtension($view) === '') { - $view .= '.php'; - } - if (strncmp($view, '@', 1) === 0) { - if (($file = Yii::getAlias($view)) === false) { - throw new InvalidConfigException("Invalid path alias: $view"); - } - } elseif (strncmp($view, '/', 1) === 0) { - $file = Yii::$app->getLayoutPath() . DIRECTORY_SEPARATOR . $view; - } else { - $file = $module->getLayoutPath() . DIRECTORY_SEPARATOR . $view; - } - - if (is_file($file)) { - if ($this->enableTheme && ($theme = Yii::$app->getTheme()) !== null) { - $file = $theme->apply($file); - } - return $this->enableI18N ? FileHelper::localize($file, $this->language, $this->sourceLanguage) : $file; - } else { - throw new InvalidConfigException("Layout file for layout '$view' does not exist: $file"); - } - } + // + // /** + // * Begins recording a clip. + // * This method is a shortcut to beginning [[yii\widgets\Clip]] + // * @param string $id the clip ID. + // * @param array $properties initial property values for [[yii\widgets\Clip]] + // */ + // public function beginClip($id, $properties = array()) + // { + // $properties['id'] = $id; + // $this->beginWidget('yii\widgets\Clip', $properties); + // } + // + // /** + // * Ends recording a clip. + // */ + // public function endClip() + // { + // $this->endWidget(); + // } + // + // /** + // * Begins fragment caching. + // * This method will display cached content if it is available. + // * If not, it will start caching and would expect an [[endCache()]] + // * call to end the cache and save the content into cache. + // * A typical usage of fragment caching is as follows, + // * + // * ~~~ + // * if($this->beginCache($id)) { + // * // ...generate content here + // * $this->endCache(); + // * } + // * ~~~ + // * + // * @param string $id a unique ID identifying the fragment to be cached. + // * @param array $properties initial property values for [[yii\widgets\OutputCache]] + // * @return boolean whether we need to generate content for caching. False if cached version is available. + // * @see endCache + // */ + // public function beginCache($id, $properties = array()) + // { + // $properties['id'] = $id; + // $cache = $this->beginWidget('yii\widgets\OutputCache', $properties); + // if ($cache->getIsContentCached()) { + // $this->endCache(); + // return false; + // } else { + // return true; + // } + // } + // + // /** + // * Ends fragment caching. + // * This is an alias to [[endWidget()]] + // * @see beginCache + // */ + // public function endCache() + // { + // $this->endWidget(); + // } + // + // /** + // * Begins the rendering of content that is to be decorated by the specified view. + // * @param mixed $view the name of the view that will be used to decorate the content. The actual view script + // * is resolved via {@link getViewFile}. If this parameter is null (default), + // * the default layout will be used as the decorative view. + // * Note that if the current controller does not belong to + // * any module, the default layout refers to the application's {@link CWebApplication::layout default layout}; + // * If the controller belongs to a module, the default layout refers to the module's + // * {@link CWebModule::layout default layout}. + // * @param array $params the variables (name=>value) to be extracted and made available in the decorative view. + // * @see endContent + // * @see yii\widgets\ContentDecorator + // */ + // public function beginContent($view, $params = array()) + // { + // $this->beginWidget('yii\widgets\ContentDecorator', array( + // 'view' => $view, + // 'params' => $params, + // )); + // } + // + // /** + // * Ends the rendering of content. + // * @see beginContent + // */ + // public function endContent() + // { + // $this->endWidget(); + // } } \ No newline at end of file diff --git a/framework/base/Widget.php b/framework/base/Widget.php index 2f8264c..a9fe092 100644 --- a/framework/base/Widget.php +++ b/framework/base/Widget.php @@ -7,6 +7,9 @@ namespace yii\base; +use Yii; +use yii\util\FileHelper; + /** * Widget is the base class for widgets. * @@ -70,35 +73,27 @@ class Widget extends Component /** * Renders a view. - * - * The method first finds the actual view file corresponding to the specified view. - * It then calls [[renderFile()]] to render the view file. The rendering result is returned - * as a string. If the view file does not exist, an exception will be thrown. - * - * To determine which view file should be rendered, the method calls [[findViewFile()]] which - * will search in the directories as specified by [[basePath]]. - * - * View name can be a path alias representing an absolute file path (e.g. `@app/views/layout/index`), - * or a path relative to [[basePath]]. The file suffix is optional and defaults to `.php` if not given - * in the view name. - * - * @param string $view the view to be rendered. This can be either a path alias or a path relative to [[basePath]]. - * @param array $params the parameters that should be made available in the view. The PHP function `extract()` - * will be called on this variable to extract the variables from this parameter. - * @return string the rendering result - * @throws Exception if the view file cannot be found + * @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. + * @throws InvalidParamException if the view file does not exist. */ public function render($view, $params = array()) { - return $this->createView()->renderPartial($view, $params); + $file = $this->findViewFile($view); + return Yii::$app->getView()->render($this, $file, $params); } /** - * @return View + * Renders a view file. + * @param string $file the view file to be rendered. This can be either a file path or a path alias. + * @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 createView() + public function renderFile($file, $params = array()) { - return new View($this); + return Yii::$app->getView()->render($this, $file, $params); } /** @@ -112,4 +107,44 @@ 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. + * + * The 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 [[viewPath]]. + * + * If the view name does not contain a file extension, it will use the default one `.php`. + * + * @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 + */ + public function findViewFile($view) + { + if (strncmp($view, '@', 1) === 0) { + // e.g. "@app/views/common" + $file = Yii::getAlias($view); + } elseif (strncmp($view, '/', 1) !== 0) { + // e.g. "index" + $file = $this->getViewPath() . DIRECTORY_SEPARATOR . $view; + } elseif (strncmp($view, '//', 2) !== 0 && Yii::$app->controller !== null) { + // e.g. "/site/index" + $file = Yii::$app->controller->module->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); + } else { + // e.g. "//layouts/main" + $file = Yii::$app->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); + } + if (FileHelper::getExtension($file) === '') { + $file .= '.php'; + } + return $file; + } } \ No newline at end of file diff --git a/framework/caching/FileCache.php b/framework/caching/FileCache.php index 2c7bf0a..e565cad 100644 --- a/framework/caching/FileCache.php +++ b/framework/caching/FileCache.php @@ -52,9 +52,6 @@ class FileCache extends Cache { parent::init(); $this->cachePath = \Yii::getAlias($this->cachePath); - if ($this->cachePath === false) { - throw new InvalidConfigException('FileCache.cachePath must be a valid path alias.'); - } if (!is_dir($this->cachePath)) { mkdir($this->cachePath, 0777, true); } diff --git a/framework/console/controllers/MigrateController.php b/framework/console/controllers/MigrateController.php index aec198c..06e100b 100644 --- a/framework/console/controllers/MigrateController.php +++ b/framework/console/controllers/MigrateController.php @@ -114,7 +114,7 @@ class MigrateController extends Controller { if (parent::beforeAction($action)) { $path = Yii::getAlias($this->migrationPath); - if ($path === false || !is_dir($path)) { + if (!is_dir($path)) { throw new Exception("The migration directory \"{$this->migrationPath}\" does not exist."); } $this->migrationPath = $path; diff --git a/framework/util/FileHelper.php b/framework/util/FileHelper.php index e9af8c6..9108476 100644 --- a/framework/util/FileHelper.php +++ b/framework/util/FileHelper.php @@ -43,7 +43,7 @@ class FileHelper public static function ensureDirectory($path) { $p = \Yii::getAlias($path); - if ($p !== false && ($p = realpath($p)) !== false && is_dir($p)) { + if (($p = realpath($p)) !== false && is_dir($p)) { return $p; } else { throw new InvalidConfigException('Directory does not exist: ' . $path); diff --git a/framework/views/error.php b/framework/views/error.php index 28c50f1..893640a 100644 --- a/framework/views/error.php +++ b/framework/views/error.php @@ -1,10 +1,10 @@ owner; -$title = $owner->htmlEncode($exception instanceof \yii\base\Exception || $exception instanceof \yii\base\ErrorException ? $exception->getName() : get_class($exception)); +$context = $this->context; +$title = $context->htmlEncode($exception instanceof \yii\base\Exception || $exception instanceof \yii\base\ErrorException ? $exception->getName() : get_class($exception)); ?> @@ -52,7 +52,7 @@ $title = $owner->htmlEncode($exception instanceof \yii\base\Exception || $except

                -

                htmlEncode($exception->getMessage()))?>

                +

                htmlEncode($exception->getMessage()))?>

                The above error occurred while the Web server was processing your request.

                @@ -61,7 +61,7 @@ $title = $owner->htmlEncode($exception instanceof \yii\base\Exception || $except

                - versionInfo : ''?> + versionInfo : ''?>
                \ No newline at end of file diff --git a/framework/views/exception.php b/framework/views/exception.php index 526a270..6257ffe 100644 --- a/framework/views/exception.php +++ b/framework/views/exception.php @@ -1,10 +1,10 @@ owner; -$title = $owner->htmlEncode($exception instanceof \yii\base\Exception || $exception instanceof \yii\base\ErrorException ? $exception->getName().' ('.get_class($exception).')' : get_class($exception)); +$context = $this->context; +$title = $context->htmlEncode($exception instanceof \yii\base\Exception || $exception instanceof \yii\base\ErrorException ? $exception->getName().' ('.get_class($exception).')' : get_class($exception)); ?> @@ -164,26 +164,26 @@ $title = $owner->htmlEncode($exception instanceof \yii\base\Exception || $except

                - htmlEncode($exception->getMessage()))?> + htmlEncode($exception->getMessage()))?>

                - htmlEncode($exception->getFile()) . '(' . $exception->getLine() . ')'?> + htmlEncode($exception->getFile()) . '(' . $exception->getLine() . ')'?>

                - renderSourceCode($exception->getFile(), $exception->getLine(), $owner->maxSourceLines)?> + renderSourceCode($exception->getFile(), $exception->getLine(), $context->maxSourceLines)?>

                Stack Trace

                - renderTrace($exception->getTrace())?> + renderTrace($exception->getTrace())?>
                - versionInfo : ''?> + versionInfo : ''?>
                diff --git a/framework/web/Session.php b/framework/web/Session.php index 14bea3f..5697679 100644 --- a/framework/web/Session.php +++ b/framework/web/Session.php @@ -214,7 +214,7 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co public function setSavePath($value) { $path = Yii::getAlias($value); - if ($path !== false && is_dir($path)) { + if (is_dir($path)) { session_save_path($path); } else { throw new InvalidParamException("Session save path is not a valid directory: $value"); From 2ecf1d92842913246c9b02c3350e6ca6c642004d Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Fri, 22 Mar 2013 08:50:57 -0400 Subject: [PATCH 090/117] view sip --- framework/base/Controller.php | 4 +-- framework/base/View.php | 59 +++++++++++++++++++++++++++++++++---------- 2 files changed, 47 insertions(+), 16 deletions(-) diff --git a/framework/base/Controller.php b/framework/base/Controller.php index 8840cca..3069ce3 100644 --- a/framework/base/Controller.php +++ b/framework/base/Controller.php @@ -414,12 +414,10 @@ class Controller extends Component */ protected function findLayoutFile() { - /** @var $module Module */ + $module = $this->module; if (is_string($this->layout)) { - $module = $this->module; $view = $this->layout; } elseif ($this->layout === null) { - $module = $this->module; while ($module !== null && $module->layout === null) { $module = $module->module; } diff --git a/framework/base/View.php b/framework/base/View.php index 75a911d..9911cd7 100644 --- a/framework/base/View.php +++ b/framework/base/View.php @@ -79,20 +79,10 @@ class View extends Component * @return string the rendering result * @throws InvalidParamException if the view file or the layout file does not exist. */ - public function render($context, $viewFile, $params = array(), $layoutFile = false) + public function render($view, $params = array()) { - $oldContext = $this->context; - $this->context = $context; - - $content = $this->renderFile($viewFile, $params); - - if ($layoutFile !== false) { - $content = $this->renderFile($layoutFile, array('content' => $content)); - } - - $this->context = $oldContext; - - return $content; + $viewFile = $this->findViewFile($this->context, $view); + return $this->renderFile($viewFile, $params); } /** @@ -156,6 +146,49 @@ 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 [[viewPath]]. + * + * If the view name does not contain a file extension, it will use the default one `.php`. + * + * @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 + */ + protected function findViewFile($context, $view) + { + if (FileHelper::getExtension($view) === '') { + $view .= '.php'; + } + + if (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->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); + } elseif (strncmp($view, '@', 1) !== 0) { + // e.g. "index" or "view/item" + if (method_exists($context, 'getViewPath')) { + $file = $context->getViewPath() . DIRECTORY_SEPARATOR . $view; + } else { + $file = Yii::$app->getViewPath() . DIRECTORY_SEPARATOR . $view; + } + } + + return $file; + } + + /** * Creates a widget. * This method will use [[Yii::createObject()]] to create the widget. * @param string $class the widget class name or path alias From a5ee5b842b29da5afebb6b6e5d2f1cd43161d9bc Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Fri, 22 Mar 2013 17:22:29 -0400 Subject: [PATCH 091/117] view WIP --- framework/base/Controller.php | 52 ++------- framework/base/View.php | 188 ++++++++++++++++++--------------- framework/base/Widget.php | 45 +------- framework/widgets/Clip.php | 57 ++++++++++ framework/widgets/ContentDecorator.php | 81 ++++++++++++++ 5 files changed, 249 insertions(+), 174 deletions(-) create mode 100644 framework/widgets/Clip.php create mode 100644 framework/widgets/ContentDecorator.php diff --git a/framework/base/Controller.php b/framework/base/Controller.php index 3069ce3..9219904 100644 --- a/framework/base/Controller.php +++ b/framework/base/Controller.php @@ -304,9 +304,13 @@ class Controller extends Component */ public function render($view, $params = array()) { - $viewFile = $this->findViewFile($view); + $output = Yii::$app->getView()->render($view, $params, $this); $layoutFile = $this->findLayoutFile(); - return Yii::$app->getView()->render($this, $viewFile, $params, $layoutFile); + if ($layoutFile !== false) { + return Yii::$app->getView()->renderFile($layoutFile, array('content' => $output), $this); + } else { + return $output; + } } /** @@ -319,7 +323,7 @@ class Controller extends Component */ public function renderPartial($view, $params = array()) { - return $this->renderFile($this->findViewFile($view), $params); + return Yii::$app->getView()->render($view, $params, $this); } /** @@ -331,7 +335,7 @@ class Controller extends Component */ public function renderFile($file, $params = array()) { - return Yii::$app->getView()->render($this, $file, $params); + return Yii::$app->getView()->renderFile($file, $params, $this); } /** @@ -346,46 +350,6 @@ class Controller 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 [[viewPath]]. - * - * If the view name does not contain a file extension, it will use the default one `.php`. - * - * @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 - */ - protected function findViewFile($view) - { - if (strncmp($view, '@', 1) === 0) { - // e.g. "@app/views/common" - $file = Yii::getAlias($view); - } elseif (strncmp($view, '/', 1) !== 0) { - // e.g. "index" - $file = $this->getViewPath() . DIRECTORY_SEPARATOR . $view; - } elseif (strncmp($view, '//', 2) !== 0) { - // e.g. "/site/index" - $file = $this->module->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); - } else { - // e.g. "//layouts/main" - $file = Yii::$app->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); - } - if (FileHelper::getExtension($file) === '') { - $file .= '.php'; - } - return $file; - } - - /** * Finds the applicable layout file. * * This method locates an applicable layout file via two steps. diff --git a/framework/base/View.php b/framework/base/View.php index 9911cd7..709b761 100644 --- a/framework/base/View.php +++ b/framework/base/View.php @@ -39,6 +39,12 @@ class View extends Component * If not set, it means theming is not enabled. */ public $theme; + /** + * @var array a list of named output clips. You can call [[beginClip()]] and [[endClip()]] + * to capture small fragments of a view. They can be later accessed at somewhere else + * through this property. + */ + public $clips; /** * @var Widget[] the widgets that are currently not ended @@ -61,35 +67,29 @@ class View extends Component } /** - * Renders a view file under a context with an optional layout. + * Renders a view. * - * This method is similar to [[renderFile()]] except that it will update [[context]] - * with the provided $context parameter. It will also apply layout to the rendering result - * of the view file if $layoutFile is given. + * 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. * - * Theming and localization will be performed for the view file and the layout file, if possible. - * - * @param object $context the context object for rendering the file. This could be a controller, a widget, - * or any other object that serves as the rendering context of the view file. In the view file, - * it can be accessed through the [[context]] property. - * @param string $viewFile the view file. This can be a file path or a path alias. + * @param string $view the view name. Please refer to [[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 string|boolean $layoutFile the layout file. This can be a file path or a path alias. - * If it is false, it means no layout should be applied. + * @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 InvalidParamException if the view file or the layout file does not exist. + * @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()) + public function render($view, $params = array(), $context = null) { - $viewFile = $this->findViewFile($this->context, $view); - return $this->renderFile($viewFile, $params); + $viewFile = $this->findViewFile($context, $view); + return $this->renderFile($viewFile, $params, $context); } /** * Renders a view file. * - * This method renders the specified view file under the existing [[context]]. - * * If [[theme]] is enabled (not null), it will try to render the themed version of the view file as long * as it is available. * @@ -99,12 +99,14 @@ class View extends Component * Otherwise, it will simply include the view file as a normal PHP file, capture its output and * return it as a string. * - * @param string $viewFile the view file. This can be a file path or a path alias. + * @param string $viewFile the view file. This can be either a file path or a path alias. * @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 InvalidParamException if the view file does not exist */ - public function renderFile($viewFile, $params = array()) + public function renderFile($viewFile, $params = array(), $context = null) { $viewFile = Yii::getAlias($viewFile); if (is_file($viewFile)) { @@ -116,11 +118,18 @@ class View extends Component throw new InvalidParamException("The view file does not exist: $viewFile"); } + $oldContext = $this->context; + $this->context = $context; + if ($this->renderer !== null) { - return $this->renderer->render($this, $viewFile, $params); + $output = $this->renderer->render($this, $viewFile, $params); } else { - return $this->renderPhpFile($viewFile, $params); + $output = $this->renderPhpFile($viewFile, $params); } + + $this->context = $oldContext; + + return $output; } /** @@ -156,36 +165,36 @@ class View extends Component * - 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]]. + * - 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 + * @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 (FileHelper::getExtension($view) === '') { - $view .= '.php'; - } - - if (strncmp($view, '//', 2) === 0) { + 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->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); - } elseif (strncmp($view, '@', 1) !== 0) { - // e.g. "index" or "view/item" - if (method_exists($context, 'getViewPath')) { - $file = $context->getViewPath() . DIRECTORY_SEPARATOR . $view; - } else { - $file = Yii::$app->getViewPath() . DIRECTORY_SEPARATOR . $view; - } + $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 $file; + return FileHelper::getExtension($file) === '' ? $file . '.php' : $file; } /** @@ -260,26 +269,31 @@ class View extends Component } } - // - // /** - // * Begins recording a clip. - // * This method is a shortcut to beginning [[yii\widgets\Clip]] - // * @param string $id the clip ID. - // * @param array $properties initial property values for [[yii\widgets\Clip]] - // */ - // public function beginClip($id, $properties = array()) - // { - // $properties['id'] = $id; - // $this->beginWidget('yii\widgets\Clip', $properties); - // } - // - // /** - // * Ends recording a clip. - // */ - // public function endClip() - // { - // $this->endWidget(); - // } + /** + * Begins recording a clip. + * This method is a shortcut to beginning [[yii\widgets\Clip]] + * @param string $id the clip ID. + * @param boolean $renderInPlace whether to render the clip content in place. + * Defaults to false, meaning the captured clip will not be displayed. + * @return \yii\widgets\Clip the Clip widget instance + */ + public function beginClip($id, $renderInPlace = false) + { + return $this->beginWidget('yii\widgets\Clip', array( + 'id' => $id, + 'renderInPlace' => $renderInPlace, + 'view' => $this, + )); + } + + /** + * Ends recording a clip. + */ + public function endClip() + { + $this->endWidget(); + } + // // /** // * Begins fragment caching. @@ -322,33 +336,33 @@ class View extends Component // $this->endWidget(); // } // - // /** - // * Begins the rendering of content that is to be decorated by the specified view. - // * @param mixed $view the name of the view that will be used to decorate the content. The actual view script - // * is resolved via {@link getViewFile}. If this parameter is null (default), - // * the default layout will be used as the decorative view. - // * Note that if the current controller does not belong to - // * any module, the default layout refers to the application's {@link CWebApplication::layout default layout}; - // * If the controller belongs to a module, the default layout refers to the module's - // * {@link CWebModule::layout default layout}. - // * @param array $params the variables (name=>value) to be extracted and made available in the decorative view. - // * @see endContent - // * @see yii\widgets\ContentDecorator - // */ - // public function beginContent($view, $params = array()) - // { - // $this->beginWidget('yii\widgets\ContentDecorator', array( - // 'view' => $view, - // 'params' => $params, - // )); - // } - // - // /** - // * Ends the rendering of content. - // * @see beginContent - // */ - // public function endContent() - // { - // $this->endWidget(); - // } + /** + * Begins the rendering of content that is to be decorated by the specified view. + * @param mixed $view the name of the view that will be used to decorate the content. The actual view script + * is resolved via {@link getViewFile}. If this parameter is null (default), + * the default layout will be used as the decorative view. + * Note that if the current controller does not belong to + * any module, the default layout refers to the application's {@link CWebApplication::layout default layout}; + * If the controller belongs to a module, the default layout refers to the module's + * {@link CWebModule::layout default layout}. + * @param array $params the variables (name=>value) to be extracted and made available in the decorative view. + * @see endContent + * @see yii\widgets\ContentDecorator + */ + public function beginContent($view, $params = array()) + { + $this->beginWidget('yii\widgets\ContentDecorator', array( + 'view' => $view, + 'params' => $params, + )); + } + + /** + * Ends the rendering of content. + * @see beginContent + */ + public function endContent() + { + $this->endWidget(); + } } \ No newline at end of file diff --git a/framework/base/Widget.php b/framework/base/Widget.php index a9fe092..6b92f09 100644 --- a/framework/base/Widget.php +++ b/framework/base/Widget.php @@ -80,8 +80,7 @@ class Widget extends Component */ public function render($view, $params = array()) { - $file = $this->findViewFile($view); - return Yii::$app->getView()->render($this, $file, $params); + return Yii::$app->getView()->render($view, $params, $this); } /** @@ -93,7 +92,7 @@ class Widget extends Component */ public function renderFile($file, $params = array()) { - return Yii::$app->getView()->render($this, $file, $params); + return Yii::$app->getView()->renderFile($file, $params, $this); } /** @@ -107,44 +106,4 @@ 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. - * - * The 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 [[viewPath]]. - * - * If the view name does not contain a file extension, it will use the default one `.php`. - * - * @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 - */ - public function findViewFile($view) - { - if (strncmp($view, '@', 1) === 0) { - // e.g. "@app/views/common" - $file = Yii::getAlias($view); - } elseif (strncmp($view, '/', 1) !== 0) { - // e.g. "index" - $file = $this->getViewPath() . DIRECTORY_SEPARATOR . $view; - } elseif (strncmp($view, '//', 2) !== 0 && Yii::$app->controller !== null) { - // e.g. "/site/index" - $file = Yii::$app->controller->module->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); - } else { - // e.g. "//layouts/main" - $file = Yii::$app->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); - } - if (FileHelper::getExtension($file) === '') { - $file .= '.php'; - } - return $file; - } } \ No newline at end of file diff --git a/framework/widgets/Clip.php b/framework/widgets/Clip.php new file mode 100644 index 0000000..d540b24 --- /dev/null +++ b/framework/widgets/Clip.php @@ -0,0 +1,57 @@ + + * @since 2.0 + */ +class Clip extends Widget +{ + /** + * @var string the ID of this clip. + */ + 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. + */ + public $renderInPlace = false; + + /** + * Starts recording a clip. + */ + public function init() + { + ob_start(); + ob_implicit_flush(false); + } + + /** + * Ends recording a clip. + * This method stops output buffering and saves the rendering result as a named clip in the controller. + */ + public function run() + { + $clip = ob_get_clean(); + if ($this->renderClip) { + echo $clip; + } + $view = $this->view !== null ? $this->view : Yii::$app->getView(); + $view->clips[$this->id] = $clip; + } +} \ No newline at end of file diff --git a/framework/widgets/ContentDecorator.php b/framework/widgets/ContentDecorator.php new file mode 100644 index 0000000..0087698 --- /dev/null +++ b/framework/widgets/ContentDecorator.php @@ -0,0 +1,81 @@ + + * @link http://www.yiiframework.com/ + * @copyright 2008-2013 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +/** + * CContentDecorator decorates the content it encloses with the specified view. + * + * CContentDecorator is mostly used to implement nested layouts, i.e., a layout + * is embedded within another layout. {@link CBaseController} defines a pair of + * convenient methods to use CContentDecorator: + *
                + * $this->beginContent('path/to/view');
                + * // ... content to be decorated
                + * $this->endContent();
                + * 
                + * + * The property {@link view} specifies the name of the view that is used to + * decorate the content. In the view, the content being decorated may be + * accessed with variable $content. + * + * @author Qiang Xue + * @package system.web.widgets + * @since 1.0 + */ +class CContentDecorator extends COutputProcessor +{ + /** + * @var mixed the name of the view that will be used to decorate the captured content. + * If this property is null (default value), the default layout will be used as + * the decorative view. Note that if the current controller does not belong to + * any module, the default layout refers to the application's {@link CWebApplication::layout default layout}; + * If the controller belongs to a module, the default layout refers to the module's + * {@link CWebModule::layout default layout}. + */ + public $view; + /** + * @var array the variables (name=>value) to be extracted and made available in the decorative view. + */ + public $data=array(); + + /** + * Processes the captured output. + * This method decorates the output with the specified {@link view}. + * @param string $output the captured output to be processed + */ + public function processOutput($output) + { + $output=$this->decorate($output); + parent::processOutput($output); + } + + /** + * Decorates the content by rendering a view and embedding the content in it. + * The content being embedded can be accessed in the view using variable $content + * The decorated content will be displayed directly. + * @param string $content the content to be decorated + * @return string the decorated content + */ + protected function decorate($content) + { + $owner=$this->getOwner(); + if($this->view===null) + $viewFile=Yii::app()->getController()->getLayoutFile(null); + else + $viewFile=$owner->getViewFile($this->view); + if($viewFile!==false) + { + $data=$this->data; + $data['content']=$content; + return $owner->renderFile($viewFile,$data,true); + } + else + return $content; + } +} From 5a6c5ccb4ad042ddf48fd4a1880a87832632f86a Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Sat, 23 Mar 2013 15:28:18 -0400 Subject: [PATCH 092/117] Finished ContentDecorator. --- framework/base/View.php | 59 +++++++++++------------ framework/widgets/ContentDecorator.php | 88 +++++++++++++--------------------- 2 files changed, 62 insertions(+), 85 deletions(-) diff --git a/framework/base/View.php b/framework/base/View.php index 709b761..86020ad 100644 --- a/framework/base/View.php +++ b/framework/base/View.php @@ -119,7 +119,9 @@ class View extends Component } $oldContext = $this->context; - $this->context = $context; + if ($context !== null) { + $this->context = $context; + } if ($this->renderer !== null) { $output = $this->renderer->render($this, $viewFile, $params); @@ -276,6 +278,7 @@ class View extends Component * @param boolean $renderInPlace whether to render the clip content in place. * Defaults to false, meaning the captured clip will not be displayed. * @return \yii\widgets\Clip the Clip widget instance + * @see \yii\widgets\Clip */ public function beginClip($id, $renderInPlace = false) { @@ -294,6 +297,31 @@ class View extends Component $this->endWidget(); } + /** + * 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. + * @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()) + { + return $this->beginWidget('yii\widgets\ContentDecorator', array( + 'view' => $this, + 'viewName' => $view, + 'params' => $params, + )); + } + + /** + * Ends the rendering of content. + */ + public function endContent() + { + $this->endWidget(); + } + // // /** // * Begins fragment caching. @@ -336,33 +364,4 @@ class View extends Component // $this->endWidget(); // } // - /** - * Begins the rendering of content that is to be decorated by the specified view. - * @param mixed $view the name of the view that will be used to decorate the content. The actual view script - * is resolved via {@link getViewFile}. If this parameter is null (default), - * the default layout will be used as the decorative view. - * Note that if the current controller does not belong to - * any module, the default layout refers to the application's {@link CWebApplication::layout default layout}; - * If the controller belongs to a module, the default layout refers to the module's - * {@link CWebModule::layout default layout}. - * @param array $params the variables (name=>value) to be extracted and made available in the decorative view. - * @see endContent - * @see yii\widgets\ContentDecorator - */ - public function beginContent($view, $params = array()) - { - $this->beginWidget('yii\widgets\ContentDecorator', array( - 'view' => $view, - 'params' => $params, - )); - } - - /** - * Ends the rendering of content. - * @see beginContent - */ - public function endContent() - { - $this->endWidget(); - } } \ No newline at end of file diff --git a/framework/widgets/ContentDecorator.php b/framework/widgets/ContentDecorator.php index 0087698..4c3ae70 100644 --- a/framework/widgets/ContentDecorator.php +++ b/framework/widgets/ContentDecorator.php @@ -1,81 +1,59 @@ * @link http://www.yiiframework.com/ - * @copyright 2008-2013 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ +namespace yii\widgets; + +use Yii; +use yii\base\InvalidConfigException; +use yii\base\Widget; +use yii\base\View; + /** - * CContentDecorator decorates the content it encloses with the specified view. - * - * CContentDecorator is mostly used to implement nested layouts, i.e., a layout - * is embedded within another layout. {@link CBaseController} defines a pair of - * convenient methods to use CContentDecorator: - *
                - * $this->beginContent('path/to/view');
                - * // ... content to be decorated
                - * $this->endContent();
                - * 
                - * - * The property {@link view} specifies the name of the view that is used to - * decorate the content. In the view, the content being decorated may be - * accessed with variable $content. - * * @author Qiang Xue - * @package system.web.widgets - * @since 1.0 + * @since 2.0 */ -class CContentDecorator extends COutputProcessor +class ContentDecorator extends Widget { /** - * @var mixed the name of the view that will be used to decorate the captured content. - * If this property is null (default value), the default layout will be used as - * the decorative view. Note that if the current controller does not belong to - * any module, the default layout refers to the application's {@link CWebApplication::layout default layout}; - * If the controller belongs to a module, the default layout refers to the module's - * {@link CWebModule::layout default layout}. + * @var View the view object for rendering [[viewName]]. If not set, the view registered with the application + * will be used. */ public $view; /** - * @var array the variables (name=>value) to be extracted and made available in the decorative 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 $data=array(); + public $viewName; + /** + * @var array the parameters (name=>value) to be extracted and made available in the decorative view. + */ + public $params = array(); /** - * Processes the captured output. - * This method decorates the output with the specified {@link view}. - * @param string $output the captured output to be processed + * Starts recording a clip. */ - public function processOutput($output) + public function init() { - $output=$this->decorate($output); - parent::processOutput($output); + if ($this->viewName === null) { + throw new InvalidConfigException('ContentDecorator::viewName must be set.'); + } + ob_start(); + ob_implicit_flush(false); } /** - * Decorates the content by rendering a view and embedding the content in it. - * The content being embedded can be accessed in the view using variable $content - * The decorated content will be displayed directly. - * @param string $content the content to be decorated - * @return string the decorated content + * Ends recording a clip. + * This method stops output buffering and saves the rendering result as a named clip in the controller. */ - protected function decorate($content) + public function run() { - $owner=$this->getOwner(); - if($this->view===null) - $viewFile=Yii::app()->getController()->getLayoutFile(null); - else - $viewFile=$owner->getViewFile($this->view); - if($viewFile!==false) - { - $data=$this->data; - $data['content']=$content; - return $owner->renderFile($viewFile,$data,true); - } - else - return $content; + $params = $this->params; + $params['content'] = ob_get_clean(); + $view = $this->view !== null ? $this->view : Yii::$app->getView(); + echo $view->render($this->viewName, $params); } } From 339ac9b49f7d8f1ee00aa49e7d4babe3b2184ebf Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Sat, 23 Mar 2013 15:30:54 -0400 Subject: [PATCH 093/117] renamed util to helpers. --- framework/helpers/ArrayHelper.php | 340 ++++++++++++ framework/helpers/ConsoleColor.php | 478 +++++++++++++++++ framework/helpers/FileHelper.php | 274 ++++++++++ framework/helpers/Html.php | 976 +++++++++++++++++++++++++++++++++++ framework/helpers/SecurityHelper.php | 272 ++++++++++ framework/helpers/StringHelper.php | 125 +++++ framework/helpers/VarDumper.php | 134 +++++ framework/helpers/mimeTypes.php | 187 +++++++ framework/util/ArrayHelper.php | 340 ------------ framework/util/ConsoleColor.php | 478 ----------------- framework/util/FileHelper.php | 274 ---------- framework/util/Html.php | 976 ----------------------------------- framework/util/SecurityHelper.php | 272 ---------- framework/util/StringHelper.php | 125 ----- framework/util/VarDumper.php | 134 ----- framework/util/mimeTypes.php | 187 ------- 16 files changed, 2786 insertions(+), 2786 deletions(-) create mode 100644 framework/helpers/ArrayHelper.php create mode 100644 framework/helpers/ConsoleColor.php create mode 100644 framework/helpers/FileHelper.php create mode 100644 framework/helpers/Html.php create mode 100644 framework/helpers/SecurityHelper.php create mode 100644 framework/helpers/StringHelper.php create mode 100644 framework/helpers/VarDumper.php create mode 100644 framework/helpers/mimeTypes.php delete mode 100644 framework/util/ArrayHelper.php delete mode 100644 framework/util/ConsoleColor.php delete mode 100644 framework/util/FileHelper.php delete mode 100644 framework/util/Html.php delete mode 100644 framework/util/SecurityHelper.php delete mode 100644 framework/util/StringHelper.php delete mode 100644 framework/util/VarDumper.php delete mode 100644 framework/util/mimeTypes.php diff --git a/framework/helpers/ArrayHelper.php b/framework/helpers/ArrayHelper.php new file mode 100644 index 0000000..447d034 --- /dev/null +++ b/framework/helpers/ArrayHelper.php @@ -0,0 +1,340 @@ + + * @since 2.0 + */ +class ArrayHelper +{ + /** + * Merges two or more arrays into one recursively. + * If each array has an element with the same string key value, the latter + * will overwrite the former (different from array_merge_recursive). + * Recursive merging will be conducted if both arrays have an element of array + * type and are having the same key. + * For integer-keyed elements, the elements from the latter array will + * be appended to the former array. + * @param array $a array to be merged to + * @param array $b array to be merged from. You can specify additional + * arrays via third argument, fourth argument etc. + * @return array the merged array (the original arrays are not changed.) + */ + public static function merge($a, $b) + { + $args = func_get_args(); + $res = array_shift($args); + while ($args !== array()) { + $next = array_shift($args); + foreach ($next as $k => $v) { + if (is_integer($k)) { + isset($res[$k]) ? $res[] = $v : $res[$k] = $v; + } elseif (is_array($v) && isset($res[$k]) && is_array($res[$k])) { + $res[$k] = self::merge($res[$k], $v); + } else { + $res[$k] = $v; + } + } + } + return $res; + } + + /** + * Retrieves the value of an array element or object property with the given key or property name. + * If the key does not exist in the array, the default value will be returned instead. + * + * Below are some usage examples, + * + * ~~~ + * // working with array + * $username = \yii\util\ArrayHelper::getValue($_POST, 'username'); + * // working with object + * $username = \yii\util\ArrayHelper::getValue($user, 'username'); + * // working with anonymous function + * $fullName = \yii\util\ArrayHelper::getValue($user, function($user, $defaultValue) { + * return $user->firstName . ' ' . $user->lastName; + * }); + * ~~~ + * + * @param array|object $array array or object to extract value from + * @param string|\Closure $key key name of the array element, or property name of the object, + * or an anonymous function returning the value. The anonymous function signature should be: + * `function($array, $defaultValue)`. + * @param mixed $default the default value to be returned if the specified key does not exist + * @return mixed the value of the + */ + public static function getValue($array, $key, $default = null) + { + if ($key instanceof \Closure) { + return $key($array, $default); + } elseif (is_array($array)) { + return isset($array[$key]) || array_key_exists($key, $array) ? $array[$key] : $default; + } else { + return $array->$key; + } + } + + /** + * Indexes an array according to a specified key. + * The input array should be multidimensional or an array of objects. + * + * The key can be a key name of the sub-array, a property name of object, or an anonymous + * function which returns the key value given an array element. + * + * If a key value is null, the corresponding array element will be discarded and not put in the result. + * + * For example, + * + * ~~~ + * $array = array( + * array('id' => '123', 'data' => 'abc'), + * array('id' => '345', 'data' => 'def'), + * ); + * $result = ArrayHelper::index($array, 'id'); + * // the result is: + * // array( + * // '123' => array('id' => '123', 'data' => 'abc'), + * // '345' => array('id' => '345', 'data' => 'def'), + * // ) + * + * // using anonymous function + * $result = ArrayHelper::index($array, function(element) { + * return $element['id']; + * }); + * ~~~ + * + * @param array $array the array that needs to be indexed + * @param string|\Closure $key the column name or anonymous function whose result will be used to index the array + * @return array the indexed array + */ + public static function index($array, $key) + { + $result = array(); + foreach ($array as $element) { + $value = static::getValue($element, $key); + $result[$value] = $element; + } + return $result; + } + + /** + * Returns the values of a specified column in an array. + * The input array should be multidimensional or an array of objects. + * + * For example, + * + * ~~~ + * $array = array( + * array('id' => '123', 'data' => 'abc'), + * array('id' => '345', 'data' => 'def'), + * ); + * $result = ArrayHelper::getColumn($array, 'id'); + * // the result is: array( '123', '345') + * + * // using anonymous function + * $result = ArrayHelper::getColumn($array, function(element) { + * return $element['id']; + * }); + * ~~~ + * + * @param array $array + * @param string|\Closure $name + * @param boolean $keepKeys whether to maintain the array keys. If false, the resulting array + * will be re-indexed with integers. + * @return array the list of column values + */ + public static function getColumn($array, $name, $keepKeys = true) + { + $result = array(); + if ($keepKeys) { + foreach ($array as $k => $element) { + $result[$k] = static::getValue($element, $name); + } + } else { + foreach ($array as $element) { + $result[] = static::getValue($element, $name); + } + } + + return $result; + } + + /** + * Builds a map (key-value pairs) from a multidimensional array or an array of objects. + * The `$from` and `$to` parameters specify the key names or property names to set up the map. + * Optionally, one can further group the map according to a grouping field `$group`. + * + * For example, + * + * ~~~ + * $array = array( + * array('id' => '123', 'name' => 'aaa', 'class' => 'x'), + * array('id' => '124', 'name' => 'bbb', 'class' => 'x'), + * array('id' => '345', 'name' => 'ccc', 'class' => 'y'), + * ); + * + * $result = ArrayHelper::map($array, 'id', 'name'); + * // the result is: + * // array( + * // '123' => 'aaa', + * // '124' => 'bbb', + * // '345' => 'ccc', + * // ) + * + * $result = ArrayHelper::map($array, 'id', 'name', 'class'); + * // the result is: + * // array( + * // 'x' => array( + * // '123' => 'aaa', + * // '124' => 'bbb', + * // ), + * // 'y' => array( + * // '345' => 'ccc', + * // ), + * // ) + * ~~~ + * + * @param array $array + * @param string|\Closure $from + * @param string|\Closure $to + * @param string|\Closure $group + * @return array + */ + public static function map($array, $from, $to, $group = null) + { + $result = array(); + foreach ($array as $element) { + $key = static::getValue($element, $from); + $value = static::getValue($element, $to); + if ($group !== null) { + $result[static::getValue($element, $group)][$key] = $value; + } else { + $result[$key] = $value; + } + } + return $result; + } + + /** + * Sorts an array of objects or arrays (with the same structure) by one or several keys. + * @param array $array the array to be sorted. The array will be modified after calling this method. + * @param string|\Closure|array $key the key(s) to be sorted by. This refers to a key name of the sub-array + * elements, a property name of the objects, or an anonymous function returning the values for comparison + * purpose. The anonymous function signature should be: `function($item)`. + * To sort by multiple keys, provide an array of keys here. + * @param boolean|array $ascending whether to sort in ascending or descending order. When + * sorting by multiple keys with different ascending orders, use an array of ascending flags. + * @param integer|array $sortFlag the PHP sort flag. Valid values include: + * `SORT_REGULAR`, `SORT_NUMERIC`, `SORT_STRING`, and `SORT_STRING | SORT_FLAG_CASE`. The last + * value is for sorting strings in case-insensitive manner. Please refer to + * See [PHP manual](http://php.net/manual/en/function.sort.php) for more details. + * When sorting by multiple keys with different sort flags, use an array of sort flags. + * @throws InvalidParamException if the $ascending or $sortFlag parameters do not have + * correct number of elements as that of $key. + */ + public static function multisort(&$array, $key, $ascending = true, $sortFlag = SORT_REGULAR) + { + $keys = is_array($key) ? $key : array($key); + if (empty($keys) || empty($array)) { + return; + } + $n = count($keys); + if (is_scalar($ascending)) { + $ascending = array_fill(0, $n, $ascending); + } elseif (count($ascending) !== $n) { + throw new InvalidParamException('The length of $ascending parameter must be the same as that of $keys.'); + } + if (is_scalar($sortFlag)) { + $sortFlag = array_fill(0, $n, $sortFlag); + } elseif (count($sortFlag) !== $n) { + throw new InvalidParamException('The length of $ascending parameter must be the same as that of $keys.'); + } + $args = array(); + foreach ($keys as $i => $key) { + $flag = $sortFlag[$i]; + if ($flag == (SORT_STRING | SORT_FLAG_CASE)) { + $flag = SORT_STRING; + $column = array(); + foreach (static::getColumn($array, $key) as $k => $value) { + $column[$k] = strtolower($value); + } + $args[] = $column; + } else { + $args[] = static::getColumn($array, $key); + } + $args[] = $ascending[$i] ? SORT_ASC : SORT_DESC; + $args[] = $flag; + } + $args[] = &$array; + call_user_func_array('array_multisort', $args); + } + + /** + * Encodes special characters in an array of strings into HTML entities. + * Both the array keys and values will be encoded. + * If a value is an array, this method will also encode it recursively. + * @param array $data data to be encoded + * @param boolean $valuesOnly whether to encode array values only. If false, + * both the array keys and array values will be encoded. + * @param string $charset the charset that the data is using. If not set, + * [[\yii\base\Application::charset]] will be used. + * @return array the encoded data + * @see http://www.php.net/manual/en/function.htmlspecialchars.php + */ + public static function htmlEncode($data, $valuesOnly = true, $charset = null) + { + if ($charset === null) { + $charset = Yii::$app->charset; + } + $d = array(); + foreach ($data as $key => $value) { + if (!$valuesOnly && is_string($key)) { + $key = htmlspecialchars($key, ENT_QUOTES, $charset); + } + if (is_string($value)) { + $d[$key] = htmlspecialchars($value, ENT_QUOTES, $charset); + } elseif (is_array($value)) { + $d[$key] = static::htmlEncode($value, $charset); + } + } + return $d; + } + + /** + * Decodes HTML entities into the corresponding characters in an array of strings. + * Both the array keys and values will be decoded. + * If a value is an array, this method will also decode it recursively. + * @param array $data data to be decoded + * @param boolean $valuesOnly whether to decode array values only. If false, + * both the array keys and array values will be decoded. + * @return array the decoded data + * @see http://www.php.net/manual/en/function.htmlspecialchars-decode.php + */ + public static function htmlDecode($data, $valuesOnly = true) + { + $d = array(); + foreach ($data as $key => $value) { + if (!$valuesOnly && is_string($key)) { + $key = htmlspecialchars_decode($key, ENT_QUOTES); + } + if (is_string($value)) { + $d[$key] = htmlspecialchars_decode($value, ENT_QUOTES); + } elseif (is_array($value)) { + $d[$key] = static::htmlDecode($value); + } + } + return $d; + } +} \ No newline at end of file diff --git a/framework/helpers/ConsoleColor.php b/framework/helpers/ConsoleColor.php new file mode 100644 index 0000000..74aa154 --- /dev/null +++ b/framework/helpers/ConsoleColor.php @@ -0,0 +1,478 @@ + + * @since 2.0 + */ +class ConsoleColor +{ + const FG_BLACK = 30; + const FG_RED = 31; + const FG_GREEN = 32; + const FG_YELLOW = 33; + const FG_BLUE = 34; + const FG_PURPLE = 35; + const FG_CYAN = 36; + const FG_GREY = 37; + + const BG_BLACK = 40; + const BG_RED = 41; + const BG_GREEN = 42; + const BG_YELLOW = 43; + const BG_BLUE = 44; + const BG_PURPLE = 45; + const BG_CYAN = 46; + const BG_GREY = 47; + + const BOLD = 1; + const ITALIC = 3; + const UNDERLINE = 4; + const BLINK = 5; + const NEGATIVE = 7; + const CONCEALED = 8; + const CROSSED_OUT = 9; + const FRAMED = 51; + const ENCIRCLED = 52; + const OVERLINED = 53; + + /** + * Moves the terminal cursor up by sending ANSI control code CUU to the terminal. + * If the cursor is already at the edge of the screen, this has no effect. + * @param integer $rows number of rows the cursor should be moved up + */ + public static function moveCursorUp($rows=1) + { + echo "\033[" . (int) $rows . 'A'; + } + + /** + * Moves the terminal cursor down by sending ANSI control code CUD to the terminal. + * If the cursor is already at the edge of the screen, this has no effect. + * @param integer $rows number of rows the cursor should be moved down + */ + public static function moveCursorDown($rows=1) + { + echo "\033[" . (int) $rows . 'B'; + } + + /** + * Moves the terminal cursor forward by sending ANSI control code CUF to the terminal. + * If the cursor is already at the edge of the screen, this has no effect. + * @param integer $steps number of steps the cursor should be moved forward + */ + public static function moveCursorForward($steps=1) + { + echo "\033[" . (int) $steps . 'C'; + } + + /** + * Moves the terminal cursor backward by sending ANSI control code CUB to the terminal. + * If the cursor is already at the edge of the screen, this has no effect. + * @param integer $steps number of steps the cursor should be moved backward + */ + public static function moveCursorBackward($steps=1) + { + echo "\033[" . (int) $steps . 'D'; + } + + /** + * Moves the terminal cursor to the beginning of the next line by sending ANSI control code CNL to the terminal. + * @param integer $lines number of lines the cursor should be moved down + */ + public static function moveCursorNextLine($lines=1) + { + echo "\033[" . (int) $lines . 'E'; + } + + /** + * Moves the terminal cursor to the beginning of the previous line by sending ANSI control code CPL to the terminal. + * @param integer $lines number of lines the cursor should be moved up + */ + public static function moveCursorPrevLine($lines=1) + { + echo "\033[" . (int) $lines . 'F'; + } + + /** + * Moves the cursor to an absolute position given as column and row by sending ANSI control code CUP or CHA to the terminal. + * @param integer $column 1-based column number, 1 is the left edge of the screen. + * @param integer|null $row 1-based row number, 1 is the top edge of the screen. if not set, will move cursor only in current line. + */ + public static function moveCursorTo($column, $row=null) + { + if ($row === null) { + echo "\033[" . (int) $column . 'G'; + } else { + echo "\033[" . (int) $row . ';' . (int) $column . 'H'; + } + } + + /** + * Scrolls whole page up by sending ANSI control code SU to the terminal. + * New lines are added at the bottom. This is not supported by ANSI.SYS used in windows. + * @param int $lines number of lines to scroll up + */ + public static function scrollUp($lines=1) + { + echo "\033[".(int)$lines."S"; + } + + /** + * Scrolls whole page down by sending ANSI control code SD to the terminal. + * New lines are added at the top. This is not supported by ANSI.SYS used in windows. + * @param int $lines number of lines to scroll down + */ + public static function scrollDown($lines=1) + { + echo "\033[".(int)$lines."T"; + } + + /** + * Saves the current cursor position by sending ANSI control code SCP to the terminal. + * Position can then be restored with {@link restoreCursorPosition}. + */ + public static function saveCursorPosition() + { + echo "\033[s"; + } + + /** + * Restores the cursor position saved with {@link saveCursorPosition} by sending ANSI control code RCP to the terminal. + */ + public static function restoreCursorPosition() + { + echo "\033[u"; + } + + /** + * Hides the cursor by sending ANSI DECTCEM code ?25l to the terminal. + * Use {@link showCursor} to bring it back. + * Do not forget to show cursor when your application exits. Cursor might stay hidden in terminal after exit. + */ + public static function hideCursor() + { + echo "\033[?25l"; + } + + /** + * Will show a cursor again when it has been hidden by {@link hideCursor} by sending ANSI DECTCEM code ?25h to the terminal. + */ + public static function showCursor() + { + echo "\033[?25h"; + } + + /** + * Clears entire screen content by sending ANSI control code ED with argument 2 to the terminal. + * Cursor position will not be changed. + * **Note:** ANSI.SYS implementation used in windows will reset cursor position to upper left corner of the screen. + */ + public static function clearScreen() + { + echo "\033[2J"; + } + + /** + * Clears text from cursor to the beginning of the screen by sending ANSI control code ED with argument 1 to the terminal. + * Cursor position will not be changed. + */ + public static function clearScreenBeforeCursor() + { + echo "\033[1J"; + } + + /** + * Clears text from cursor to the end of the screen by sending ANSI control code ED with argument 0 to the terminal. + * Cursor position will not be changed. + */ + public static function clearScreenAfterCursor() + { + echo "\033[0J"; + } + + /** + * Clears the line, the cursor is currently on by sending ANSI control code EL with argument 2 to the terminal. + * Cursor position will not be changed. + */ + public static function clearLine() + { + echo "\033[2K"; + } + + /** + * Clears text from cursor position to the beginning of the line by sending ANSI control code EL with argument 1 to the terminal. + * Cursor position will not be changed. + */ + public static function clearLineBeforeCursor() + { + echo "\033[1K"; + } + + /** + * Clears text from cursor position to the end of the line by sending ANSI control code EL with argument 0 to the terminal. + * Cursor position will not be changed. + */ + public static function clearLineAfterCursor() + { + echo "\033[0K"; + } + + /** + * Will send ANSI format for following output + * + * You can pass any of the FG_*, BG_* and TEXT_* constants and also xterm256ColorBg + * TODO: documentation + */ + public static function ansiStyle() + { + echo "\033[" . implode(';', func_get_args()) . 'm'; + } + + /** + * Will return a string formatted with the given ANSI style + * + * See {@link ansiStyle} for possible arguments. + * @param string $string the string to be formatted + * @return string + */ + public static function ansiStyleString($string) + { + $args = func_get_args(); + array_shift($args); + $code = implode(';', $args); + return "\033[0m" . ($code !== '' ? "\033[" . $code . "m" : '') . $string."\033[0m"; + } + + //const COLOR_XTERM256 = 38;// http://en.wikipedia.org/wiki/Talk:ANSI_escape_code#xterm-256colors + public static function xterm256ColorFg($i) // TODO naming! + { + return '38;5;'.$i; + } + + public static function xterm256ColorBg($i) // TODO naming! + { + return '48;5;'.$i; + } + + /** + * Usage: list($w, $h) = ConsoleHelper::getScreenSize(); + * + * @return array + */ + public static function getScreenSize() + { + // TODO implement + return array(150,50); + } + + /** + * resets any ansi style set by previous method {@link ansiStyle} + * Any output after this is will have default text style. + */ + public static function reset() + { + echo "\033[0m"; + } + + /** + * Strips ANSI control codes from a string + * + * @param string $string String to strip + * @return string + */ + public static function strip($string) + { + return preg_replace('/\033\[[\d;]+m/', '', $string); // TODO currently only strips color + } + + // TODO refactor and review + public static function ansiToHtml($string) + { + $tags = 0; + return preg_replace_callback('/\033\[[\d;]+m/', function($ansi) use (&$tags) { + $styleA = array(); + foreach(explode(';', $ansi) as $controlCode) + { + switch($controlCode) + { + case static::FG_BLACK: $style = array('color' => '#000000'); break; + case static::FG_BLUE: $style = array('color' => '#000078'); break; + case static::FG_CYAN: $style = array('color' => '#007878'); break; + case static::FG_GREEN: $style = array('color' => '#007800'); break; + case static::FG_GREY: $style = array('color' => '#787878'); break; + case static::FG_PURPLE: $style = array('color' => '#780078'); break; + case static::FG_RED: $style = array('color' => '#780000'); break; + case static::FG_YELLOW: $style = array('color' => '#787800'); break; + case static::BG_BLACK: $style = array('background-color' => '#000000'); break; + case static::BG_BLUE: $style = array('background-color' => '#000078'); break; + case static::BG_CYAN: $style = array('background-color' => '#007878'); break; + case static::BG_GREEN: $style = array('background-color' => '#007800'); break; + case static::BG_GREY: $style = array('background-color' => '#787878'); break; + case static::BG_PURPLE: $style = array('background-color' => '#780078'); break; + case static::BG_RED: $style = array('background-color' => '#780000'); break; + case static::BG_YELLOW: $style = array('background-color' => '#787800'); break; + case static::BOLD: $style = array('font-weight' => 'bold'); break; + case static::ITALIC: $style = array('font-style' => 'italic'); break; + case static::UNDERLINE: $style = array('text-decoration' => array('underline')); break; + case static::OVERLINED: $style = array('text-decoration' => array('overline')); break; + case static::CROSSED_OUT:$style = array('text-decoration' => array('line-through')); break; + case static::BLINK: $style = array('text-decoration' => array('blink')); break; + case static::NEGATIVE: // ??? + case static::CONCEALED: + case static::ENCIRCLED: + case static::FRAMED: + // TODO allow resetting codes + break; + case 0: // ansi reset + $return = ''; + for($n=$tags; $tags>0; $tags--) { + $return .= ''; + } + return $return; + } + + $styleA = ArrayHelper::merge($styleA, $style); + } + $styleString[] = array(); + foreach($styleA as $name => $content) { + if ($name === 'text-decoration') { + $content = implode(' ', $content); + } + $styleString[] = $name.':'.$content; + } + $tags++; + return ' $ds, '\\' => $ds)), $ds); + } + + /** + * Returns the localized version of a specified file. + * + * The searching is based on the specified language code. In particular, + * a file with the same name will be looked for under the subdirectory + * whose name is same as the language code. For example, given the file "path/to/view.php" + * and language code "zh_cn", the localized file will be looked for as + * "path/to/zh_cn/view.php". If the file is not found, the original file + * will be returned. + * + * If the target and the source language codes are the same, + * the original file will be returned. + * + * For consistency, it is recommended that the language code is given + * in lower case and in the format of LanguageID_RegionID (e.g. "en_us"). + * + * @param string $file the original file + * @param string $language the target language that the file should be localized to. + * If not set, the value of [[\yii\base\Application::language]] will be used. + * @param string $sourceLanguage the language that the original file is in. + * If not set, the value of [[\yii\base\Application::sourceLanguage]] will be used. + * @return string the matching localized file, or the original file if the localized version is not found. + * If the target and the source language codes are the same, the original file will be returned. + */ + public static function localize($file, $language = null, $sourceLanguage = null) + { + if ($language === null) { + $language = \Yii::$app->language; + } + if ($sourceLanguage === null) { + $sourceLanguage = \Yii::$app->sourceLanguage; + } + if ($language === $sourceLanguage) { + return $file; + } + $desiredFile = dirname($file) . DIRECTORY_SEPARATOR . $sourceLanguage . DIRECTORY_SEPARATOR . basename($file); + return is_file($desiredFile) ? $desiredFile : $file; + } + + /** + * Determines the MIME type of the specified file. + * This method will first try to determine the MIME type based on + * [finfo_open](http://php.net/manual/en/function.finfo-open.php). If this doesn't work, it will + * fall back to [[getMimeTypeByExtension()]]. + * @param string $file the file name. + * @param string $magicFile name of the optional magic database file, usually something like `/path/to/magic.mime`. + * This will be passed as the second parameter to [finfo_open](http://php.net/manual/en/function.finfo-open.php). + * @param boolean $checkExtension whether to use the file extension to determine the MIME type in case + * `finfo_open()` cannot determine it. + * @return string the MIME type (e.g. `text/plain`). Null is returned if the MIME type cannot be determined. + */ + public static function getMimeType($file, $magicFile = null, $checkExtension = true) + { + if (function_exists('finfo_open')) { + $info = finfo_open(FILEINFO_MIME_TYPE, $magicFile); + if ($info && ($result = finfo_file($info, $file)) !== false) { + return $result; + } + } + + return $checkExtension ? self::getMimeTypeByExtension($file) : null; + } + + /** + * Determines the MIME type based on the extension name of the specified file. + * This method will use a local map between extension names and MIME types. + * @param string $file the file name. + * @param string $magicFile the path of the file that contains all available MIME type information. + * If this is not set, the default file aliased by `@yii/util/mimeTypes.php` will be used. + * @return string the MIME type. Null is returned if the MIME type cannot be determined. + */ + public static function getMimeTypeByExtension($file, $magicFile = null) + { + if ($magicFile === null) { + $magicFile = \Yii::getAlias('@yii/util/mimeTypes.php'); + } + $mimeTypes = require($magicFile); + if (($ext = pathinfo($file, PATHINFO_EXTENSION)) !== '') { + $ext = strtolower($ext); + if (isset($mimeTypes[$ext])) { + return $mimeTypes[$ext]; + } + } + return null; + } + + /** + * Copies a list of files from one place to another. + * @param array $fileList the list of files to be copied (name=>spec). + * The array keys are names displayed during the copy process, and array values are specifications + * for files to be copied. Each array value must be an array of the following structure: + *
                  + *
                • source: required, the full path of the file/directory to be copied from
                • + *
                • target: required, the full path of the file/directory to be copied to
                • + *
                • callback: optional, the callback to be invoked when copying a file. The callback function + * should be declared as follows: + *
                  +	 *   function foo($source,$params)
                  +	 *   
                  + * where $source parameter is the source file path, and the content returned + * by the function will be saved into the target file.
                • + *
                • params: optional, the parameters to be passed to the callback
                • + *
                + * @see buildFileList + */ + public static function copyFiles($fileList) + { + $overwriteAll = false; + foreach($fileList as $name=>$file) { + $source = strtr($file['source'], '/\\', DIRECTORY_SEPARATOR); + $target = strtr($file['target'], '/\\', DIRECTORY_SEPARATOR); + $callback = isset($file['callback']) ? $file['callback'] : null; + $params = isset($file['params']) ? $file['params'] : null; + + if(is_dir($source)) { + try { + self::ensureDirectory($target); + } + catch (Exception $e) { + mkdir($target, true, 0777); + } + continue; + } + + if($callback !== null) { + $content = call_user_func($callback, $source, $params); + } + else { + $content = file_get_contents($source); + } + if(is_file($target)) { + if($content === file_get_contents($target)) { + echo " unchanged $name\n"; + continue; + } + if($overwriteAll) { + echo " overwrite $name\n"; + } + else { + echo " exist $name\n"; + echo " ...overwrite? [Yes|No|All|Quit] "; + $answer = trim(fgets(STDIN)); + if(!strncasecmp($answer, 'q', 1)) { + return; + } + elseif(!strncasecmp($answer, 'y', 1)) { + echo " overwrite $name\n"; + } + elseif(!strncasecmp($answer, 'a', 1)) { + echo " overwrite $name\n"; + $overwriteAll = true; + } + else { + echo " skip $name\n"; + continue; + } + } + } + else { + try { + self::ensureDirectory(dirname($target)); + } + catch (Exception $e) { + mkdir(dirname($target), true, 0777); + } + echo " generate $name\n"; + } + file_put_contents($target, $content); + } + } + + /** + * Builds the file list of a directory. + * This method traverses through the specified directory and builds + * a list of files and subdirectories that the directory contains. + * The result of this function can be passed to {@link copyFiles}. + * @param string $sourceDir the source directory + * @param string $targetDir the target directory + * @param string $baseDir base directory + * @param array $ignoreFiles list of the names of files that should + * be ignored in list building process. Argument available since 1.1.11. + * @param array $renameMap hash array of file names that should be + * renamed. Example value: array('1.old.txt'=>'2.new.txt'). + * @return array the file list (see {@link copyFiles}) + */ + public static function buildFileList($sourceDir, $targetDir, $baseDir='', $ignoreFiles=array(), $renameMap=array()) + { + $list = array(); + $handle = opendir($sourceDir); + while(($file = readdir($handle)) !== false) { + if(in_array($file, array('.', '..', '.svn', '.gitignore')) || in_array($file, $ignoreFiles)) { + continue; + } + $sourcePath = $sourceDir.DIRECTORY_SEPARATOR.$file; + $targetPath = $targetDir.DIRECTORY_SEPARATOR.strtr($file, $renameMap); + $name = $baseDir === '' ? $file : $baseDir.'/'.$file; + $list[$name] = array( + 'source' => $sourcePath, + 'target' => $targetPath, + ); + if(is_dir($sourcePath)) { + $list = array_merge($list, self::buildFileList($sourcePath, $targetPath, $name, $ignoreFiles, $renameMap)); + } + } + closedir($handle); + return $list; + } +} \ No newline at end of file diff --git a/framework/helpers/Html.php b/framework/helpers/Html.php new file mode 100644 index 0000000..a7b744b --- /dev/null +++ b/framework/helpers/Html.php @@ -0,0 +1,976 @@ + + * @since 2.0 + */ +class Html +{ + /** + * @var boolean whether to close void (empty) elements. Defaults to true. + * @see voidElements + */ + public static $closeVoidElements = true; + /** + * @var array list of void elements (element name => 1) + * @see closeVoidElements + * @see http://www.w3.org/TR/html-markup/syntax.html#void-element + */ + public static $voidElements = array( + 'area' => 1, + 'base' => 1, + 'br' => 1, + 'col' => 1, + 'command' => 1, + 'embed' => 1, + 'hr' => 1, + 'img' => 1, + 'input' => 1, + 'keygen' => 1, + 'link' => 1, + 'meta' => 1, + 'param' => 1, + 'source' => 1, + 'track' => 1, + 'wbr' => 1, + ); + /** + * @var boolean whether to show the values of boolean attributes in element tags. + * If false, only the attribute names will be generated. + * @see booleanAttributes + */ + public static $showBooleanAttributeValues = true; + /** + * @var array list of boolean attributes. The presence of a boolean attribute on + * an element represents the true value, and the absence of the attribute represents the false value. + * @see showBooleanAttributeValues + * @see http://www.w3.org/TR/html5/infrastructure.html#boolean-attributes + */ + public static $booleanAttributes = array( + 'async' => 1, + 'autofocus' => 1, + 'autoplay' => 1, + 'checked' => 1, + 'controls' => 1, + 'declare' => 1, + 'default' => 1, + 'defer' => 1, + 'disabled' => 1, + 'formnovalidate' => 1, + 'hidden' => 1, + 'ismap' => 1, + 'loop' => 1, + 'multiple' => 1, + 'muted' => 1, + 'nohref' => 1, + 'noresize' => 1, + 'novalidate' => 1, + 'open' => 1, + 'readonly' => 1, + 'required' => 1, + 'reversed' => 1, + 'scoped' => 1, + 'seamless' => 1, + 'selected' => 1, + 'typemustmatch' => 1, + ); + /** + * @var array the preferred order of attributes in a tag. This mainly affects the order of the attributes + * that are rendered by [[renderAttributes()]]. + */ + public static $attributeOrder = array( + 'type', + 'id', + 'class', + 'name', + 'value', + + 'href', + 'src', + 'action', + 'method', + + 'selected', + 'checked', + 'readonly', + 'disabled', + 'multiple', + + 'size', + 'maxlength', + 'width', + 'height', + 'rows', + 'cols', + + 'alt', + 'title', + 'rel', + 'media', + ); + + /** + * Encodes special characters into HTML entities. + * The [[yii\base\Application::charset|application charset]] will be used for encoding. + * @param string $content the content to be encoded + * @return string the encoded content + * @see decode + * @see http://www.php.net/manual/en/function.htmlspecialchars.php + */ + public static function encode($content) + { + return htmlspecialchars($content, ENT_QUOTES, Yii::$app->charset); + } + + /** + * Decodes special HTML entities back to the corresponding characters. + * This is the opposite of [[encode()]]. + * @param string $content the content to be decoded + * @return string the decoded content + * @see encode + * @see http://www.php.net/manual/en/function.htmlspecialchars-decode.php + */ + public static function decode($content) + { + return htmlspecialchars_decode($content, ENT_QUOTES); + } + + /** + * Generates a complete HTML tag. + * @param string $name the tag name + * @param string $content the content to be enclosed between the start and end tags. It will not be HTML-encoded. + * If this is coming from end users, you should consider [[encode()]] it to prevent XSS attacks. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated HTML tag + * @see beginTag + * @see endTag + */ + public static function tag($name, $content = '', $options = array()) + { + $html = '<' . $name . static::renderTagAttributes($options); + if (isset(static::$voidElements[strtolower($name)])) { + return $html . (static::$closeVoidElements ? ' />' : '>'); + } else { + return $html . ">$content"; + } + } + + /** + * Generates a start tag. + * @param string $name the tag name + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated start tag + * @see endTag + * @see tag + */ + public static function beginTag($name, $options = array()) + { + return '<' . $name . static::renderTagAttributes($options) . '>'; + } + + /** + * Generates an end tag. + * @param string $name the tag name + * @return string the generated end tag + * @see beginTag + * @see tag + */ + public static function endTag($name) + { + return ""; + } + + /** + * Encloses the given content within a CDATA tag. + * @param string $content the content to be enclosed within the CDATA tag + * @return string the CDATA tag with the enclosed content. + */ + public static function cdata($content) + { + return ''; + } + + /** + * Generates a style tag. + * @param string $content the style content + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * If the options does not contain "type", a "type" attribute with value "text/css" will be used. + * @return string the generated style tag + */ + public static function style($content, $options = array()) + { + if (!isset($options['type'])) { + $options['type'] = 'text/css'; + } + return static::tag('style', "/**/", $options); + } + + /** + * Generates a script tag. + * @param string $content the script content + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * If the options does not contain "type", a "type" attribute with value "text/javascript" will be rendered. + * @return string the generated script tag + */ + public static function script($content, $options = array()) + { + if (!isset($options['type'])) { + $options['type'] = 'text/javascript'; + } + return static::tag('script', "/**/", $options); + } + + /** + * Generates a link tag that refers to an external CSS file. + * @param array|string $url the URL of the external CSS file. This parameter will be processed by [[url()]]. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated link tag + * @see url + */ + public static function cssFile($url, $options = array()) + { + $options['rel'] = 'stylesheet'; + $options['type'] = 'text/css'; + $options['href'] = static::url($url); + return static::tag('link', '', $options); + } + + /** + * Generates a script tag that refers to an external JavaScript file. + * @param string $url the URL of the external JavaScript file. This parameter will be processed by [[url()]]. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated script tag + * @see url + */ + public static function jsFile($url, $options = array()) + { + $options['type'] = 'text/javascript'; + $options['src'] = static::url($url); + return static::tag('script', '', $options); + } + + /** + * Generates a form start tag. + * @param array|string $action the form action URL. This parameter will be processed by [[url()]]. + * @param string $method the form submission method, either "post" or "get" (case-insensitive) + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated form start tag. + * @see endForm + */ + public static function beginForm($action = '', $method = 'post', $options = array()) + { + $action = static::url($action); + + // query parameters in the action are ignored for GET method + // we use hidden fields to add them back + $hiddens = array(); + if (!strcasecmp($method, 'get') && ($pos = strpos($action, '?')) !== false) { + foreach (explode('&', substr($action, $pos + 1)) as $pair) { + if (($pos1 = strpos($pair, '=')) !== false) { + $hiddens[] = static::hiddenInput(urldecode(substr($pair, 0, $pos1)), urldecode(substr($pair, $pos1 + 1))); + } else { + $hiddens[] = static::hiddenInput(urldecode($pair), ''); + } + } + $action = substr($action, 0, $pos); + } + + $options['action'] = $action; + $options['method'] = $method; + $form = static::beginTag('form', $options); + if ($hiddens !== array()) { + $form .= "\n" . implode("\n", $hiddens); + } + + return $form; + } + + /** + * Generates a form end tag. + * @return string the generated tag + * @see beginForm + */ + public static function endForm() + { + return ''; + } + + /** + * Generates a hyperlink tag. + * @param string $text link body. It will NOT be HTML-encoded. Therefore you can pass in HTML code + * such as an image tag. If this is is coming from end users, you should consider [[encode()]] + * it to prevent XSS attacks. + * @param array|string|null $url the URL for the hyperlink tag. This parameter will be processed by [[url()]] + * and will be used for the "href" attribute of the tag. If this parameter is null, the "href" attribute + * will not be generated. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated hyperlink + * @see url + */ + public static function a($text, $url = null, $options = array()) + { + if ($url !== null) { + $options['href'] = static::url($url); + } + return static::tag('a', $text, $options); + } + + /** + * Generates a mailto hyperlink. + * @param string $text link body. It will NOT be HTML-encoded. Therefore you can pass in HTML code + * such as an image tag. If this is is coming from end users, you should consider [[encode()]] + * it to prevent XSS attacks. + * @param string $email email address. If this is null, the first parameter (link body) will be treated + * as the email address and used. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated mailto link + */ + public static function mailto($text, $email = null, $options = array()) + { + return static::a($text, 'mailto:' . ($email === null ? $text : $email), $options); + } + + /** + * Generates an image tag. + * @param string $src the image URL. This parameter will be processed by [[url()]]. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated image tag + */ + public static function img($src, $options = array()) + { + $options['src'] = static::url($src); + if (!isset($options['alt'])) { + $options['alt'] = ''; + } + return static::tag('img', null, $options); + } + + /** + * Generates a label tag. + * @param string $content label text. It will NOT be HTML-encoded. Therefore you can pass in HTML code + * such as an image tag. If this is is coming from end users, you should consider [[encode()]] + * it to prevent XSS attacks. + * @param string $for the ID of the HTML element that this label is associated with. + * If this is null, the "for" attribute will not be generated. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated label tag + */ + public static function label($content, $for = null, $options = array()) + { + $options['for'] = $for; + return static::tag('label', $content, $options); + } + + /** + * Generates a button tag. + * @param string $name the name attribute. If it is null, the name attribute will not be generated. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param string $content the content enclosed within the button tag. It will NOT be HTML-encoded. + * Therefore you can pass in HTML code such as an image tag. If this is is coming from end users, + * you should consider [[encode()]] it to prevent XSS attacks. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * If the options does not contain "type", a "type" attribute with value "button" will be rendered. + * @return string the generated button tag + */ + public static function button($name = null, $value = null, $content = 'Button', $options = array()) + { + $options['name'] = $name; + $options['value'] = $value; + if (!isset($options['type'])) { + $options['type'] = 'button'; + } + return static::tag('button', $content, $options); + } + + /** + * Generates a submit button tag. + * @param string $name the name attribute. If it is null, the name attribute will not be generated. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param string $content the content enclosed within the button tag. It will NOT be HTML-encoded. + * Therefore you can pass in HTML code such as an image tag. If this is is coming from end users, + * you should consider [[encode()]] it to prevent XSS attacks. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated submit button tag + */ + public static function submitButton($name = null, $value = null, $content = 'Submit', $options = array()) + { + $options['type'] = 'submit'; + return static::button($name, $value, $content, $options); + } + + /** + * Generates a reset button tag. + * @param string $name the name attribute. If it is null, the name attribute will not be generated. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param string $content the content enclosed within the button tag. It will NOT be HTML-encoded. + * Therefore you can pass in HTML code such as an image tag. If this is is coming from end users, + * you should consider [[encode()]] it to prevent XSS attacks. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated reset button tag + */ + public static function resetButton($name = null, $value = null, $content = 'Reset', $options = array()) + { + $options['type'] = 'reset'; + return static::button($name, $value, $content, $options); + } + + /** + * Generates an input type of the given type. + * @param string $type the type attribute. + * @param string $name the name attribute. If it is null, the name attribute will not be generated. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated input tag + */ + public static function input($type, $name = null, $value = null, $options = array()) + { + $options['type'] = $type; + $options['name'] = $name; + $options['value'] = $value; + return static::tag('input', null, $options); + } + + /** + * Generates an input button. + * @param string $name the name attribute. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated button tag + */ + public static function buttonInput($name, $value = 'Button', $options = array()) + { + return static::input('button', $name, $value, $options); + } + + /** + * Generates a submit input button. + * @param string $name the name attribute. If it is null, the name attribute will not be generated. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated button tag + */ + public static function submitInput($name = null, $value = 'Submit', $options = array()) + { + return static::input('submit', $name, $value, $options); + } + + /** + * Generates a reset input button. + * @param string $name the name attribute. If it is null, the name attribute will not be generated. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param array $options the attributes of the button tag. The values will be HTML-encoded using [[encode()]]. + * Attributes whose value is null will be ignored and not put in the tag returned. + * @return string the generated button tag + */ + public static function resetInput($name = null, $value = 'Reset', $options = array()) + { + return static::input('reset', $name, $value, $options); + } + + /** + * Generates a text input field. + * @param string $name the name attribute. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated button tag + */ + public static function textInput($name, $value = null, $options = array()) + { + return static::input('text', $name, $value, $options); + } + + /** + * Generates a hidden input field. + * @param string $name the name attribute. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated button tag + */ + public static function hiddenInput($name, $value = null, $options = array()) + { + return static::input('hidden', $name, $value, $options); + } + + /** + * Generates a password input field. + * @param string $name the name attribute. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated button tag + */ + public static function passwordInput($name, $value = null, $options = array()) + { + return static::input('password', $name, $value, $options); + } + + /** + * Generates a file input field. + * To use a file input field, you should set the enclosing form's "enctype" attribute to + * be "multipart/form-data". After the form is submitted, the uploaded file information + * can be obtained via $_FILES[$name] (see PHP documentation). + * @param string $name the name attribute. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated button tag + */ + public static function fileInput($name, $value = null, $options = array()) + { + return static::input('file', $name, $value, $options); + } + + /** + * Generates a text area input. + * @param string $name the input name + * @param string $value the input value. Note that it will be encoded using [[encode()]]. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated text area tag + */ + public static function textarea($name, $value = '', $options = array()) + { + $options['name'] = $name; + return static::tag('textarea', static::encode($value), $options); + } + + /** + * Generates a radio button input. + * @param string $name the name attribute. + * @param boolean $checked whether the radio button should be checked. + * @param string $value the value attribute. If it is null, the value attribute will not be rendered. + * @param array $options the tag options in terms of name-value pairs. The following options are supported: + * + * - uncheck: string, the value associated with the uncheck state of the radio button. When this attribute + * is present, a hidden input will be generated so that if the radio button is not checked and is submitted, + * the value of this attribute will still be submitted to the server via the hidden input. + * + * The rest of the options will be rendered as the attributes of the resulting tag. The values will + * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. + * + * @return string the generated radio button tag + */ + public static function radio($name, $checked = false, $value = '1', $options = array()) + { + $options['checked'] = $checked; + $options['value'] = $value; + if (isset($options['uncheck'])) { + // add a hidden field so that if the radio button is not selected, it still submits a value + $hidden = static::hiddenInput($name, $options['uncheck']); + unset($options['uncheck']); + } else { + $hidden = ''; + } + return $hidden . static::input('radio', $name, $value, $options); + } + + /** + * Generates a checkbox input. + * @param string $name the name attribute. + * @param boolean $checked whether the checkbox should be checked. + * @param string $value the value attribute. If it is null, the value attribute will not be rendered. + * @param array $options the tag options in terms of name-value pairs. The following options are supported: + * + * - uncheck: string, the value associated with the uncheck state of the checkbox. When this attribute + * is present, a hidden input will be generated so that if the checkbox is not checked and is submitted, + * the value of this attribute will still be submitted to the server via the hidden input. + * + * The rest of the options will be rendered as the attributes of the resulting tag. The values will + * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. + * + * @return string the generated checkbox tag + */ + public static function checkbox($name, $checked = false, $value = '1', $options = array()) + { + $options['checked'] = $checked; + $options['value'] = $value; + if (isset($options['uncheck'])) { + // add a hidden field so that if the checkbox is not selected, it still submits a value + $hidden = static::hiddenInput($name, $options['uncheck']); + unset($options['uncheck']); + } else { + $hidden = ''; + } + return $hidden . static::input('checkbox', $name, $value, $options); + } + + /** + * Generates a drop-down list. + * @param string $name the input name + * @param string $selection the selected value + * @param array $items the option data items. The array keys are option values, and the array values + * are the corresponding option labels. The array can also be nested (i.e. some array values are arrays too). + * For each sub-array, an option group will be generated whose label is the key associated with the sub-array. + * If you have a list of data models, you may convert them into the format described above using + * [[\yii\util\ArrayHelper::map()]]. + * + * Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in + * the labels will also be HTML-encoded. + * @param array $options the tag options in terms of name-value pairs. The following options are supported: + * + * - prompt: string, a prompt text to be displayed as the first option; + * - options: array, the attributes for the select option tags. The array keys must be valid option values, + * and the array values are the extra attributes for the corresponding option tags. For example, + * + * ~~~ + * array( + * 'value1' => array('disabled' => true), + * 'value2' => array('label' => 'value 2'), + * ); + * ~~~ + * + * - groups: array, the attributes for the optgroup tags. The structure of this is similar to that of 'options', + * except that the array keys represent the optgroup labels specified in $items. + * + * The rest of the options will be rendered as the attributes of the resulting tag. The values will + * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. + * + * @return string the generated drop-down list tag + */ + public static function dropDownList($name, $selection = null, $items = array(), $options = array()) + { + $options['name'] = $name; + $selectOptions = static::renderSelectOptions($selection, $items, $options); + return static::tag('select', "\n" . $selectOptions . "\n", $options); + } + + /** + * Generates a list box. + * @param string $name the input name + * @param string|array $selection the selected value(s) + * @param array $items the option data items. The array keys are option values, and the array values + * are the corresponding option labels. The array can also be nested (i.e. some array values are arrays too). + * For each sub-array, an option group will be generated whose label is the key associated with the sub-array. + * If you have a list of data models, you may convert them into the format described above using + * [[\yii\util\ArrayHelper::map()]]. + * + * Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in + * the labels will also be HTML-encoded. + * @param array $options the tag options in terms of name-value pairs. The following options are supported: + * + * - prompt: string, a prompt text to be displayed as the first option; + * - options: array, the attributes for the select option tags. The array keys must be valid option values, + * and the array values are the extra attributes for the corresponding option tags. For example, + * + * ~~~ + * array( + * 'value1' => array('disabled' => true), + * 'value2' => array('label' => 'value 2'), + * ); + * ~~~ + * + * - groups: array, the attributes for the optgroup tags. The structure of this is similar to that of 'options', + * except that the array keys represent the optgroup labels specified in $items. + * - unselect: string, the value that will be submitted when no option is selected. + * When this attribute is set, a hidden field will be generated so that if no option is selected in multiple + * mode, we can still obtain the posted unselect value. + * + * The rest of the options will be rendered as the attributes of the resulting tag. The values will + * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. + * + * @return string the generated list box tag + */ + public static function listBox($name, $selection = null, $items = array(), $options = array()) + { + if (!isset($options['size'])) { + $options['size'] = 4; + } + if (isset($options['multiple']) && $options['multiple'] && substr($name, -2) !== '[]') { + $name .= '[]'; + } + $options['name'] = $name; + if (isset($options['unselect'])) { + // add a hidden field so that if the list box has no option being selected, it still submits a value + if (substr($name, -2) === '[]') { + $name = substr($name, 0, -2); + } + $hidden = static::hiddenInput($name, $options['unselect']); + unset($options['unselect']); + } else { + $hidden = ''; + } + $selectOptions = static::renderSelectOptions($selection, $items, $options); + return $hidden . static::tag('select', "\n" . $selectOptions . "\n", $options); + } + + /** + * Generates a list of checkboxes. + * A checkbox list allows multiple selection, like [[listBox()]]. + * As a result, the corresponding submitted value is an array. + * @param string $name the name attribute of each checkbox. + * @param string|array $selection the selected value(s). + * @param array $items the data item used to generate the checkboxes. + * The array keys are the labels, while the array values are the corresponding checkbox values. + * Note that the labels will NOT be HTML-encoded, while the values will. + * @param array $options options (name => config) for the checkbox list. The following options are supported: + * + * - unselect: string, the value that should be submitted when none of the checkboxes is selected. + * By setting this option, a hidden input will be generated. + * - separator: string, the HTML code that separates items. + * - item: callable, a callback that can be used to customize the generation of the HTML code + * corresponding to a single item in $items. The signature of this callback must be: + * + * ~~~ + * function ($index, $label, $name, $checked, $value) + * ~~~ + * + * where $index is the zero-based index of the checkbox in the whole list; $label + * is the label for the checkbox; and $name, $value and $checked represent the name, + * value and the checked status of the checkbox input. + * @return string the generated checkbox list + */ + public static function checkboxList($name, $selection = null, $items = array(), $options = array()) + { + if (substr($name, -2) !== '[]') { + $name .= '[]'; + } + + $formatter = isset($options['item']) ? $options['item'] : null; + $lines = array(); + $index = 0; + foreach ($items as $value => $label) { + $checked = $selection !== null && + (!is_array($selection) && !strcmp($value, $selection) + || is_array($selection) && in_array($value, $selection)); + if ($formatter !== null) { + $lines[] = call_user_func($formatter, $index, $label, $name, $checked, $value); + } else { + $lines[] = static::label(static::checkbox($name, $checked, $value) . ' ' . $label); + } + $index++; + } + + if (isset($options['unselect'])) { + // add a hidden field so that if the list box has no option being selected, it still submits a value + $name2 = substr($name, -2) === '[]' ? substr($name, 0, -2) : $name; + $hidden = static::hiddenInput($name2, $options['unselect']); + } else { + $hidden = ''; + } + $separator = isset($options['separator']) ? $options['separator'] : "\n"; + + return $hidden . implode($separator, $lines); + } + + /** + * Generates a list of radio buttons. + * A radio button list is like a checkbox list, except that it only allows single selection. + * @param string $name the name attribute of each radio button. + * @param string|array $selection the selected value(s). + * @param array $items the data item used to generate the radio buttons. + * The array keys are the labels, while the array values are the corresponding radio button values. + * Note that the labels will NOT be HTML-encoded, while the values will. + * @param array $options options (name => config) for the radio button list. The following options are supported: + * + * - unselect: string, the value that should be submitted when none of the radio buttons is selected. + * By setting this option, a hidden input will be generated. + * - separator: string, the HTML code that separates items. + * - item: callable, a callback that can be used to customize the generation of the HTML code + * corresponding to a single item in $items. The signature of this callback must be: + * + * ~~~ + * function ($index, $label, $name, $checked, $value) + * ~~~ + * + * where $index is the zero-based index of the radio button in the whole list; $label + * is the label for the radio button; and $name, $value and $checked represent the name, + * value and the checked status of the radio button input. + * @return string the generated radio button list + */ + public static function radioList($name, $selection = null, $items = array(), $options = array()) + { + $formatter = isset($options['item']) ? $options['item'] : null; + $lines = array(); + $index = 0; + foreach ($items as $value => $label) { + $checked = $selection !== null && + (!is_array($selection) && !strcmp($value, $selection) + || is_array($selection) && in_array($value, $selection)); + if ($formatter !== null) { + $lines[] = call_user_func($formatter, $index, $label, $name, $checked, $value); + } else { + $lines[] = static::label(static::radio($name, $checked, $value) . ' ' . $label); + } + $index++; + } + + $separator = isset($options['separator']) ? $options['separator'] : "\n"; + if (isset($options['unselect'])) { + // add a hidden field so that if the list box has no option being selected, it still submits a value + $hidden = static::hiddenInput($name, $options['unselect']); + } else { + $hidden = ''; + } + + return $hidden . implode($separator, $lines); + } + + /** + * Renders the option tags that can be used by [[dropDownList()]] and [[listBox()]]. + * @param string|array $selection the selected value(s). This can be either a string for single selection + * or an array for multiple selections. + * @param array $items the option data items. The array keys are option values, and the array values + * are the corresponding option labels. The array can also be nested (i.e. some array values are arrays too). + * For each sub-array, an option group will be generated whose label is the key associated with the sub-array. + * If you have a list of data models, you may convert them into the format described above using + * [[\yii\util\ArrayHelper::map()]]. + * + * Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in + * the labels will also be HTML-encoded. + * @param array $tagOptions the $options parameter that is passed to the [[dropDownList()]] or [[listBox()]] call. + * This method will take out these elements, if any: "prompt", "options" and "groups". See more details + * in [[dropDownList()]] for the explanation of these elements. + * + * @return string the generated list options + */ + public static function renderSelectOptions($selection, $items, &$tagOptions = array()) + { + $lines = array(); + if (isset($tagOptions['prompt'])) { + $prompt = str_replace(' ', ' ', static::encode($tagOptions['prompt'])); + $lines[] = static::tag('option', $prompt, array('value' => '')); + } + + $options = isset($tagOptions['options']) ? $tagOptions['options'] : array(); + $groups = isset($tagOptions['groups']) ? $tagOptions['groups'] : array(); + unset($tagOptions['prompt'], $tagOptions['options'], $tagOptions['groups']); + + foreach ($items as $key => $value) { + if (is_array($value)) { + $groupAttrs = isset($groups[$key]) ? $groups[$key] : array(); + $groupAttrs['label'] = $key; + $attrs = array('options' => $options, 'groups' => $groups); + $content = static::renderSelectOptions($selection, $value, $attrs); + $lines[] = static::tag('optgroup', "\n" . $content . "\n", $groupAttrs); + } else { + $attrs = isset($options[$key]) ? $options[$key] : array(); + $attrs['value'] = $key; + $attrs['selected'] = $selection !== null && + (!is_array($selection) && !strcmp($key, $selection) + || is_array($selection) && in_array($key, $selection)); + $lines[] = static::tag('option', str_replace(' ', ' ', static::encode($value)), $attrs); + } + } + + return implode("\n", $lines); + } + + /** + * Renders the HTML tag attributes. + * Boolean attributes such as s 'checked', 'disabled', 'readonly', will be handled specially + * according to [[booleanAttributes]] and [[showBooleanAttributeValues]]. + * @param array $attributes attributes to be rendered. The attribute values will be HTML-encoded using [[encode()]]. + * Attributes whose value is null will be ignored and not put in the rendering result. + * @return string the rendering result. If the attributes are not empty, they will be rendered + * into a string with a leading white space (such that it can be directly appended to the tag name + * in a tag. If there is no attribute, an empty string will be returned. + */ + public static function renderTagAttributes($attributes) + { + if (count($attributes) > 1) { + $sorted = array(); + foreach (static::$attributeOrder as $name) { + if (isset($attributes[$name])) { + $sorted[$name] = $attributes[$name]; + } + } + $attributes = array_merge($sorted, $attributes); + } + + $html = ''; + foreach ($attributes as $name => $value) { + if (isset(static::$booleanAttributes[strtolower($name)])) { + if ($value || strcasecmp($name, $value) === 0) { + $html .= static::$showBooleanAttributeValues ? " $name=\"$name\"" : " $name"; + } + } elseif ($value !== null) { + $html .= " $name=\"" . static::encode($value) . '"'; + } + } + return $html; + } + + /** + * Normalizes the input parameter to be a valid URL. + * + * 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 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')`. + * + * @param array|string $url the parameter to be used to generate a valid URL + * @return string the normalized URL + * @throws InvalidParamException if the parameter is invalid. + */ + public static function url($url) + { + if (is_array($url)) { + if (isset($url[0])) { + return Yii::$app->createUrl($url[0], array_splice($url, 1)); + } else { + throw new InvalidParamException('The array specifying a URL must contain at least one element.'); + } + } elseif ($url === '') { + return Yii::$app->getRequest()->getUrl(); + } else { + return Yii::getAlias($url); + } + } +} diff --git a/framework/helpers/SecurityHelper.php b/framework/helpers/SecurityHelper.php new file mode 100644 index 0000000..4186681 --- /dev/null +++ b/framework/helpers/SecurityHelper.php @@ -0,0 +1,272 @@ + + * @author Tom Worster + * @since 2.0 + */ +class SecurityHelper +{ + /** + * Encrypts data. + * @param string $data data to be encrypted. + * @param string $key the encryption secret key + * @return string the encrypted data + * @throws Exception if PHP Mcrypt extension is not loaded or failed to be initialized + * @see decrypt() + */ + public static function encrypt($data, $key) + { + $module = static::openCryptModule(); + $key = StringHelper::substr($key, 0, mcrypt_enc_get_key_size($module)); + srand(); + $iv = mcrypt_create_iv(mcrypt_enc_get_iv_size($module), MCRYPT_RAND); + mcrypt_generic_init($module, $key, $iv); + $encrypted = $iv . mcrypt_generic($module, $data); + mcrypt_generic_deinit($module); + mcrypt_module_close($module); + return $encrypted; + } + + /** + * Decrypts data + * @param string $data data to be decrypted. + * @param string $key the decryption secret key + * @return string the decrypted data + * @throws Exception if PHP Mcrypt extension is not loaded or failed to be initialized + * @see encrypt() + */ + public static function decrypt($data, $key) + { + $module = static::openCryptModule(); + $key = StringHelper::substr($key, 0, mcrypt_enc_get_key_size($module)); + $ivSize = mcrypt_enc_get_iv_size($module); + $iv = StringHelper::substr($data, 0, $ivSize); + mcrypt_generic_init($module, $key, $iv); + $decrypted = mdecrypt_generic($module, StringHelper::substr($data, $ivSize, StringHelper::strlen($data))); + mcrypt_generic_deinit($module); + mcrypt_module_close($module); + return rtrim($decrypted, "\0"); + } + + /** + * Prefixes data with a keyed hash value so that it can later be detected if it is tampered. + * @param string $data the data to be protected + * @param string $key the secret key to be used for generating hash + * @param string $algorithm the hashing algorithm (e.g. "md5", "sha1", "sha256", etc.). Call PHP "hash_algos()" + * function to see the supported hashing algorithms on your system. + * @return string the data prefixed with the keyed hash + * @see validateData() + * @see getSecretKey() + */ + public static function hashData($data, $key, $algorithm = 'sha256') + { + return hash_hmac($algorithm, $data, $key) . $data; + } + + /** + * Validates if the given data is tampered. + * @param string $data the data to be validated. The data must be previously + * generated by [[hashData()]]. + * @param string $key the secret key that was previously used to generate the hash for the data in [[hashData()]]. + * @param string $algorithm the hashing algorithm (e.g. "md5", "sha1", "sha256", etc.). Call PHP "hash_algos()" + * function to see the supported hashing algorithms on your system. This must be the same + * as the value passed to [[hashData()]] when generating the hash for the data. + * @return string the real data with the hash stripped off. False if the data is tampered. + * @see hashData() + */ + public static function validateData($data, $key, $algorithm = 'sha256') + { + $hashSize = StringHelper::strlen(hash_hmac($algorithm, 'test', $key)); + $n = StringHelper::strlen($data); + if ($n >= $hashSize) { + $hash = StringHelper::substr($data, 0, $hashSize); + $data2 = StringHelper::substr($data, $hashSize, $n - $hashSize); + return $hash === hash_hmac($algorithm, $data2, $key) ? $data2 : false; + } else { + return false; + } + } + + /** + * Returns a secret key associated with the specified name. + * If the secret key does not exist, a random key will be generated + * and saved in the file "keys.php" under the application's runtime directory + * so that the same secret key can be returned in future requests. + * @param string $name the name that is associated with the secret key + * @param integer $length the length of the key that should be generated if not exists + * @return string the secret key associated with the specified name + */ + public static function getSecretKey($name, $length = 32) + { + static $keys; + $keyFile = Yii::$app->getRuntimePath() . '/keys.php'; + if ($keys === null) { + $keys = is_file($keyFile) ? require($keyFile) : array(); + } + if (!isset($keys[$name])) { + // generate a 32-char random key + $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + $keys[$name] = substr(str_shuffle(str_repeat($chars, 5)), 0, $length); + file_put_contents($keyFile, " 30) { + throw new InvalidParamException('Hash is invalid.'); + } + + $test = crypt($password, $hash); + $n = strlen($test); + if (strlen($test) < 32 || $n !== strlen($hash)) { + return false; + } + + // Use a for-loop to compare two strings to prevent timing attacks. See: + // http://codereview.stackexchange.com/questions/13512 + $check = 0; + for ($i = 0; $i < $n; ++$i) { + $check |= (ord($test[$i]) ^ ord($hash[$i])); + } + + return $check === 0; + } + + /** + * Generates a salt that can be used to generate a password hash. + * + * The PHP [crypt()](http://php.net/manual/en/function.crypt.php) built-in function + * requires, for the Blowfish hash algorithm, a salt string in a specific format: + * "$2a$", "$2x$" or "$2y$", a two digit cost parameter, "$", and 22 characters + * from the alphabet "./0-9A-Za-z". + * + * @param integer $cost the cost parameter + * @return string the random salt value. + * @throws InvalidParamException if the cost parameter is not between 4 and 30 + */ + protected static function generateSalt($cost = 13) + { + $cost = (int)$cost; + if ($cost < 4 || $cost > 30) { + throw new InvalidParamException('Cost must be between 4 and 31.'); + } + + // Get 20 * 8bits of pseudo-random entropy from mt_rand(). + $rand = ''; + for ($i = 0; $i < 20; ++$i) { + $rand .= chr(mt_rand(0, 255)); + } + + // Add the microtime for a little more entropy. + $rand .= microtime(); + // Mix the bits cryptographically into a 20-byte binary string. + $rand = sha1($rand, true); + // Form the prefix that specifies Blowfish algorithm and cost parameter. + $salt = sprintf("$2y$%02d$", $cost); + // Append the random salt data in the required base64 format. + $salt .= str_replace('+', '.', substr(base64_encode($rand), 0, 22)); + return $salt; + } +} \ No newline at end of file diff --git a/framework/helpers/StringHelper.php b/framework/helpers/StringHelper.php new file mode 100644 index 0000000..3874701 --- /dev/null +++ b/framework/helpers/StringHelper.php @@ -0,0 +1,125 @@ + + * @author Alex Makarov + * @since 2.0 + */ +class StringHelper +{ + /** + * Returns the number of bytes in the given string. + * This method ensures the string is treated as a byte array. + * It will use `mb_strlen()` if it is available. + * @param string $string the string being measured for length + * @return integer the number of bytes in the given string. + */ + public static function strlen($string) + { + return function_exists('mb_strlen') ? mb_strlen($string, '8bit') : strlen($string); + } + + /** + * Returns the portion of string specified by the start and length parameters. + * This method ensures the string is treated as a byte array. + * It will use `mb_substr()` if it is available. + * @param string $string the input string. Must be one character or longer. + * @param integer $start the starting position + * @param integer $length the desired portion length + * @return string the extracted part of string, or FALSE on failure or an empty string. + * @see http://www.php.net/manual/en/function.substr.php + */ + public static function substr($string, $start, $length) + { + return function_exists('mb_substr') ? mb_substr($string, $start, $length, '8bit') : substr($string, $start, $length); + } + + /** + * Converts a word to its plural form. + * Note that this is for English only! + * For example, 'apple' will become 'apples', and 'child' will become 'children'. + * @param string $name the word to be pluralized + * @return string the pluralized word + */ + public static function pluralize($name) + { + static $rules = array( + '/(m)ove$/i' => '\1oves', + '/(f)oot$/i' => '\1eet', + '/(c)hild$/i' => '\1hildren', + '/(h)uman$/i' => '\1umans', + '/(m)an$/i' => '\1en', + '/(s)taff$/i' => '\1taff', + '/(t)ooth$/i' => '\1eeth', + '/(p)erson$/i' => '\1eople', + '/([m|l])ouse$/i' => '\1ice', + '/(x|ch|ss|sh|us|as|is|os)$/i' => '\1es', + '/([^aeiouy]|qu)y$/i' => '\1ies', + '/(?:([^f])fe|([lr])f)$/i' => '\1\2ves', + '/(shea|lea|loa|thie)f$/i' => '\1ves', + '/([ti])um$/i' => '\1a', + '/(tomat|potat|ech|her|vet)o$/i' => '\1oes', + '/(bu)s$/i' => '\1ses', + '/(ax|test)is$/i' => '\1es', + '/s$/' => 's', + ); + foreach ($rules as $rule => $replacement) { + if (preg_match($rule, $name)) { + return preg_replace($rule, $replacement, $name); + } + } + return $name . 's'; + } + + /** + * Converts a CamelCase name into space-separated words. + * For example, 'PostTag' will be converted to 'Post Tag'. + * @param string $name the string to be converted + * @param boolean $ucwords whether to capitalize the first letter in each word + * @return string the resulting words + */ + public static function camel2words($name, $ucwords = true) + { + $label = trim(strtolower(str_replace(array('-', '_', '.'), ' ', preg_replace('/(? + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2011 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\util; + +/** + * VarDumper is intended to replace the buggy PHP function var_dump and print_r. + * It can correctly identify the recursively referenced objects in a complex + * object structure. It also has a recursive depth control to avoid indefinite + * recursive display of some peculiar variables. + * + * VarDumper can be used as follows, + * + * ~~~ + * VarDumper::dump($var); + * ~~~ + * + * @author Qiang Xue + * @since 2.0 + */ +class CVarDumper +{ + private static $_objects; + private static $_output; + private static $_depth; + + /** + * Displays a variable. + * This method achieves the similar functionality as var_dump and print_r + * but is more robust when handling complex objects such as Yii controllers. + * @param mixed $var variable to be dumped + * @param integer $depth maximum depth that the dumper should go into the variable. Defaults to 10. + * @param boolean $highlight whether the result should be syntax-highlighted + */ + public static function dump($var, $depth = 10, $highlight = false) + { + echo self::dumpAsString($var, $depth, $highlight); + } + + /** + * Dumps a variable in terms of a string. + * This method achieves the similar functionality as var_dump and print_r + * but is more robust when handling complex objects such as Yii controllers. + * @param mixed $var variable to be dumped + * @param integer $depth maximum depth that the dumper should go into the variable. Defaults to 10. + * @param boolean $highlight whether the result should be syntax-highlighted + * @return string the string representation of the variable + */ + public static function dumpAsString($var, $depth = 10, $highlight = false) + { + self::$_output = ''; + self::$_objects = array(); + self::$_depth = $depth; + self::dumpInternal($var, 0); + if ($highlight) { + $result = highlight_string("/', '', $result, 1); + } + return self::$_output; + } + + /* + * @param mixed $var variable to be dumped + * @param integer $level depth level + */ + private static function dumpInternal($var, $level) + { + switch (gettype($var)) { + case 'boolean': + self::$_output .= $var ? 'true' : 'false'; + break; + case 'integer': + self::$_output .= "$var"; + break; + case 'double': + self::$_output .= "$var"; + break; + case 'string': + self::$_output .= "'" . addslashes($var) . "'"; + break; + case 'resource': + self::$_output .= '{resource}'; + break; + case 'NULL': + self::$_output .= "null"; + break; + case 'unknown type': + self::$_output .= '{unknown}'; + break; + case 'array': + if (self::$_depth <= $level) { + self::$_output .= 'array(...)'; + } elseif (empty($var)) { + self::$_output .= 'array()'; + } else { + $keys = array_keys($var); + $spaces = str_repeat(' ', $level * 4); + self::$_output .= "array\n" . $spaces . '('; + foreach ($keys as $key) { + self::$_output .= "\n" . $spaces . ' '; + self::dumpInternal($key, 0); + self::$_output .= ' => '; + self::dumpInternal($var[$key], $level + 1); + } + self::$_output .= "\n" . $spaces . ')'; + } + break; + case 'object': + if (($id = array_search($var, self::$_objects, true)) !== false) { + self::$_output .= get_class($var) . '#' . ($id + 1) . '(...)'; + } elseif (self::$_depth <= $level) { + self::$_output .= get_class($var) . '(...)'; + } else { + $id = self::$_objects[] = $var; + $className = get_class($var); + $members = (array)$var; + $spaces = str_repeat(' ', $level * 4); + self::$_output .= "$className#$id\n" . $spaces . '('; + foreach ($members as $key => $value) { + $keyDisplay = strtr(trim($key), array("\0" => ':')); + self::$_output .= "\n" . $spaces . " [$keyDisplay] => "; + self::dumpInternal($value, $level + 1); + } + self::$_output .= "\n" . $spaces . ')'; + } + break; + } + } +} \ No newline at end of file diff --git a/framework/helpers/mimeTypes.php b/framework/helpers/mimeTypes.php new file mode 100644 index 0000000..ffdba4b --- /dev/null +++ b/framework/helpers/mimeTypes.php @@ -0,0 +1,187 @@ + 'application/postscript', + 'aif' => 'audio/x-aiff', + 'aifc' => 'audio/x-aiff', + 'aiff' => 'audio/x-aiff', + 'anx' => 'application/annodex', + 'asc' => 'text/plain', + 'au' => 'audio/basic', + 'avi' => 'video/x-msvideo', + 'axa' => 'audio/annodex', + 'axv' => 'video/annodex', + 'bcpio' => 'application/x-bcpio', + 'bin' => 'application/octet-stream', + 'bmp' => 'image/bmp', + 'c' => 'text/plain', + 'cc' => 'text/plain', + 'ccad' => 'application/clariscad', + 'cdf' => 'application/x-netcdf', + 'class' => 'application/octet-stream', + 'cpio' => 'application/x-cpio', + 'cpt' => 'application/mac-compactpro', + 'csh' => 'application/x-csh', + 'css' => 'text/css', + 'dcr' => 'application/x-director', + 'dir' => 'application/x-director', + 'dms' => 'application/octet-stream', + 'doc' => 'application/msword', + 'drw' => 'application/drafting', + 'dvi' => 'application/x-dvi', + 'dwg' => 'application/acad', + 'dxf' => 'application/dxf', + 'dxr' => 'application/x-director', + 'eps' => 'application/postscript', + 'etx' => 'text/x-setext', + 'exe' => 'application/octet-stream', + 'ez' => 'application/andrew-inset', + 'f' => 'text/plain', + 'f90' => 'text/plain', + 'flac' => 'audio/flac', + 'fli' => 'video/x-fli', + 'flv' => 'video/x-flv', + 'gif' => 'image/gif', + 'gtar' => 'application/x-gtar', + 'gz' => 'application/x-gzip', + 'h' => 'text/plain', + 'hdf' => 'application/x-hdf', + 'hh' => 'text/plain', + 'hqx' => 'application/mac-binhex40', + 'htm' => 'text/html', + 'html' => 'text/html', + 'ice' => 'x-conference/x-cooltalk', + 'ief' => 'image/ief', + 'iges' => 'model/iges', + 'igs' => 'model/iges', + 'ips' => 'application/x-ipscript', + 'ipx' => 'application/x-ipix', + 'jpe' => 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'jpg' => 'image/jpeg', + 'js' => 'application/x-javascript', + 'kar' => 'audio/midi', + 'latex' => 'application/x-latex', + 'lha' => 'application/octet-stream', + 'lsp' => 'application/x-lisp', + 'lzh' => 'application/octet-stream', + 'm' => 'text/plain', + 'man' => 'application/x-troff-man', + 'me' => 'application/x-troff-me', + 'mesh' => 'model/mesh', + 'mid' => 'audio/midi', + 'midi' => 'audio/midi', + 'mif' => 'application/vnd.mif', + 'mime' => 'www/mime', + 'mov' => 'video/quicktime', + 'movie' => 'video/x-sgi-movie', + 'mp2' => 'audio/mpeg', + 'mp3' => 'audio/mpeg', + 'mpe' => 'video/mpeg', + 'mpeg' => 'video/mpeg', + 'mpg' => 'video/mpeg', + 'mpga' => 'audio/mpeg', + 'ms' => 'application/x-troff-ms', + 'msh' => 'model/mesh', + 'nc' => 'application/x-netcdf', + 'oga' => 'audio/ogg', + 'ogg' => 'audio/ogg', + 'ogv' => 'video/ogg', + 'ogx' => 'application/ogg', + 'oda' => 'application/oda', + 'pbm' => 'image/x-portable-bitmap', + 'pdb' => 'chemical/x-pdb', + 'pdf' => 'application/pdf', + 'pgm' => 'image/x-portable-graymap', + 'pgn' => 'application/x-chess-pgn', + 'png' => 'image/png', + 'pnm' => 'image/x-portable-anymap', + 'pot' => 'application/mspowerpoint', + 'ppm' => 'image/x-portable-pixmap', + 'pps' => 'application/mspowerpoint', + 'ppt' => 'application/mspowerpoint', + 'ppz' => 'application/mspowerpoint', + 'pre' => 'application/x-freelance', + 'prt' => 'application/pro_eng', + 'ps' => 'application/postscript', + 'qt' => 'video/quicktime', + 'ra' => 'audio/x-realaudio', + 'ram' => 'audio/x-pn-realaudio', + 'ras' => 'image/cmu-raster', + 'rgb' => 'image/x-rgb', + 'rm' => 'audio/x-pn-realaudio', + 'roff' => 'application/x-troff', + 'rpm' => 'audio/x-pn-realaudio-plugin', + 'rtf' => 'text/rtf', + 'rtx' => 'text/richtext', + 'scm' => 'application/x-lotusscreencam', + 'set' => 'application/set', + 'sgm' => 'text/sgml', + 'sgml' => 'text/sgml', + 'sh' => 'application/x-sh', + 'shar' => 'application/x-shar', + 'silo' => 'model/mesh', + 'sit' => 'application/x-stuffit', + 'skd' => 'application/x-koan', + 'skm' => 'application/x-koan', + 'skp' => 'application/x-koan', + 'skt' => 'application/x-koan', + 'smi' => 'application/smil', + 'smil' => 'application/smil', + 'snd' => 'audio/basic', + 'sol' => 'application/solids', + 'spl' => 'application/x-futuresplash', + 'spx' => 'audio/ogg', + 'src' => 'application/x-wais-source', + 'step' => 'application/STEP', + 'stl' => 'application/SLA', + 'stp' => 'application/STEP', + 'sv4cpio' => 'application/x-sv4cpio', + 'sv4crc' => 'application/x-sv4crc', + 'swf' => 'application/x-shockwave-flash', + 't' => 'application/x-troff', + 'tar' => 'application/x-tar', + 'tcl' => 'application/x-tcl', + 'tex' => 'application/x-tex', + 'texi' => 'application/x-texinfo', + 'texinfo' => 'application/x-texinfo', + 'tif' => 'image/tiff', + 'tiff' => 'image/tiff', + 'tr' => 'application/x-troff', + 'tsi' => 'audio/TSP-audio', + 'tsp' => 'application/dsptype', + 'tsv' => 'text/tab-separated-values', + 'txt' => 'text/plain', + 'unv' => 'application/i-deas', + 'ustar' => 'application/x-ustar', + 'vcd' => 'application/x-cdlink', + 'vda' => 'application/vda', + 'viv' => 'video/vnd.vivo', + 'vivo' => 'video/vnd.vivo', + 'vrml' => 'model/vrml', + 'wav' => 'audio/x-wav', + 'wrl' => 'model/vrml', + 'xbm' => 'image/x-xbitmap', + 'xlc' => 'application/vnd.ms-excel', + 'xll' => 'application/vnd.ms-excel', + 'xlm' => 'application/vnd.ms-excel', + 'xls' => 'application/vnd.ms-excel', + 'xlw' => 'application/vnd.ms-excel', + 'xml' => 'application/xml', + 'xpm' => 'image/x-xpixmap', + 'xspf' => 'application/xspf+xml', + 'xwd' => 'image/x-xwindowdump', + 'xyz' => 'chemical/x-pdb', + 'zip' => 'application/zip', +); diff --git a/framework/util/ArrayHelper.php b/framework/util/ArrayHelper.php deleted file mode 100644 index 447d034..0000000 --- a/framework/util/ArrayHelper.php +++ /dev/null @@ -1,340 +0,0 @@ - - * @since 2.0 - */ -class ArrayHelper -{ - /** - * Merges two or more arrays into one recursively. - * If each array has an element with the same string key value, the latter - * will overwrite the former (different from array_merge_recursive). - * Recursive merging will be conducted if both arrays have an element of array - * type and are having the same key. - * For integer-keyed elements, the elements from the latter array will - * be appended to the former array. - * @param array $a array to be merged to - * @param array $b array to be merged from. You can specify additional - * arrays via third argument, fourth argument etc. - * @return array the merged array (the original arrays are not changed.) - */ - public static function merge($a, $b) - { - $args = func_get_args(); - $res = array_shift($args); - while ($args !== array()) { - $next = array_shift($args); - foreach ($next as $k => $v) { - if (is_integer($k)) { - isset($res[$k]) ? $res[] = $v : $res[$k] = $v; - } elseif (is_array($v) && isset($res[$k]) && is_array($res[$k])) { - $res[$k] = self::merge($res[$k], $v); - } else { - $res[$k] = $v; - } - } - } - return $res; - } - - /** - * Retrieves the value of an array element or object property with the given key or property name. - * If the key does not exist in the array, the default value will be returned instead. - * - * Below are some usage examples, - * - * ~~~ - * // working with array - * $username = \yii\util\ArrayHelper::getValue($_POST, 'username'); - * // working with object - * $username = \yii\util\ArrayHelper::getValue($user, 'username'); - * // working with anonymous function - * $fullName = \yii\util\ArrayHelper::getValue($user, function($user, $defaultValue) { - * return $user->firstName . ' ' . $user->lastName; - * }); - * ~~~ - * - * @param array|object $array array or object to extract value from - * @param string|\Closure $key key name of the array element, or property name of the object, - * or an anonymous function returning the value. The anonymous function signature should be: - * `function($array, $defaultValue)`. - * @param mixed $default the default value to be returned if the specified key does not exist - * @return mixed the value of the - */ - public static function getValue($array, $key, $default = null) - { - if ($key instanceof \Closure) { - return $key($array, $default); - } elseif (is_array($array)) { - return isset($array[$key]) || array_key_exists($key, $array) ? $array[$key] : $default; - } else { - return $array->$key; - } - } - - /** - * Indexes an array according to a specified key. - * The input array should be multidimensional or an array of objects. - * - * The key can be a key name of the sub-array, a property name of object, or an anonymous - * function which returns the key value given an array element. - * - * If a key value is null, the corresponding array element will be discarded and not put in the result. - * - * For example, - * - * ~~~ - * $array = array( - * array('id' => '123', 'data' => 'abc'), - * array('id' => '345', 'data' => 'def'), - * ); - * $result = ArrayHelper::index($array, 'id'); - * // the result is: - * // array( - * // '123' => array('id' => '123', 'data' => 'abc'), - * // '345' => array('id' => '345', 'data' => 'def'), - * // ) - * - * // using anonymous function - * $result = ArrayHelper::index($array, function(element) { - * return $element['id']; - * }); - * ~~~ - * - * @param array $array the array that needs to be indexed - * @param string|\Closure $key the column name or anonymous function whose result will be used to index the array - * @return array the indexed array - */ - public static function index($array, $key) - { - $result = array(); - foreach ($array as $element) { - $value = static::getValue($element, $key); - $result[$value] = $element; - } - return $result; - } - - /** - * Returns the values of a specified column in an array. - * The input array should be multidimensional or an array of objects. - * - * For example, - * - * ~~~ - * $array = array( - * array('id' => '123', 'data' => 'abc'), - * array('id' => '345', 'data' => 'def'), - * ); - * $result = ArrayHelper::getColumn($array, 'id'); - * // the result is: array( '123', '345') - * - * // using anonymous function - * $result = ArrayHelper::getColumn($array, function(element) { - * return $element['id']; - * }); - * ~~~ - * - * @param array $array - * @param string|\Closure $name - * @param boolean $keepKeys whether to maintain the array keys. If false, the resulting array - * will be re-indexed with integers. - * @return array the list of column values - */ - public static function getColumn($array, $name, $keepKeys = true) - { - $result = array(); - if ($keepKeys) { - foreach ($array as $k => $element) { - $result[$k] = static::getValue($element, $name); - } - } else { - foreach ($array as $element) { - $result[] = static::getValue($element, $name); - } - } - - return $result; - } - - /** - * Builds a map (key-value pairs) from a multidimensional array or an array of objects. - * The `$from` and `$to` parameters specify the key names or property names to set up the map. - * Optionally, one can further group the map according to a grouping field `$group`. - * - * For example, - * - * ~~~ - * $array = array( - * array('id' => '123', 'name' => 'aaa', 'class' => 'x'), - * array('id' => '124', 'name' => 'bbb', 'class' => 'x'), - * array('id' => '345', 'name' => 'ccc', 'class' => 'y'), - * ); - * - * $result = ArrayHelper::map($array, 'id', 'name'); - * // the result is: - * // array( - * // '123' => 'aaa', - * // '124' => 'bbb', - * // '345' => 'ccc', - * // ) - * - * $result = ArrayHelper::map($array, 'id', 'name', 'class'); - * // the result is: - * // array( - * // 'x' => array( - * // '123' => 'aaa', - * // '124' => 'bbb', - * // ), - * // 'y' => array( - * // '345' => 'ccc', - * // ), - * // ) - * ~~~ - * - * @param array $array - * @param string|\Closure $from - * @param string|\Closure $to - * @param string|\Closure $group - * @return array - */ - public static function map($array, $from, $to, $group = null) - { - $result = array(); - foreach ($array as $element) { - $key = static::getValue($element, $from); - $value = static::getValue($element, $to); - if ($group !== null) { - $result[static::getValue($element, $group)][$key] = $value; - } else { - $result[$key] = $value; - } - } - return $result; - } - - /** - * Sorts an array of objects or arrays (with the same structure) by one or several keys. - * @param array $array the array to be sorted. The array will be modified after calling this method. - * @param string|\Closure|array $key the key(s) to be sorted by. This refers to a key name of the sub-array - * elements, a property name of the objects, or an anonymous function returning the values for comparison - * purpose. The anonymous function signature should be: `function($item)`. - * To sort by multiple keys, provide an array of keys here. - * @param boolean|array $ascending whether to sort in ascending or descending order. When - * sorting by multiple keys with different ascending orders, use an array of ascending flags. - * @param integer|array $sortFlag the PHP sort flag. Valid values include: - * `SORT_REGULAR`, `SORT_NUMERIC`, `SORT_STRING`, and `SORT_STRING | SORT_FLAG_CASE`. The last - * value is for sorting strings in case-insensitive manner. Please refer to - * See [PHP manual](http://php.net/manual/en/function.sort.php) for more details. - * When sorting by multiple keys with different sort flags, use an array of sort flags. - * @throws InvalidParamException if the $ascending or $sortFlag parameters do not have - * correct number of elements as that of $key. - */ - public static function multisort(&$array, $key, $ascending = true, $sortFlag = SORT_REGULAR) - { - $keys = is_array($key) ? $key : array($key); - if (empty($keys) || empty($array)) { - return; - } - $n = count($keys); - if (is_scalar($ascending)) { - $ascending = array_fill(0, $n, $ascending); - } elseif (count($ascending) !== $n) { - throw new InvalidParamException('The length of $ascending parameter must be the same as that of $keys.'); - } - if (is_scalar($sortFlag)) { - $sortFlag = array_fill(0, $n, $sortFlag); - } elseif (count($sortFlag) !== $n) { - throw new InvalidParamException('The length of $ascending parameter must be the same as that of $keys.'); - } - $args = array(); - foreach ($keys as $i => $key) { - $flag = $sortFlag[$i]; - if ($flag == (SORT_STRING | SORT_FLAG_CASE)) { - $flag = SORT_STRING; - $column = array(); - foreach (static::getColumn($array, $key) as $k => $value) { - $column[$k] = strtolower($value); - } - $args[] = $column; - } else { - $args[] = static::getColumn($array, $key); - } - $args[] = $ascending[$i] ? SORT_ASC : SORT_DESC; - $args[] = $flag; - } - $args[] = &$array; - call_user_func_array('array_multisort', $args); - } - - /** - * Encodes special characters in an array of strings into HTML entities. - * Both the array keys and values will be encoded. - * If a value is an array, this method will also encode it recursively. - * @param array $data data to be encoded - * @param boolean $valuesOnly whether to encode array values only. If false, - * both the array keys and array values will be encoded. - * @param string $charset the charset that the data is using. If not set, - * [[\yii\base\Application::charset]] will be used. - * @return array the encoded data - * @see http://www.php.net/manual/en/function.htmlspecialchars.php - */ - public static function htmlEncode($data, $valuesOnly = true, $charset = null) - { - if ($charset === null) { - $charset = Yii::$app->charset; - } - $d = array(); - foreach ($data as $key => $value) { - if (!$valuesOnly && is_string($key)) { - $key = htmlspecialchars($key, ENT_QUOTES, $charset); - } - if (is_string($value)) { - $d[$key] = htmlspecialchars($value, ENT_QUOTES, $charset); - } elseif (is_array($value)) { - $d[$key] = static::htmlEncode($value, $charset); - } - } - return $d; - } - - /** - * Decodes HTML entities into the corresponding characters in an array of strings. - * Both the array keys and values will be decoded. - * If a value is an array, this method will also decode it recursively. - * @param array $data data to be decoded - * @param boolean $valuesOnly whether to decode array values only. If false, - * both the array keys and array values will be decoded. - * @return array the decoded data - * @see http://www.php.net/manual/en/function.htmlspecialchars-decode.php - */ - public static function htmlDecode($data, $valuesOnly = true) - { - $d = array(); - foreach ($data as $key => $value) { - if (!$valuesOnly && is_string($key)) { - $key = htmlspecialchars_decode($key, ENT_QUOTES); - } - if (is_string($value)) { - $d[$key] = htmlspecialchars_decode($value, ENT_QUOTES); - } elseif (is_array($value)) { - $d[$key] = static::htmlDecode($value); - } - } - return $d; - } -} \ No newline at end of file diff --git a/framework/util/ConsoleColor.php b/framework/util/ConsoleColor.php deleted file mode 100644 index 74aa154..0000000 --- a/framework/util/ConsoleColor.php +++ /dev/null @@ -1,478 +0,0 @@ - - * @since 2.0 - */ -class ConsoleColor -{ - const FG_BLACK = 30; - const FG_RED = 31; - const FG_GREEN = 32; - const FG_YELLOW = 33; - const FG_BLUE = 34; - const FG_PURPLE = 35; - const FG_CYAN = 36; - const FG_GREY = 37; - - const BG_BLACK = 40; - const BG_RED = 41; - const BG_GREEN = 42; - const BG_YELLOW = 43; - const BG_BLUE = 44; - const BG_PURPLE = 45; - const BG_CYAN = 46; - const BG_GREY = 47; - - const BOLD = 1; - const ITALIC = 3; - const UNDERLINE = 4; - const BLINK = 5; - const NEGATIVE = 7; - const CONCEALED = 8; - const CROSSED_OUT = 9; - const FRAMED = 51; - const ENCIRCLED = 52; - const OVERLINED = 53; - - /** - * Moves the terminal cursor up by sending ANSI control code CUU to the terminal. - * If the cursor is already at the edge of the screen, this has no effect. - * @param integer $rows number of rows the cursor should be moved up - */ - public static function moveCursorUp($rows=1) - { - echo "\033[" . (int) $rows . 'A'; - } - - /** - * Moves the terminal cursor down by sending ANSI control code CUD to the terminal. - * If the cursor is already at the edge of the screen, this has no effect. - * @param integer $rows number of rows the cursor should be moved down - */ - public static function moveCursorDown($rows=1) - { - echo "\033[" . (int) $rows . 'B'; - } - - /** - * Moves the terminal cursor forward by sending ANSI control code CUF to the terminal. - * If the cursor is already at the edge of the screen, this has no effect. - * @param integer $steps number of steps the cursor should be moved forward - */ - public static function moveCursorForward($steps=1) - { - echo "\033[" . (int) $steps . 'C'; - } - - /** - * Moves the terminal cursor backward by sending ANSI control code CUB to the terminal. - * If the cursor is already at the edge of the screen, this has no effect. - * @param integer $steps number of steps the cursor should be moved backward - */ - public static function moveCursorBackward($steps=1) - { - echo "\033[" . (int) $steps . 'D'; - } - - /** - * Moves the terminal cursor to the beginning of the next line by sending ANSI control code CNL to the terminal. - * @param integer $lines number of lines the cursor should be moved down - */ - public static function moveCursorNextLine($lines=1) - { - echo "\033[" . (int) $lines . 'E'; - } - - /** - * Moves the terminal cursor to the beginning of the previous line by sending ANSI control code CPL to the terminal. - * @param integer $lines number of lines the cursor should be moved up - */ - public static function moveCursorPrevLine($lines=1) - { - echo "\033[" . (int) $lines . 'F'; - } - - /** - * Moves the cursor to an absolute position given as column and row by sending ANSI control code CUP or CHA to the terminal. - * @param integer $column 1-based column number, 1 is the left edge of the screen. - * @param integer|null $row 1-based row number, 1 is the top edge of the screen. if not set, will move cursor only in current line. - */ - public static function moveCursorTo($column, $row=null) - { - if ($row === null) { - echo "\033[" . (int) $column . 'G'; - } else { - echo "\033[" . (int) $row . ';' . (int) $column . 'H'; - } - } - - /** - * Scrolls whole page up by sending ANSI control code SU to the terminal. - * New lines are added at the bottom. This is not supported by ANSI.SYS used in windows. - * @param int $lines number of lines to scroll up - */ - public static function scrollUp($lines=1) - { - echo "\033[".(int)$lines."S"; - } - - /** - * Scrolls whole page down by sending ANSI control code SD to the terminal. - * New lines are added at the top. This is not supported by ANSI.SYS used in windows. - * @param int $lines number of lines to scroll down - */ - public static function scrollDown($lines=1) - { - echo "\033[".(int)$lines."T"; - } - - /** - * Saves the current cursor position by sending ANSI control code SCP to the terminal. - * Position can then be restored with {@link restoreCursorPosition}. - */ - public static function saveCursorPosition() - { - echo "\033[s"; - } - - /** - * Restores the cursor position saved with {@link saveCursorPosition} by sending ANSI control code RCP to the terminal. - */ - public static function restoreCursorPosition() - { - echo "\033[u"; - } - - /** - * Hides the cursor by sending ANSI DECTCEM code ?25l to the terminal. - * Use {@link showCursor} to bring it back. - * Do not forget to show cursor when your application exits. Cursor might stay hidden in terminal after exit. - */ - public static function hideCursor() - { - echo "\033[?25l"; - } - - /** - * Will show a cursor again when it has been hidden by {@link hideCursor} by sending ANSI DECTCEM code ?25h to the terminal. - */ - public static function showCursor() - { - echo "\033[?25h"; - } - - /** - * Clears entire screen content by sending ANSI control code ED with argument 2 to the terminal. - * Cursor position will not be changed. - * **Note:** ANSI.SYS implementation used in windows will reset cursor position to upper left corner of the screen. - */ - public static function clearScreen() - { - echo "\033[2J"; - } - - /** - * Clears text from cursor to the beginning of the screen by sending ANSI control code ED with argument 1 to the terminal. - * Cursor position will not be changed. - */ - public static function clearScreenBeforeCursor() - { - echo "\033[1J"; - } - - /** - * Clears text from cursor to the end of the screen by sending ANSI control code ED with argument 0 to the terminal. - * Cursor position will not be changed. - */ - public static function clearScreenAfterCursor() - { - echo "\033[0J"; - } - - /** - * Clears the line, the cursor is currently on by sending ANSI control code EL with argument 2 to the terminal. - * Cursor position will not be changed. - */ - public static function clearLine() - { - echo "\033[2K"; - } - - /** - * Clears text from cursor position to the beginning of the line by sending ANSI control code EL with argument 1 to the terminal. - * Cursor position will not be changed. - */ - public static function clearLineBeforeCursor() - { - echo "\033[1K"; - } - - /** - * Clears text from cursor position to the end of the line by sending ANSI control code EL with argument 0 to the terminal. - * Cursor position will not be changed. - */ - public static function clearLineAfterCursor() - { - echo "\033[0K"; - } - - /** - * Will send ANSI format for following output - * - * You can pass any of the FG_*, BG_* and TEXT_* constants and also xterm256ColorBg - * TODO: documentation - */ - public static function ansiStyle() - { - echo "\033[" . implode(';', func_get_args()) . 'm'; - } - - /** - * Will return a string formatted with the given ANSI style - * - * See {@link ansiStyle} for possible arguments. - * @param string $string the string to be formatted - * @return string - */ - public static function ansiStyleString($string) - { - $args = func_get_args(); - array_shift($args); - $code = implode(';', $args); - return "\033[0m" . ($code !== '' ? "\033[" . $code . "m" : '') . $string."\033[0m"; - } - - //const COLOR_XTERM256 = 38;// http://en.wikipedia.org/wiki/Talk:ANSI_escape_code#xterm-256colors - public static function xterm256ColorFg($i) // TODO naming! - { - return '38;5;'.$i; - } - - public static function xterm256ColorBg($i) // TODO naming! - { - return '48;5;'.$i; - } - - /** - * Usage: list($w, $h) = ConsoleHelper::getScreenSize(); - * - * @return array - */ - public static function getScreenSize() - { - // TODO implement - return array(150,50); - } - - /** - * resets any ansi style set by previous method {@link ansiStyle} - * Any output after this is will have default text style. - */ - public static function reset() - { - echo "\033[0m"; - } - - /** - * Strips ANSI control codes from a string - * - * @param string $string String to strip - * @return string - */ - public static function strip($string) - { - return preg_replace('/\033\[[\d;]+m/', '', $string); // TODO currently only strips color - } - - // TODO refactor and review - public static function ansiToHtml($string) - { - $tags = 0; - return preg_replace_callback('/\033\[[\d;]+m/', function($ansi) use (&$tags) { - $styleA = array(); - foreach(explode(';', $ansi) as $controlCode) - { - switch($controlCode) - { - case static::FG_BLACK: $style = array('color' => '#000000'); break; - case static::FG_BLUE: $style = array('color' => '#000078'); break; - case static::FG_CYAN: $style = array('color' => '#007878'); break; - case static::FG_GREEN: $style = array('color' => '#007800'); break; - case static::FG_GREY: $style = array('color' => '#787878'); break; - case static::FG_PURPLE: $style = array('color' => '#780078'); break; - case static::FG_RED: $style = array('color' => '#780000'); break; - case static::FG_YELLOW: $style = array('color' => '#787800'); break; - case static::BG_BLACK: $style = array('background-color' => '#000000'); break; - case static::BG_BLUE: $style = array('background-color' => '#000078'); break; - case static::BG_CYAN: $style = array('background-color' => '#007878'); break; - case static::BG_GREEN: $style = array('background-color' => '#007800'); break; - case static::BG_GREY: $style = array('background-color' => '#787878'); break; - case static::BG_PURPLE: $style = array('background-color' => '#780078'); break; - case static::BG_RED: $style = array('background-color' => '#780000'); break; - case static::BG_YELLOW: $style = array('background-color' => '#787800'); break; - case static::BOLD: $style = array('font-weight' => 'bold'); break; - case static::ITALIC: $style = array('font-style' => 'italic'); break; - case static::UNDERLINE: $style = array('text-decoration' => array('underline')); break; - case static::OVERLINED: $style = array('text-decoration' => array('overline')); break; - case static::CROSSED_OUT:$style = array('text-decoration' => array('line-through')); break; - case static::BLINK: $style = array('text-decoration' => array('blink')); break; - case static::NEGATIVE: // ??? - case static::CONCEALED: - case static::ENCIRCLED: - case static::FRAMED: - // TODO allow resetting codes - break; - case 0: // ansi reset - $return = ''; - for($n=$tags; $tags>0; $tags--) { - $return .= ''; - } - return $return; - } - - $styleA = ArrayHelper::merge($styleA, $style); - } - $styleString[] = array(); - foreach($styleA as $name => $content) { - if ($name === 'text-decoration') { - $content = implode(' ', $content); - } - $styleString[] = $name.':'.$content; - } - $tags++; - return ' $ds, '\\' => $ds)), $ds); - } - - /** - * Returns the localized version of a specified file. - * - * The searching is based on the specified language code. In particular, - * a file with the same name will be looked for under the subdirectory - * whose name is same as the language code. For example, given the file "path/to/view.php" - * and language code "zh_cn", the localized file will be looked for as - * "path/to/zh_cn/view.php". If the file is not found, the original file - * will be returned. - * - * If the target and the source language codes are the same, - * the original file will be returned. - * - * For consistency, it is recommended that the language code is given - * in lower case and in the format of LanguageID_RegionID (e.g. "en_us"). - * - * @param string $file the original file - * @param string $language the target language that the file should be localized to. - * If not set, the value of [[\yii\base\Application::language]] will be used. - * @param string $sourceLanguage the language that the original file is in. - * If not set, the value of [[\yii\base\Application::sourceLanguage]] will be used. - * @return string the matching localized file, or the original file if the localized version is not found. - * If the target and the source language codes are the same, the original file will be returned. - */ - public static function localize($file, $language = null, $sourceLanguage = null) - { - if ($language === null) { - $language = \Yii::$app->language; - } - if ($sourceLanguage === null) { - $sourceLanguage = \Yii::$app->sourceLanguage; - } - if ($language === $sourceLanguage) { - return $file; - } - $desiredFile = dirname($file) . DIRECTORY_SEPARATOR . $sourceLanguage . DIRECTORY_SEPARATOR . basename($file); - return is_file($desiredFile) ? $desiredFile : $file; - } - - /** - * Determines the MIME type of the specified file. - * This method will first try to determine the MIME type based on - * [finfo_open](http://php.net/manual/en/function.finfo-open.php). If this doesn't work, it will - * fall back to [[getMimeTypeByExtension()]]. - * @param string $file the file name. - * @param string $magicFile name of the optional magic database file, usually something like `/path/to/magic.mime`. - * This will be passed as the second parameter to [finfo_open](http://php.net/manual/en/function.finfo-open.php). - * @param boolean $checkExtension whether to use the file extension to determine the MIME type in case - * `finfo_open()` cannot determine it. - * @return string the MIME type (e.g. `text/plain`). Null is returned if the MIME type cannot be determined. - */ - public static function getMimeType($file, $magicFile = null, $checkExtension = true) - { - if (function_exists('finfo_open')) { - $info = finfo_open(FILEINFO_MIME_TYPE, $magicFile); - if ($info && ($result = finfo_file($info, $file)) !== false) { - return $result; - } - } - - return $checkExtension ? self::getMimeTypeByExtension($file) : null; - } - - /** - * Determines the MIME type based on the extension name of the specified file. - * This method will use a local map between extension names and MIME types. - * @param string $file the file name. - * @param string $magicFile the path of the file that contains all available MIME type information. - * If this is not set, the default file aliased by `@yii/util/mimeTypes.php` will be used. - * @return string the MIME type. Null is returned if the MIME type cannot be determined. - */ - public static function getMimeTypeByExtension($file, $magicFile = null) - { - if ($magicFile === null) { - $magicFile = \Yii::getAlias('@yii/util/mimeTypes.php'); - } - $mimeTypes = require($magicFile); - if (($ext = pathinfo($file, PATHINFO_EXTENSION)) !== '') { - $ext = strtolower($ext); - if (isset($mimeTypes[$ext])) { - return $mimeTypes[$ext]; - } - } - return null; - } - - /** - * Copies a list of files from one place to another. - * @param array $fileList the list of files to be copied (name=>spec). - * The array keys are names displayed during the copy process, and array values are specifications - * for files to be copied. Each array value must be an array of the following structure: - *
                  - *
                • source: required, the full path of the file/directory to be copied from
                • - *
                • target: required, the full path of the file/directory to be copied to
                • - *
                • callback: optional, the callback to be invoked when copying a file. The callback function - * should be declared as follows: - *
                  -	 *   function foo($source,$params)
                  -	 *   
                  - * where $source parameter is the source file path, and the content returned - * by the function will be saved into the target file.
                • - *
                • params: optional, the parameters to be passed to the callback
                • - *
                - * @see buildFileList - */ - public static function copyFiles($fileList) - { - $overwriteAll = false; - foreach($fileList as $name=>$file) { - $source = strtr($file['source'], '/\\', DIRECTORY_SEPARATOR); - $target = strtr($file['target'], '/\\', DIRECTORY_SEPARATOR); - $callback = isset($file['callback']) ? $file['callback'] : null; - $params = isset($file['params']) ? $file['params'] : null; - - if(is_dir($source)) { - try { - self::ensureDirectory($target); - } - catch (Exception $e) { - mkdir($target, true, 0777); - } - continue; - } - - if($callback !== null) { - $content = call_user_func($callback, $source, $params); - } - else { - $content = file_get_contents($source); - } - if(is_file($target)) { - if($content === file_get_contents($target)) { - echo " unchanged $name\n"; - continue; - } - if($overwriteAll) { - echo " overwrite $name\n"; - } - else { - echo " exist $name\n"; - echo " ...overwrite? [Yes|No|All|Quit] "; - $answer = trim(fgets(STDIN)); - if(!strncasecmp($answer, 'q', 1)) { - return; - } - elseif(!strncasecmp($answer, 'y', 1)) { - echo " overwrite $name\n"; - } - elseif(!strncasecmp($answer, 'a', 1)) { - echo " overwrite $name\n"; - $overwriteAll = true; - } - else { - echo " skip $name\n"; - continue; - } - } - } - else { - try { - self::ensureDirectory(dirname($target)); - } - catch (Exception $e) { - mkdir(dirname($target), true, 0777); - } - echo " generate $name\n"; - } - file_put_contents($target, $content); - } - } - - /** - * Builds the file list of a directory. - * This method traverses through the specified directory and builds - * a list of files and subdirectories that the directory contains. - * The result of this function can be passed to {@link copyFiles}. - * @param string $sourceDir the source directory - * @param string $targetDir the target directory - * @param string $baseDir base directory - * @param array $ignoreFiles list of the names of files that should - * be ignored in list building process. Argument available since 1.1.11. - * @param array $renameMap hash array of file names that should be - * renamed. Example value: array('1.old.txt'=>'2.new.txt'). - * @return array the file list (see {@link copyFiles}) - */ - public static function buildFileList($sourceDir, $targetDir, $baseDir='', $ignoreFiles=array(), $renameMap=array()) - { - $list = array(); - $handle = opendir($sourceDir); - while(($file = readdir($handle)) !== false) { - if(in_array($file, array('.', '..', '.svn', '.gitignore')) || in_array($file, $ignoreFiles)) { - continue; - } - $sourcePath = $sourceDir.DIRECTORY_SEPARATOR.$file; - $targetPath = $targetDir.DIRECTORY_SEPARATOR.strtr($file, $renameMap); - $name = $baseDir === '' ? $file : $baseDir.'/'.$file; - $list[$name] = array( - 'source' => $sourcePath, - 'target' => $targetPath, - ); - if(is_dir($sourcePath)) { - $list = array_merge($list, self::buildFileList($sourcePath, $targetPath, $name, $ignoreFiles, $renameMap)); - } - } - closedir($handle); - return $list; - } -} \ No newline at end of file diff --git a/framework/util/Html.php b/framework/util/Html.php deleted file mode 100644 index a7b744b..0000000 --- a/framework/util/Html.php +++ /dev/null @@ -1,976 +0,0 @@ - - * @since 2.0 - */ -class Html -{ - /** - * @var boolean whether to close void (empty) elements. Defaults to true. - * @see voidElements - */ - public static $closeVoidElements = true; - /** - * @var array list of void elements (element name => 1) - * @see closeVoidElements - * @see http://www.w3.org/TR/html-markup/syntax.html#void-element - */ - public static $voidElements = array( - 'area' => 1, - 'base' => 1, - 'br' => 1, - 'col' => 1, - 'command' => 1, - 'embed' => 1, - 'hr' => 1, - 'img' => 1, - 'input' => 1, - 'keygen' => 1, - 'link' => 1, - 'meta' => 1, - 'param' => 1, - 'source' => 1, - 'track' => 1, - 'wbr' => 1, - ); - /** - * @var boolean whether to show the values of boolean attributes in element tags. - * If false, only the attribute names will be generated. - * @see booleanAttributes - */ - public static $showBooleanAttributeValues = true; - /** - * @var array list of boolean attributes. The presence of a boolean attribute on - * an element represents the true value, and the absence of the attribute represents the false value. - * @see showBooleanAttributeValues - * @see http://www.w3.org/TR/html5/infrastructure.html#boolean-attributes - */ - public static $booleanAttributes = array( - 'async' => 1, - 'autofocus' => 1, - 'autoplay' => 1, - 'checked' => 1, - 'controls' => 1, - 'declare' => 1, - 'default' => 1, - 'defer' => 1, - 'disabled' => 1, - 'formnovalidate' => 1, - 'hidden' => 1, - 'ismap' => 1, - 'loop' => 1, - 'multiple' => 1, - 'muted' => 1, - 'nohref' => 1, - 'noresize' => 1, - 'novalidate' => 1, - 'open' => 1, - 'readonly' => 1, - 'required' => 1, - 'reversed' => 1, - 'scoped' => 1, - 'seamless' => 1, - 'selected' => 1, - 'typemustmatch' => 1, - ); - /** - * @var array the preferred order of attributes in a tag. This mainly affects the order of the attributes - * that are rendered by [[renderAttributes()]]. - */ - public static $attributeOrder = array( - 'type', - 'id', - 'class', - 'name', - 'value', - - 'href', - 'src', - 'action', - 'method', - - 'selected', - 'checked', - 'readonly', - 'disabled', - 'multiple', - - 'size', - 'maxlength', - 'width', - 'height', - 'rows', - 'cols', - - 'alt', - 'title', - 'rel', - 'media', - ); - - /** - * Encodes special characters into HTML entities. - * The [[yii\base\Application::charset|application charset]] will be used for encoding. - * @param string $content the content to be encoded - * @return string the encoded content - * @see decode - * @see http://www.php.net/manual/en/function.htmlspecialchars.php - */ - public static function encode($content) - { - return htmlspecialchars($content, ENT_QUOTES, Yii::$app->charset); - } - - /** - * Decodes special HTML entities back to the corresponding characters. - * This is the opposite of [[encode()]]. - * @param string $content the content to be decoded - * @return string the decoded content - * @see encode - * @see http://www.php.net/manual/en/function.htmlspecialchars-decode.php - */ - public static function decode($content) - { - return htmlspecialchars_decode($content, ENT_QUOTES); - } - - /** - * Generates a complete HTML tag. - * @param string $name the tag name - * @param string $content the content to be enclosed between the start and end tags. It will not be HTML-encoded. - * If this is coming from end users, you should consider [[encode()]] it to prevent XSS attacks. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * @return string the generated HTML tag - * @see beginTag - * @see endTag - */ - public static function tag($name, $content = '', $options = array()) - { - $html = '<' . $name . static::renderTagAttributes($options); - if (isset(static::$voidElements[strtolower($name)])) { - return $html . (static::$closeVoidElements ? ' />' : '>'); - } else { - return $html . ">$content"; - } - } - - /** - * Generates a start tag. - * @param string $name the tag name - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * @return string the generated start tag - * @see endTag - * @see tag - */ - public static function beginTag($name, $options = array()) - { - return '<' . $name . static::renderTagAttributes($options) . '>'; - } - - /** - * Generates an end tag. - * @param string $name the tag name - * @return string the generated end tag - * @see beginTag - * @see tag - */ - public static function endTag($name) - { - return ""; - } - - /** - * Encloses the given content within a CDATA tag. - * @param string $content the content to be enclosed within the CDATA tag - * @return string the CDATA tag with the enclosed content. - */ - public static function cdata($content) - { - return ''; - } - - /** - * Generates a style tag. - * @param string $content the style content - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * If the options does not contain "type", a "type" attribute with value "text/css" will be used. - * @return string the generated style tag - */ - public static function style($content, $options = array()) - { - if (!isset($options['type'])) { - $options['type'] = 'text/css'; - } - return static::tag('style', "/**/", $options); - } - - /** - * Generates a script tag. - * @param string $content the script content - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * If the options does not contain "type", a "type" attribute with value "text/javascript" will be rendered. - * @return string the generated script tag - */ - public static function script($content, $options = array()) - { - if (!isset($options['type'])) { - $options['type'] = 'text/javascript'; - } - return static::tag('script', "/**/", $options); - } - - /** - * Generates a link tag that refers to an external CSS file. - * @param array|string $url the URL of the external CSS file. This parameter will be processed by [[url()]]. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * @return string the generated link tag - * @see url - */ - public static function cssFile($url, $options = array()) - { - $options['rel'] = 'stylesheet'; - $options['type'] = 'text/css'; - $options['href'] = static::url($url); - return static::tag('link', '', $options); - } - - /** - * Generates a script tag that refers to an external JavaScript file. - * @param string $url the URL of the external JavaScript file. This parameter will be processed by [[url()]]. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * @return string the generated script tag - * @see url - */ - public static function jsFile($url, $options = array()) - { - $options['type'] = 'text/javascript'; - $options['src'] = static::url($url); - return static::tag('script', '', $options); - } - - /** - * Generates a form start tag. - * @param array|string $action the form action URL. This parameter will be processed by [[url()]]. - * @param string $method the form submission method, either "post" or "get" (case-insensitive) - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * @return string the generated form start tag. - * @see endForm - */ - public static function beginForm($action = '', $method = 'post', $options = array()) - { - $action = static::url($action); - - // query parameters in the action are ignored for GET method - // we use hidden fields to add them back - $hiddens = array(); - if (!strcasecmp($method, 'get') && ($pos = strpos($action, '?')) !== false) { - foreach (explode('&', substr($action, $pos + 1)) as $pair) { - if (($pos1 = strpos($pair, '=')) !== false) { - $hiddens[] = static::hiddenInput(urldecode(substr($pair, 0, $pos1)), urldecode(substr($pair, $pos1 + 1))); - } else { - $hiddens[] = static::hiddenInput(urldecode($pair), ''); - } - } - $action = substr($action, 0, $pos); - } - - $options['action'] = $action; - $options['method'] = $method; - $form = static::beginTag('form', $options); - if ($hiddens !== array()) { - $form .= "\n" . implode("\n", $hiddens); - } - - return $form; - } - - /** - * Generates a form end tag. - * @return string the generated tag - * @see beginForm - */ - public static function endForm() - { - return ''; - } - - /** - * Generates a hyperlink tag. - * @param string $text link body. It will NOT be HTML-encoded. Therefore you can pass in HTML code - * such as an image tag. If this is is coming from end users, you should consider [[encode()]] - * it to prevent XSS attacks. - * @param array|string|null $url the URL for the hyperlink tag. This parameter will be processed by [[url()]] - * and will be used for the "href" attribute of the tag. If this parameter is null, the "href" attribute - * will not be generated. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * @return string the generated hyperlink - * @see url - */ - public static function a($text, $url = null, $options = array()) - { - if ($url !== null) { - $options['href'] = static::url($url); - } - return static::tag('a', $text, $options); - } - - /** - * Generates a mailto hyperlink. - * @param string $text link body. It will NOT be HTML-encoded. Therefore you can pass in HTML code - * such as an image tag. If this is is coming from end users, you should consider [[encode()]] - * it to prevent XSS attacks. - * @param string $email email address. If this is null, the first parameter (link body) will be treated - * as the email address and used. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * @return string the generated mailto link - */ - public static function mailto($text, $email = null, $options = array()) - { - return static::a($text, 'mailto:' . ($email === null ? $text : $email), $options); - } - - /** - * Generates an image tag. - * @param string $src the image URL. This parameter will be processed by [[url()]]. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * @return string the generated image tag - */ - public static function img($src, $options = array()) - { - $options['src'] = static::url($src); - if (!isset($options['alt'])) { - $options['alt'] = ''; - } - return static::tag('img', null, $options); - } - - /** - * Generates a label tag. - * @param string $content label text. It will NOT be HTML-encoded. Therefore you can pass in HTML code - * such as an image tag. If this is is coming from end users, you should consider [[encode()]] - * it to prevent XSS attacks. - * @param string $for the ID of the HTML element that this label is associated with. - * If this is null, the "for" attribute will not be generated. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * @return string the generated label tag - */ - public static function label($content, $for = null, $options = array()) - { - $options['for'] = $for; - return static::tag('label', $content, $options); - } - - /** - * Generates a button tag. - * @param string $name the name attribute. If it is null, the name attribute will not be generated. - * @param string $value the value attribute. If it is null, the value attribute will not be generated. - * @param string $content the content enclosed within the button tag. It will NOT be HTML-encoded. - * Therefore you can pass in HTML code such as an image tag. If this is is coming from end users, - * you should consider [[encode()]] it to prevent XSS attacks. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * If the options does not contain "type", a "type" attribute with value "button" will be rendered. - * @return string the generated button tag - */ - public static function button($name = null, $value = null, $content = 'Button', $options = array()) - { - $options['name'] = $name; - $options['value'] = $value; - if (!isset($options['type'])) { - $options['type'] = 'button'; - } - return static::tag('button', $content, $options); - } - - /** - * Generates a submit button tag. - * @param string $name the name attribute. If it is null, the name attribute will not be generated. - * @param string $value the value attribute. If it is null, the value attribute will not be generated. - * @param string $content the content enclosed within the button tag. It will NOT be HTML-encoded. - * Therefore you can pass in HTML code such as an image tag. If this is is coming from end users, - * you should consider [[encode()]] it to prevent XSS attacks. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * @return string the generated submit button tag - */ - public static function submitButton($name = null, $value = null, $content = 'Submit', $options = array()) - { - $options['type'] = 'submit'; - return static::button($name, $value, $content, $options); - } - - /** - * Generates a reset button tag. - * @param string $name the name attribute. If it is null, the name attribute will not be generated. - * @param string $value the value attribute. If it is null, the value attribute will not be generated. - * @param string $content the content enclosed within the button tag. It will NOT be HTML-encoded. - * Therefore you can pass in HTML code such as an image tag. If this is is coming from end users, - * you should consider [[encode()]] it to prevent XSS attacks. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * @return string the generated reset button tag - */ - public static function resetButton($name = null, $value = null, $content = 'Reset', $options = array()) - { - $options['type'] = 'reset'; - return static::button($name, $value, $content, $options); - } - - /** - * Generates an input type of the given type. - * @param string $type the type attribute. - * @param string $name the name attribute. If it is null, the name attribute will not be generated. - * @param string $value the value attribute. If it is null, the value attribute will not be generated. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * @return string the generated input tag - */ - public static function input($type, $name = null, $value = null, $options = array()) - { - $options['type'] = $type; - $options['name'] = $name; - $options['value'] = $value; - return static::tag('input', null, $options); - } - - /** - * Generates an input button. - * @param string $name the name attribute. - * @param string $value the value attribute. If it is null, the value attribute will not be generated. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * @return string the generated button tag - */ - public static function buttonInput($name, $value = 'Button', $options = array()) - { - return static::input('button', $name, $value, $options); - } - - /** - * Generates a submit input button. - * @param string $name the name attribute. If it is null, the name attribute will not be generated. - * @param string $value the value attribute. If it is null, the value attribute will not be generated. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * @return string the generated button tag - */ - public static function submitInput($name = null, $value = 'Submit', $options = array()) - { - return static::input('submit', $name, $value, $options); - } - - /** - * Generates a reset input button. - * @param string $name the name attribute. If it is null, the name attribute will not be generated. - * @param string $value the value attribute. If it is null, the value attribute will not be generated. - * @param array $options the attributes of the button tag. The values will be HTML-encoded using [[encode()]]. - * Attributes whose value is null will be ignored and not put in the tag returned. - * @return string the generated button tag - */ - public static function resetInput($name = null, $value = 'Reset', $options = array()) - { - return static::input('reset', $name, $value, $options); - } - - /** - * Generates a text input field. - * @param string $name the name attribute. - * @param string $value the value attribute. If it is null, the value attribute will not be generated. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * @return string the generated button tag - */ - public static function textInput($name, $value = null, $options = array()) - { - return static::input('text', $name, $value, $options); - } - - /** - * Generates a hidden input field. - * @param string $name the name attribute. - * @param string $value the value attribute. If it is null, the value attribute will not be generated. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * @return string the generated button tag - */ - public static function hiddenInput($name, $value = null, $options = array()) - { - return static::input('hidden', $name, $value, $options); - } - - /** - * Generates a password input field. - * @param string $name the name attribute. - * @param string $value the value attribute. If it is null, the value attribute will not be generated. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * @return string the generated button tag - */ - public static function passwordInput($name, $value = null, $options = array()) - { - return static::input('password', $name, $value, $options); - } - - /** - * Generates a file input field. - * To use a file input field, you should set the enclosing form's "enctype" attribute to - * be "multipart/form-data". After the form is submitted, the uploaded file information - * can be obtained via $_FILES[$name] (see PHP documentation). - * @param string $name the name attribute. - * @param string $value the value attribute. If it is null, the value attribute will not be generated. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * @return string the generated button tag - */ - public static function fileInput($name, $value = null, $options = array()) - { - return static::input('file', $name, $value, $options); - } - - /** - * Generates a text area input. - * @param string $name the input name - * @param string $value the input value. Note that it will be encoded using [[encode()]]. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * @return string the generated text area tag - */ - public static function textarea($name, $value = '', $options = array()) - { - $options['name'] = $name; - return static::tag('textarea', static::encode($value), $options); - } - - /** - * Generates a radio button input. - * @param string $name the name attribute. - * @param boolean $checked whether the radio button should be checked. - * @param string $value the value attribute. If it is null, the value attribute will not be rendered. - * @param array $options the tag options in terms of name-value pairs. The following options are supported: - * - * - uncheck: string, the value associated with the uncheck state of the radio button. When this attribute - * is present, a hidden input will be generated so that if the radio button is not checked and is submitted, - * the value of this attribute will still be submitted to the server via the hidden input. - * - * The rest of the options will be rendered as the attributes of the resulting tag. The values will - * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. - * - * @return string the generated radio button tag - */ - public static function radio($name, $checked = false, $value = '1', $options = array()) - { - $options['checked'] = $checked; - $options['value'] = $value; - if (isset($options['uncheck'])) { - // add a hidden field so that if the radio button is not selected, it still submits a value - $hidden = static::hiddenInput($name, $options['uncheck']); - unset($options['uncheck']); - } else { - $hidden = ''; - } - return $hidden . static::input('radio', $name, $value, $options); - } - - /** - * Generates a checkbox input. - * @param string $name the name attribute. - * @param boolean $checked whether the checkbox should be checked. - * @param string $value the value attribute. If it is null, the value attribute will not be rendered. - * @param array $options the tag options in terms of name-value pairs. The following options are supported: - * - * - uncheck: string, the value associated with the uncheck state of the checkbox. When this attribute - * is present, a hidden input will be generated so that if the checkbox is not checked and is submitted, - * the value of this attribute will still be submitted to the server via the hidden input. - * - * The rest of the options will be rendered as the attributes of the resulting tag. The values will - * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. - * - * @return string the generated checkbox tag - */ - public static function checkbox($name, $checked = false, $value = '1', $options = array()) - { - $options['checked'] = $checked; - $options['value'] = $value; - if (isset($options['uncheck'])) { - // add a hidden field so that if the checkbox is not selected, it still submits a value - $hidden = static::hiddenInput($name, $options['uncheck']); - unset($options['uncheck']); - } else { - $hidden = ''; - } - return $hidden . static::input('checkbox', $name, $value, $options); - } - - /** - * Generates a drop-down list. - * @param string $name the input name - * @param string $selection the selected value - * @param array $items the option data items. The array keys are option values, and the array values - * are the corresponding option labels. The array can also be nested (i.e. some array values are arrays too). - * For each sub-array, an option group will be generated whose label is the key associated with the sub-array. - * If you have a list of data models, you may convert them into the format described above using - * [[\yii\util\ArrayHelper::map()]]. - * - * Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in - * the labels will also be HTML-encoded. - * @param array $options the tag options in terms of name-value pairs. The following options are supported: - * - * - prompt: string, a prompt text to be displayed as the first option; - * - options: array, the attributes for the select option tags. The array keys must be valid option values, - * and the array values are the extra attributes for the corresponding option tags. For example, - * - * ~~~ - * array( - * 'value1' => array('disabled' => true), - * 'value2' => array('label' => 'value 2'), - * ); - * ~~~ - * - * - groups: array, the attributes for the optgroup tags. The structure of this is similar to that of 'options', - * except that the array keys represent the optgroup labels specified in $items. - * - * The rest of the options will be rendered as the attributes of the resulting tag. The values will - * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. - * - * @return string the generated drop-down list tag - */ - public static function dropDownList($name, $selection = null, $items = array(), $options = array()) - { - $options['name'] = $name; - $selectOptions = static::renderSelectOptions($selection, $items, $options); - return static::tag('select', "\n" . $selectOptions . "\n", $options); - } - - /** - * Generates a list box. - * @param string $name the input name - * @param string|array $selection the selected value(s) - * @param array $items the option data items. The array keys are option values, and the array values - * are the corresponding option labels. The array can also be nested (i.e. some array values are arrays too). - * For each sub-array, an option group will be generated whose label is the key associated with the sub-array. - * If you have a list of data models, you may convert them into the format described above using - * [[\yii\util\ArrayHelper::map()]]. - * - * Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in - * the labels will also be HTML-encoded. - * @param array $options the tag options in terms of name-value pairs. The following options are supported: - * - * - prompt: string, a prompt text to be displayed as the first option; - * - options: array, the attributes for the select option tags. The array keys must be valid option values, - * and the array values are the extra attributes for the corresponding option tags. For example, - * - * ~~~ - * array( - * 'value1' => array('disabled' => true), - * 'value2' => array('label' => 'value 2'), - * ); - * ~~~ - * - * - groups: array, the attributes for the optgroup tags. The structure of this is similar to that of 'options', - * except that the array keys represent the optgroup labels specified in $items. - * - unselect: string, the value that will be submitted when no option is selected. - * When this attribute is set, a hidden field will be generated so that if no option is selected in multiple - * mode, we can still obtain the posted unselect value. - * - * The rest of the options will be rendered as the attributes of the resulting tag. The values will - * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. - * - * @return string the generated list box tag - */ - public static function listBox($name, $selection = null, $items = array(), $options = array()) - { - if (!isset($options['size'])) { - $options['size'] = 4; - } - if (isset($options['multiple']) && $options['multiple'] && substr($name, -2) !== '[]') { - $name .= '[]'; - } - $options['name'] = $name; - if (isset($options['unselect'])) { - // add a hidden field so that if the list box has no option being selected, it still submits a value - if (substr($name, -2) === '[]') { - $name = substr($name, 0, -2); - } - $hidden = static::hiddenInput($name, $options['unselect']); - unset($options['unselect']); - } else { - $hidden = ''; - } - $selectOptions = static::renderSelectOptions($selection, $items, $options); - return $hidden . static::tag('select', "\n" . $selectOptions . "\n", $options); - } - - /** - * Generates a list of checkboxes. - * A checkbox list allows multiple selection, like [[listBox()]]. - * As a result, the corresponding submitted value is an array. - * @param string $name the name attribute of each checkbox. - * @param string|array $selection the selected value(s). - * @param array $items the data item used to generate the checkboxes. - * The array keys are the labels, while the array values are the corresponding checkbox values. - * Note that the labels will NOT be HTML-encoded, while the values will. - * @param array $options options (name => config) for the checkbox list. The following options are supported: - * - * - unselect: string, the value that should be submitted when none of the checkboxes is selected. - * By setting this option, a hidden input will be generated. - * - separator: string, the HTML code that separates items. - * - item: callable, a callback that can be used to customize the generation of the HTML code - * corresponding to a single item in $items. The signature of this callback must be: - * - * ~~~ - * function ($index, $label, $name, $checked, $value) - * ~~~ - * - * where $index is the zero-based index of the checkbox in the whole list; $label - * is the label for the checkbox; and $name, $value and $checked represent the name, - * value and the checked status of the checkbox input. - * @return string the generated checkbox list - */ - public static function checkboxList($name, $selection = null, $items = array(), $options = array()) - { - if (substr($name, -2) !== '[]') { - $name .= '[]'; - } - - $formatter = isset($options['item']) ? $options['item'] : null; - $lines = array(); - $index = 0; - foreach ($items as $value => $label) { - $checked = $selection !== null && - (!is_array($selection) && !strcmp($value, $selection) - || is_array($selection) && in_array($value, $selection)); - if ($formatter !== null) { - $lines[] = call_user_func($formatter, $index, $label, $name, $checked, $value); - } else { - $lines[] = static::label(static::checkbox($name, $checked, $value) . ' ' . $label); - } - $index++; - } - - if (isset($options['unselect'])) { - // add a hidden field so that if the list box has no option being selected, it still submits a value - $name2 = substr($name, -2) === '[]' ? substr($name, 0, -2) : $name; - $hidden = static::hiddenInput($name2, $options['unselect']); - } else { - $hidden = ''; - } - $separator = isset($options['separator']) ? $options['separator'] : "\n"; - - return $hidden . implode($separator, $lines); - } - - /** - * Generates a list of radio buttons. - * A radio button list is like a checkbox list, except that it only allows single selection. - * @param string $name the name attribute of each radio button. - * @param string|array $selection the selected value(s). - * @param array $items the data item used to generate the radio buttons. - * The array keys are the labels, while the array values are the corresponding radio button values. - * Note that the labels will NOT be HTML-encoded, while the values will. - * @param array $options options (name => config) for the radio button list. The following options are supported: - * - * - unselect: string, the value that should be submitted when none of the radio buttons is selected. - * By setting this option, a hidden input will be generated. - * - separator: string, the HTML code that separates items. - * - item: callable, a callback that can be used to customize the generation of the HTML code - * corresponding to a single item in $items. The signature of this callback must be: - * - * ~~~ - * function ($index, $label, $name, $checked, $value) - * ~~~ - * - * where $index is the zero-based index of the radio button in the whole list; $label - * is the label for the radio button; and $name, $value and $checked represent the name, - * value and the checked status of the radio button input. - * @return string the generated radio button list - */ - public static function radioList($name, $selection = null, $items = array(), $options = array()) - { - $formatter = isset($options['item']) ? $options['item'] : null; - $lines = array(); - $index = 0; - foreach ($items as $value => $label) { - $checked = $selection !== null && - (!is_array($selection) && !strcmp($value, $selection) - || is_array($selection) && in_array($value, $selection)); - if ($formatter !== null) { - $lines[] = call_user_func($formatter, $index, $label, $name, $checked, $value); - } else { - $lines[] = static::label(static::radio($name, $checked, $value) . ' ' . $label); - } - $index++; - } - - $separator = isset($options['separator']) ? $options['separator'] : "\n"; - if (isset($options['unselect'])) { - // add a hidden field so that if the list box has no option being selected, it still submits a value - $hidden = static::hiddenInput($name, $options['unselect']); - } else { - $hidden = ''; - } - - return $hidden . implode($separator, $lines); - } - - /** - * Renders the option tags that can be used by [[dropDownList()]] and [[listBox()]]. - * @param string|array $selection the selected value(s). This can be either a string for single selection - * or an array for multiple selections. - * @param array $items the option data items. The array keys are option values, and the array values - * are the corresponding option labels. The array can also be nested (i.e. some array values are arrays too). - * For each sub-array, an option group will be generated whose label is the key associated with the sub-array. - * If you have a list of data models, you may convert them into the format described above using - * [[\yii\util\ArrayHelper::map()]]. - * - * Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in - * the labels will also be HTML-encoded. - * @param array $tagOptions the $options parameter that is passed to the [[dropDownList()]] or [[listBox()]] call. - * This method will take out these elements, if any: "prompt", "options" and "groups". See more details - * in [[dropDownList()]] for the explanation of these elements. - * - * @return string the generated list options - */ - public static function renderSelectOptions($selection, $items, &$tagOptions = array()) - { - $lines = array(); - if (isset($tagOptions['prompt'])) { - $prompt = str_replace(' ', ' ', static::encode($tagOptions['prompt'])); - $lines[] = static::tag('option', $prompt, array('value' => '')); - } - - $options = isset($tagOptions['options']) ? $tagOptions['options'] : array(); - $groups = isset($tagOptions['groups']) ? $tagOptions['groups'] : array(); - unset($tagOptions['prompt'], $tagOptions['options'], $tagOptions['groups']); - - foreach ($items as $key => $value) { - if (is_array($value)) { - $groupAttrs = isset($groups[$key]) ? $groups[$key] : array(); - $groupAttrs['label'] = $key; - $attrs = array('options' => $options, 'groups' => $groups); - $content = static::renderSelectOptions($selection, $value, $attrs); - $lines[] = static::tag('optgroup', "\n" . $content . "\n", $groupAttrs); - } else { - $attrs = isset($options[$key]) ? $options[$key] : array(); - $attrs['value'] = $key; - $attrs['selected'] = $selection !== null && - (!is_array($selection) && !strcmp($key, $selection) - || is_array($selection) && in_array($key, $selection)); - $lines[] = static::tag('option', str_replace(' ', ' ', static::encode($value)), $attrs); - } - } - - return implode("\n", $lines); - } - - /** - * Renders the HTML tag attributes. - * Boolean attributes such as s 'checked', 'disabled', 'readonly', will be handled specially - * according to [[booleanAttributes]] and [[showBooleanAttributeValues]]. - * @param array $attributes attributes to be rendered. The attribute values will be HTML-encoded using [[encode()]]. - * Attributes whose value is null will be ignored and not put in the rendering result. - * @return string the rendering result. If the attributes are not empty, they will be rendered - * into a string with a leading white space (such that it can be directly appended to the tag name - * in a tag. If there is no attribute, an empty string will be returned. - */ - public static function renderTagAttributes($attributes) - { - if (count($attributes) > 1) { - $sorted = array(); - foreach (static::$attributeOrder as $name) { - if (isset($attributes[$name])) { - $sorted[$name] = $attributes[$name]; - } - } - $attributes = array_merge($sorted, $attributes); - } - - $html = ''; - foreach ($attributes as $name => $value) { - if (isset(static::$booleanAttributes[strtolower($name)])) { - if ($value || strcasecmp($name, $value) === 0) { - $html .= static::$showBooleanAttributeValues ? " $name=\"$name\"" : " $name"; - } - } elseif ($value !== null) { - $html .= " $name=\"" . static::encode($value) . '"'; - } - } - return $html; - } - - /** - * Normalizes the input parameter to be a valid URL. - * - * 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 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')`. - * - * @param array|string $url the parameter to be used to generate a valid URL - * @return string the normalized URL - * @throws InvalidParamException if the parameter is invalid. - */ - public static function url($url) - { - if (is_array($url)) { - if (isset($url[0])) { - return Yii::$app->createUrl($url[0], array_splice($url, 1)); - } else { - throw new InvalidParamException('The array specifying a URL must contain at least one element.'); - } - } elseif ($url === '') { - return Yii::$app->getRequest()->getUrl(); - } else { - return Yii::getAlias($url); - } - } -} diff --git a/framework/util/SecurityHelper.php b/framework/util/SecurityHelper.php deleted file mode 100644 index 4186681..0000000 --- a/framework/util/SecurityHelper.php +++ /dev/null @@ -1,272 +0,0 @@ - - * @author Tom Worster - * @since 2.0 - */ -class SecurityHelper -{ - /** - * Encrypts data. - * @param string $data data to be encrypted. - * @param string $key the encryption secret key - * @return string the encrypted data - * @throws Exception if PHP Mcrypt extension is not loaded or failed to be initialized - * @see decrypt() - */ - public static function encrypt($data, $key) - { - $module = static::openCryptModule(); - $key = StringHelper::substr($key, 0, mcrypt_enc_get_key_size($module)); - srand(); - $iv = mcrypt_create_iv(mcrypt_enc_get_iv_size($module), MCRYPT_RAND); - mcrypt_generic_init($module, $key, $iv); - $encrypted = $iv . mcrypt_generic($module, $data); - mcrypt_generic_deinit($module); - mcrypt_module_close($module); - return $encrypted; - } - - /** - * Decrypts data - * @param string $data data to be decrypted. - * @param string $key the decryption secret key - * @return string the decrypted data - * @throws Exception if PHP Mcrypt extension is not loaded or failed to be initialized - * @see encrypt() - */ - public static function decrypt($data, $key) - { - $module = static::openCryptModule(); - $key = StringHelper::substr($key, 0, mcrypt_enc_get_key_size($module)); - $ivSize = mcrypt_enc_get_iv_size($module); - $iv = StringHelper::substr($data, 0, $ivSize); - mcrypt_generic_init($module, $key, $iv); - $decrypted = mdecrypt_generic($module, StringHelper::substr($data, $ivSize, StringHelper::strlen($data))); - mcrypt_generic_deinit($module); - mcrypt_module_close($module); - return rtrim($decrypted, "\0"); - } - - /** - * Prefixes data with a keyed hash value so that it can later be detected if it is tampered. - * @param string $data the data to be protected - * @param string $key the secret key to be used for generating hash - * @param string $algorithm the hashing algorithm (e.g. "md5", "sha1", "sha256", etc.). Call PHP "hash_algos()" - * function to see the supported hashing algorithms on your system. - * @return string the data prefixed with the keyed hash - * @see validateData() - * @see getSecretKey() - */ - public static function hashData($data, $key, $algorithm = 'sha256') - { - return hash_hmac($algorithm, $data, $key) . $data; - } - - /** - * Validates if the given data is tampered. - * @param string $data the data to be validated. The data must be previously - * generated by [[hashData()]]. - * @param string $key the secret key that was previously used to generate the hash for the data in [[hashData()]]. - * @param string $algorithm the hashing algorithm (e.g. "md5", "sha1", "sha256", etc.). Call PHP "hash_algos()" - * function to see the supported hashing algorithms on your system. This must be the same - * as the value passed to [[hashData()]] when generating the hash for the data. - * @return string the real data with the hash stripped off. False if the data is tampered. - * @see hashData() - */ - public static function validateData($data, $key, $algorithm = 'sha256') - { - $hashSize = StringHelper::strlen(hash_hmac($algorithm, 'test', $key)); - $n = StringHelper::strlen($data); - if ($n >= $hashSize) { - $hash = StringHelper::substr($data, 0, $hashSize); - $data2 = StringHelper::substr($data, $hashSize, $n - $hashSize); - return $hash === hash_hmac($algorithm, $data2, $key) ? $data2 : false; - } else { - return false; - } - } - - /** - * Returns a secret key associated with the specified name. - * If the secret key does not exist, a random key will be generated - * and saved in the file "keys.php" under the application's runtime directory - * so that the same secret key can be returned in future requests. - * @param string $name the name that is associated with the secret key - * @param integer $length the length of the key that should be generated if not exists - * @return string the secret key associated with the specified name - */ - public static function getSecretKey($name, $length = 32) - { - static $keys; - $keyFile = Yii::$app->getRuntimePath() . '/keys.php'; - if ($keys === null) { - $keys = is_file($keyFile) ? require($keyFile) : array(); - } - if (!isset($keys[$name])) { - // generate a 32-char random key - $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - $keys[$name] = substr(str_shuffle(str_repeat($chars, 5)), 0, $length); - file_put_contents($keyFile, " 30) { - throw new InvalidParamException('Hash is invalid.'); - } - - $test = crypt($password, $hash); - $n = strlen($test); - if (strlen($test) < 32 || $n !== strlen($hash)) { - return false; - } - - // Use a for-loop to compare two strings to prevent timing attacks. See: - // http://codereview.stackexchange.com/questions/13512 - $check = 0; - for ($i = 0; $i < $n; ++$i) { - $check |= (ord($test[$i]) ^ ord($hash[$i])); - } - - return $check === 0; - } - - /** - * Generates a salt that can be used to generate a password hash. - * - * The PHP [crypt()](http://php.net/manual/en/function.crypt.php) built-in function - * requires, for the Blowfish hash algorithm, a salt string in a specific format: - * "$2a$", "$2x$" or "$2y$", a two digit cost parameter, "$", and 22 characters - * from the alphabet "./0-9A-Za-z". - * - * @param integer $cost the cost parameter - * @return string the random salt value. - * @throws InvalidParamException if the cost parameter is not between 4 and 30 - */ - protected static function generateSalt($cost = 13) - { - $cost = (int)$cost; - if ($cost < 4 || $cost > 30) { - throw new InvalidParamException('Cost must be between 4 and 31.'); - } - - // Get 20 * 8bits of pseudo-random entropy from mt_rand(). - $rand = ''; - for ($i = 0; $i < 20; ++$i) { - $rand .= chr(mt_rand(0, 255)); - } - - // Add the microtime for a little more entropy. - $rand .= microtime(); - // Mix the bits cryptographically into a 20-byte binary string. - $rand = sha1($rand, true); - // Form the prefix that specifies Blowfish algorithm and cost parameter. - $salt = sprintf("$2y$%02d$", $cost); - // Append the random salt data in the required base64 format. - $salt .= str_replace('+', '.', substr(base64_encode($rand), 0, 22)); - return $salt; - } -} \ No newline at end of file diff --git a/framework/util/StringHelper.php b/framework/util/StringHelper.php deleted file mode 100644 index 3874701..0000000 --- a/framework/util/StringHelper.php +++ /dev/null @@ -1,125 +0,0 @@ - - * @author Alex Makarov - * @since 2.0 - */ -class StringHelper -{ - /** - * Returns the number of bytes in the given string. - * This method ensures the string is treated as a byte array. - * It will use `mb_strlen()` if it is available. - * @param string $string the string being measured for length - * @return integer the number of bytes in the given string. - */ - public static function strlen($string) - { - return function_exists('mb_strlen') ? mb_strlen($string, '8bit') : strlen($string); - } - - /** - * Returns the portion of string specified by the start and length parameters. - * This method ensures the string is treated as a byte array. - * It will use `mb_substr()` if it is available. - * @param string $string the input string. Must be one character or longer. - * @param integer $start the starting position - * @param integer $length the desired portion length - * @return string the extracted part of string, or FALSE on failure or an empty string. - * @see http://www.php.net/manual/en/function.substr.php - */ - public static function substr($string, $start, $length) - { - return function_exists('mb_substr') ? mb_substr($string, $start, $length, '8bit') : substr($string, $start, $length); - } - - /** - * Converts a word to its plural form. - * Note that this is for English only! - * For example, 'apple' will become 'apples', and 'child' will become 'children'. - * @param string $name the word to be pluralized - * @return string the pluralized word - */ - public static function pluralize($name) - { - static $rules = array( - '/(m)ove$/i' => '\1oves', - '/(f)oot$/i' => '\1eet', - '/(c)hild$/i' => '\1hildren', - '/(h)uman$/i' => '\1umans', - '/(m)an$/i' => '\1en', - '/(s)taff$/i' => '\1taff', - '/(t)ooth$/i' => '\1eeth', - '/(p)erson$/i' => '\1eople', - '/([m|l])ouse$/i' => '\1ice', - '/(x|ch|ss|sh|us|as|is|os)$/i' => '\1es', - '/([^aeiouy]|qu)y$/i' => '\1ies', - '/(?:([^f])fe|([lr])f)$/i' => '\1\2ves', - '/(shea|lea|loa|thie)f$/i' => '\1ves', - '/([ti])um$/i' => '\1a', - '/(tomat|potat|ech|her|vet)o$/i' => '\1oes', - '/(bu)s$/i' => '\1ses', - '/(ax|test)is$/i' => '\1es', - '/s$/' => 's', - ); - foreach ($rules as $rule => $replacement) { - if (preg_match($rule, $name)) { - return preg_replace($rule, $replacement, $name); - } - } - return $name . 's'; - } - - /** - * Converts a CamelCase name into space-separated words. - * For example, 'PostTag' will be converted to 'Post Tag'. - * @param string $name the string to be converted - * @param boolean $ucwords whether to capitalize the first letter in each word - * @return string the resulting words - */ - public static function camel2words($name, $ucwords = true) - { - $label = trim(strtolower(str_replace(array('-', '_', '.'), ' ', preg_replace('/(? - * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008-2011 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -namespace yii\util; - -/** - * VarDumper is intended to replace the buggy PHP function var_dump and print_r. - * It can correctly identify the recursively referenced objects in a complex - * object structure. It also has a recursive depth control to avoid indefinite - * recursive display of some peculiar variables. - * - * VarDumper can be used as follows, - * - * ~~~ - * VarDumper::dump($var); - * ~~~ - * - * @author Qiang Xue - * @since 2.0 - */ -class CVarDumper -{ - private static $_objects; - private static $_output; - private static $_depth; - - /** - * Displays a variable. - * This method achieves the similar functionality as var_dump and print_r - * but is more robust when handling complex objects such as Yii controllers. - * @param mixed $var variable to be dumped - * @param integer $depth maximum depth that the dumper should go into the variable. Defaults to 10. - * @param boolean $highlight whether the result should be syntax-highlighted - */ - public static function dump($var, $depth = 10, $highlight = false) - { - echo self::dumpAsString($var, $depth, $highlight); - } - - /** - * Dumps a variable in terms of a string. - * This method achieves the similar functionality as var_dump and print_r - * but is more robust when handling complex objects such as Yii controllers. - * @param mixed $var variable to be dumped - * @param integer $depth maximum depth that the dumper should go into the variable. Defaults to 10. - * @param boolean $highlight whether the result should be syntax-highlighted - * @return string the string representation of the variable - */ - public static function dumpAsString($var, $depth = 10, $highlight = false) - { - self::$_output = ''; - self::$_objects = array(); - self::$_depth = $depth; - self::dumpInternal($var, 0); - if ($highlight) { - $result = highlight_string("/', '', $result, 1); - } - return self::$_output; - } - - /* - * @param mixed $var variable to be dumped - * @param integer $level depth level - */ - private static function dumpInternal($var, $level) - { - switch (gettype($var)) { - case 'boolean': - self::$_output .= $var ? 'true' : 'false'; - break; - case 'integer': - self::$_output .= "$var"; - break; - case 'double': - self::$_output .= "$var"; - break; - case 'string': - self::$_output .= "'" . addslashes($var) . "'"; - break; - case 'resource': - self::$_output .= '{resource}'; - break; - case 'NULL': - self::$_output .= "null"; - break; - case 'unknown type': - self::$_output .= '{unknown}'; - break; - case 'array': - if (self::$_depth <= $level) { - self::$_output .= 'array(...)'; - } elseif (empty($var)) { - self::$_output .= 'array()'; - } else { - $keys = array_keys($var); - $spaces = str_repeat(' ', $level * 4); - self::$_output .= "array\n" . $spaces . '('; - foreach ($keys as $key) { - self::$_output .= "\n" . $spaces . ' '; - self::dumpInternal($key, 0); - self::$_output .= ' => '; - self::dumpInternal($var[$key], $level + 1); - } - self::$_output .= "\n" . $spaces . ')'; - } - break; - case 'object': - if (($id = array_search($var, self::$_objects, true)) !== false) { - self::$_output .= get_class($var) . '#' . ($id + 1) . '(...)'; - } elseif (self::$_depth <= $level) { - self::$_output .= get_class($var) . '(...)'; - } else { - $id = self::$_objects[] = $var; - $className = get_class($var); - $members = (array)$var; - $spaces = str_repeat(' ', $level * 4); - self::$_output .= "$className#$id\n" . $spaces . '('; - foreach ($members as $key => $value) { - $keyDisplay = strtr(trim($key), array("\0" => ':')); - self::$_output .= "\n" . $spaces . " [$keyDisplay] => "; - self::dumpInternal($value, $level + 1); - } - self::$_output .= "\n" . $spaces . ')'; - } - break; - } - } -} \ No newline at end of file diff --git a/framework/util/mimeTypes.php b/framework/util/mimeTypes.php deleted file mode 100644 index ffdba4b..0000000 --- a/framework/util/mimeTypes.php +++ /dev/null @@ -1,187 +0,0 @@ - 'application/postscript', - 'aif' => 'audio/x-aiff', - 'aifc' => 'audio/x-aiff', - 'aiff' => 'audio/x-aiff', - 'anx' => 'application/annodex', - 'asc' => 'text/plain', - 'au' => 'audio/basic', - 'avi' => 'video/x-msvideo', - 'axa' => 'audio/annodex', - 'axv' => 'video/annodex', - 'bcpio' => 'application/x-bcpio', - 'bin' => 'application/octet-stream', - 'bmp' => 'image/bmp', - 'c' => 'text/plain', - 'cc' => 'text/plain', - 'ccad' => 'application/clariscad', - 'cdf' => 'application/x-netcdf', - 'class' => 'application/octet-stream', - 'cpio' => 'application/x-cpio', - 'cpt' => 'application/mac-compactpro', - 'csh' => 'application/x-csh', - 'css' => 'text/css', - 'dcr' => 'application/x-director', - 'dir' => 'application/x-director', - 'dms' => 'application/octet-stream', - 'doc' => 'application/msword', - 'drw' => 'application/drafting', - 'dvi' => 'application/x-dvi', - 'dwg' => 'application/acad', - 'dxf' => 'application/dxf', - 'dxr' => 'application/x-director', - 'eps' => 'application/postscript', - 'etx' => 'text/x-setext', - 'exe' => 'application/octet-stream', - 'ez' => 'application/andrew-inset', - 'f' => 'text/plain', - 'f90' => 'text/plain', - 'flac' => 'audio/flac', - 'fli' => 'video/x-fli', - 'flv' => 'video/x-flv', - 'gif' => 'image/gif', - 'gtar' => 'application/x-gtar', - 'gz' => 'application/x-gzip', - 'h' => 'text/plain', - 'hdf' => 'application/x-hdf', - 'hh' => 'text/plain', - 'hqx' => 'application/mac-binhex40', - 'htm' => 'text/html', - 'html' => 'text/html', - 'ice' => 'x-conference/x-cooltalk', - 'ief' => 'image/ief', - 'iges' => 'model/iges', - 'igs' => 'model/iges', - 'ips' => 'application/x-ipscript', - 'ipx' => 'application/x-ipix', - 'jpe' => 'image/jpeg', - 'jpeg' => 'image/jpeg', - 'jpg' => 'image/jpeg', - 'js' => 'application/x-javascript', - 'kar' => 'audio/midi', - 'latex' => 'application/x-latex', - 'lha' => 'application/octet-stream', - 'lsp' => 'application/x-lisp', - 'lzh' => 'application/octet-stream', - 'm' => 'text/plain', - 'man' => 'application/x-troff-man', - 'me' => 'application/x-troff-me', - 'mesh' => 'model/mesh', - 'mid' => 'audio/midi', - 'midi' => 'audio/midi', - 'mif' => 'application/vnd.mif', - 'mime' => 'www/mime', - 'mov' => 'video/quicktime', - 'movie' => 'video/x-sgi-movie', - 'mp2' => 'audio/mpeg', - 'mp3' => 'audio/mpeg', - 'mpe' => 'video/mpeg', - 'mpeg' => 'video/mpeg', - 'mpg' => 'video/mpeg', - 'mpga' => 'audio/mpeg', - 'ms' => 'application/x-troff-ms', - 'msh' => 'model/mesh', - 'nc' => 'application/x-netcdf', - 'oga' => 'audio/ogg', - 'ogg' => 'audio/ogg', - 'ogv' => 'video/ogg', - 'ogx' => 'application/ogg', - 'oda' => 'application/oda', - 'pbm' => 'image/x-portable-bitmap', - 'pdb' => 'chemical/x-pdb', - 'pdf' => 'application/pdf', - 'pgm' => 'image/x-portable-graymap', - 'pgn' => 'application/x-chess-pgn', - 'png' => 'image/png', - 'pnm' => 'image/x-portable-anymap', - 'pot' => 'application/mspowerpoint', - 'ppm' => 'image/x-portable-pixmap', - 'pps' => 'application/mspowerpoint', - 'ppt' => 'application/mspowerpoint', - 'ppz' => 'application/mspowerpoint', - 'pre' => 'application/x-freelance', - 'prt' => 'application/pro_eng', - 'ps' => 'application/postscript', - 'qt' => 'video/quicktime', - 'ra' => 'audio/x-realaudio', - 'ram' => 'audio/x-pn-realaudio', - 'ras' => 'image/cmu-raster', - 'rgb' => 'image/x-rgb', - 'rm' => 'audio/x-pn-realaudio', - 'roff' => 'application/x-troff', - 'rpm' => 'audio/x-pn-realaudio-plugin', - 'rtf' => 'text/rtf', - 'rtx' => 'text/richtext', - 'scm' => 'application/x-lotusscreencam', - 'set' => 'application/set', - 'sgm' => 'text/sgml', - 'sgml' => 'text/sgml', - 'sh' => 'application/x-sh', - 'shar' => 'application/x-shar', - 'silo' => 'model/mesh', - 'sit' => 'application/x-stuffit', - 'skd' => 'application/x-koan', - 'skm' => 'application/x-koan', - 'skp' => 'application/x-koan', - 'skt' => 'application/x-koan', - 'smi' => 'application/smil', - 'smil' => 'application/smil', - 'snd' => 'audio/basic', - 'sol' => 'application/solids', - 'spl' => 'application/x-futuresplash', - 'spx' => 'audio/ogg', - 'src' => 'application/x-wais-source', - 'step' => 'application/STEP', - 'stl' => 'application/SLA', - 'stp' => 'application/STEP', - 'sv4cpio' => 'application/x-sv4cpio', - 'sv4crc' => 'application/x-sv4crc', - 'swf' => 'application/x-shockwave-flash', - 't' => 'application/x-troff', - 'tar' => 'application/x-tar', - 'tcl' => 'application/x-tcl', - 'tex' => 'application/x-tex', - 'texi' => 'application/x-texinfo', - 'texinfo' => 'application/x-texinfo', - 'tif' => 'image/tiff', - 'tiff' => 'image/tiff', - 'tr' => 'application/x-troff', - 'tsi' => 'audio/TSP-audio', - 'tsp' => 'application/dsptype', - 'tsv' => 'text/tab-separated-values', - 'txt' => 'text/plain', - 'unv' => 'application/i-deas', - 'ustar' => 'application/x-ustar', - 'vcd' => 'application/x-cdlink', - 'vda' => 'application/vda', - 'viv' => 'video/vnd.vivo', - 'vivo' => 'video/vnd.vivo', - 'vrml' => 'model/vrml', - 'wav' => 'audio/x-wav', - 'wrl' => 'model/vrml', - 'xbm' => 'image/x-xbitmap', - 'xlc' => 'application/vnd.ms-excel', - 'xll' => 'application/vnd.ms-excel', - 'xlm' => 'application/vnd.ms-excel', - 'xls' => 'application/vnd.ms-excel', - 'xlw' => 'application/vnd.ms-excel', - 'xml' => 'application/xml', - 'xpm' => 'image/x-xpixmap', - 'xspf' => 'application/xspf+xml', - 'xwd' => 'image/x-xwindowdump', - 'xyz' => 'chemical/x-pdb', - 'zip' => 'application/zip', -); From 884977a72452080cfbe582ffe10744350fb4f1ed Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Sat, 23 Mar 2013 15:32:26 -0400 Subject: [PATCH 094/117] renamed util to helpers. --- framework/base/Application.php | 2 +- framework/base/Controller.php | 4 ++-- framework/base/Dictionary.php | 2 +- framework/base/ErrorHandler.php | 2 +- framework/base/Model.php | 2 +- framework/base/Module.php | 4 ++-- framework/base/Theme.php | 2 +- framework/base/View.php | 2 +- framework/base/Widget.php | 2 +- framework/console/controllers/AppController.php | 2 +- framework/console/controllers/HelpController.php | 2 +- framework/console/controllers/MigrateController.php | 2 +- framework/db/ActiveRecord.php | 2 +- framework/helpers/ArrayHelper.php | 8 ++++---- framework/helpers/ConsoleColor.php | 2 +- framework/helpers/FileHelper.php | 2 +- framework/helpers/Html.php | 8 ++++---- framework/helpers/SecurityHelper.php | 2 +- framework/helpers/StringHelper.php | 2 +- framework/helpers/VarDumper.php | 2 +- framework/web/CookieCollection.php | 2 +- framework/web/Response.php | 2 +- framework/web/Sort.php | 2 +- framework/widgets/ActiveForm.php | 6 +++--- tests/unit/framework/util/ArrayHelperTest.php | 2 +- tests/unit/framework/util/HtmlTest.php | 2 +- tests/web/app/protected/controllers/SiteController.php | 2 +- 27 files changed, 37 insertions(+), 37 deletions(-) diff --git a/framework/base/Application.php b/framework/base/Application.php index bbc4601..3dcbb26 100644 --- a/framework/base/Application.php +++ b/framework/base/Application.php @@ -8,7 +8,7 @@ namespace yii\base; use Yii; -use yii\util\FileHelper; +use yii\helpers\FileHelper; /** * Application is the base class for all application classes. diff --git a/framework/base/Controller.php b/framework/base/Controller.php index 9219904..17fb4da 100644 --- a/framework/base/Controller.php +++ b/framework/base/Controller.php @@ -8,8 +8,8 @@ namespace yii\base; use Yii; -use yii\util\FileHelper; -use yii\util\StringHelper; +use yii\helpers\FileHelper; +use yii\helpers\StringHelper; /** * Controller is the base class for classes containing controller logic. diff --git a/framework/base/Dictionary.php b/framework/base/Dictionary.php index 343dbfd..9343d68 100644 --- a/framework/base/Dictionary.php +++ b/framework/base/Dictionary.php @@ -7,7 +7,7 @@ namespace yii\base; -use yii\util\ArrayHelper; +use yii\helpers\ArrayHelper; /** * Dictionary implements a collection that stores key-value pairs. diff --git a/framework/base/ErrorHandler.php b/framework/base/ErrorHandler.php index 996fa18..a3ab137 100644 --- a/framework/base/ErrorHandler.php +++ b/framework/base/ErrorHandler.php @@ -16,7 +16,7 @@ namespace yii\base; * @author Qiang Xue * @since 2.0 */ -use yii\util\VarDumper; +use yii\helpers\VarDumper; class ErrorHandler extends Component { diff --git a/framework/base/Model.php b/framework/base/Model.php index b761ada..402a558 100644 --- a/framework/base/Model.php +++ b/framework/base/Model.php @@ -7,7 +7,7 @@ namespace yii\base; -use yii\util\StringHelper; +use yii\helpers\StringHelper; use yii\validators\Validator; use yii\validators\RequiredValidator; diff --git a/framework/base/Module.php b/framework/base/Module.php index 0e5c1cb..9988164 100644 --- a/framework/base/Module.php +++ b/framework/base/Module.php @@ -8,8 +8,8 @@ namespace yii\base; use Yii; -use yii\util\StringHelper; -use yii\util\FileHelper; +use yii\helpers\StringHelper; +use yii\helpers\FileHelper; /** * Module is the base class for module and application classes. diff --git a/framework/base/Theme.php b/framework/base/Theme.php index c5fd925..88ecb0a 100644 --- a/framework/base/Theme.php +++ b/framework/base/Theme.php @@ -9,7 +9,7 @@ namespace yii\base; use Yii; use yii\base\InvalidConfigException; -use yii\util\FileHelper; +use yii\helpers\FileHelper; /** * Theme represents an application theme. diff --git a/framework/base/View.php b/framework/base/View.php index 86020ad..36c90ad 100644 --- a/framework/base/View.php +++ b/framework/base/View.php @@ -9,7 +9,7 @@ namespace yii\base; use Yii; use yii\base\Application; -use yii\util\FileHelper; +use yii\helpers\FileHelper; /** * View represents a view object in the MVC pattern. diff --git a/framework/base/Widget.php b/framework/base/Widget.php index 6b92f09..24d0685 100644 --- a/framework/base/Widget.php +++ b/framework/base/Widget.php @@ -8,7 +8,7 @@ namespace yii\base; use Yii; -use yii\util\FileHelper; +use yii\helpers\FileHelper; /** * Widget is the base class for widgets. diff --git a/framework/console/controllers/AppController.php b/framework/console/controllers/AppController.php index 78cf6fc..93ef5f5 100644 --- a/framework/console/controllers/AppController.php +++ b/framework/console/controllers/AppController.php @@ -8,7 +8,7 @@ namespace yii\console\controllers; use yii\console\Controller; -use yii\util\FileHelper; +use yii\helpers\FileHelper; use yii\base\Exception; /** diff --git a/framework/console/controllers/HelpController.php b/framework/console/controllers/HelpController.php index 01bc994..ea7e3d5 100644 --- a/framework/console/controllers/HelpController.php +++ b/framework/console/controllers/HelpController.php @@ -13,7 +13,7 @@ use yii\console\Exception; use yii\base\InlineAction; use yii\console\Controller; use yii\console\Request; -use yii\util\StringHelper; +use yii\helpers\StringHelper; /** * This command provides help information about console commands. diff --git a/framework/console/controllers/MigrateController.php b/framework/console/controllers/MigrateController.php index 06e100b..7f9a18f 100644 --- a/framework/console/controllers/MigrateController.php +++ b/framework/console/controllers/MigrateController.php @@ -13,7 +13,7 @@ use yii\console\Exception; use yii\console\Controller; use yii\db\Connection; use yii\db\Query; -use yii\util\ArrayHelper; +use yii\helpers\ArrayHelper; /** * This command manages application migrations. diff --git a/framework/db/ActiveRecord.php b/framework/db/ActiveRecord.php index f8d2b99..0c15121 100644 --- a/framework/db/ActiveRecord.php +++ b/framework/db/ActiveRecord.php @@ -16,7 +16,7 @@ use yii\base\InvalidCallException; use yii\db\Connection; use yii\db\TableSchema; use yii\db\Expression; -use yii\util\StringHelper; +use yii\helpers\StringHelper; /** * ActiveRecord is the base class for classes representing relational data in terms of objects. diff --git a/framework/helpers/ArrayHelper.php b/framework/helpers/ArrayHelper.php index 447d034..65fa962 100644 --- a/framework/helpers/ArrayHelper.php +++ b/framework/helpers/ArrayHelper.php @@ -5,7 +5,7 @@ * @license http://www.yiiframework.com/license/ */ -namespace yii\util; +namespace yii\helpers; use Yii; use yii\base\InvalidParamException; @@ -59,11 +59,11 @@ class ArrayHelper * * ~~~ * // working with array - * $username = \yii\util\ArrayHelper::getValue($_POST, 'username'); + * $username = \yii\helpers\ArrayHelper::getValue($_POST, 'username'); * // working with object - * $username = \yii\util\ArrayHelper::getValue($user, 'username'); + * $username = \yii\helpers\ArrayHelper::getValue($user, 'username'); * // working with anonymous function - * $fullName = \yii\util\ArrayHelper::getValue($user, function($user, $defaultValue) { + * $fullName = \yii\helpers\ArrayHelper::getValue($user, function($user, $defaultValue) { * return $user->firstName . ' ' . $user->lastName; * }); * ~~~ diff --git a/framework/helpers/ConsoleColor.php b/framework/helpers/ConsoleColor.php index 74aa154..c64db24 100644 --- a/framework/helpers/ConsoleColor.php +++ b/framework/helpers/ConsoleColor.php @@ -5,7 +5,7 @@ * @license http://www.yiiframework.com/license/ */ -namespace yii\util; +namespace yii\helpers; // todo define how subclassing will work // todo add a run() or render() method diff --git a/framework/helpers/FileHelper.php b/framework/helpers/FileHelper.php index 9108476..f850b98 100644 --- a/framework/helpers/FileHelper.php +++ b/framework/helpers/FileHelper.php @@ -7,7 +7,7 @@ * @license http://www.yiiframework.com/license/ */ -namespace yii\util; +namespace yii\helpers; use yii\base\Exception; use yii\base\InvalidConfigException; diff --git a/framework/helpers/Html.php b/framework/helpers/Html.php index a7b744b..b004885 100644 --- a/framework/helpers/Html.php +++ b/framework/helpers/Html.php @@ -5,7 +5,7 @@ * @license http://www.yiiframework.com/license/ */ -namespace yii\util; +namespace yii\helpers; use Yii; use yii\base\InvalidParamException; @@ -655,7 +655,7 @@ class Html * are the corresponding option labels. The array can also be nested (i.e. some array values are arrays too). * For each sub-array, an option group will be generated whose label is the key associated with the sub-array. * If you have a list of data models, you may convert them into the format described above using - * [[\yii\util\ArrayHelper::map()]]. + * [[\yii\helpers\ArrayHelper::map()]]. * * Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in * the labels will also be HTML-encoded. @@ -695,7 +695,7 @@ class Html * are the corresponding option labels. The array can also be nested (i.e. some array values are arrays too). * For each sub-array, an option group will be generated whose label is the key associated with the sub-array. * If you have a list of data models, you may convert them into the format described above using - * [[\yii\util\ArrayHelper::map()]]. + * [[\yii\helpers\ArrayHelper::map()]]. * * Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in * the labels will also be HTML-encoded. @@ -866,7 +866,7 @@ class Html * are the corresponding option labels. The array can also be nested (i.e. some array values are arrays too). * For each sub-array, an option group will be generated whose label is the key associated with the sub-array. * If you have a list of data models, you may convert them into the format described above using - * [[\yii\util\ArrayHelper::map()]]. + * [[\yii\helpers\ArrayHelper::map()]]. * * Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in * the labels will also be HTML-encoded. diff --git a/framework/helpers/SecurityHelper.php b/framework/helpers/SecurityHelper.php index 4186681..5029dd6 100644 --- a/framework/helpers/SecurityHelper.php +++ b/framework/helpers/SecurityHelper.php @@ -5,7 +5,7 @@ * @license http://www.yiiframework.com/license/ */ -namespace yii\util; +namespace yii\helpers; use Yii; use yii\base\Exception; diff --git a/framework/helpers/StringHelper.php b/framework/helpers/StringHelper.php index 3874701..ace34db 100644 --- a/framework/helpers/StringHelper.php +++ b/framework/helpers/StringHelper.php @@ -5,7 +5,7 @@ * @license http://www.yiiframework.com/license/ */ -namespace yii\util; +namespace yii\helpers; /** * StringHelper diff --git a/framework/helpers/VarDumper.php b/framework/helpers/VarDumper.php index ae474e6..64c3639 100644 --- a/framework/helpers/VarDumper.php +++ b/framework/helpers/VarDumper.php @@ -6,7 +6,7 @@ * @license http://www.yiiframework.com/license/ */ -namespace yii\util; +namespace yii\helpers; /** * VarDumper is intended to replace the buggy PHP function var_dump and print_r. diff --git a/framework/web/CookieCollection.php b/framework/web/CookieCollection.php index 2577d35..c76926b 100644 --- a/framework/web/CookieCollection.php +++ b/framework/web/CookieCollection.php @@ -9,7 +9,7 @@ namespace yii\web; use Yii; use yii\base\DictionaryIterator; -use yii\util\SecurityHelper; +use yii\helpers\SecurityHelper; /** * CookieCollection maintains the cookies available in the current request. diff --git a/framework/web/Response.php b/framework/web/Response.php index 3a0ce8b..d6659cf 100644 --- a/framework/web/Response.php +++ b/framework/web/Response.php @@ -7,7 +7,7 @@ namespace yii\web; -use yii\util\FileHelper; +use yii\helpers\FileHelper; /** * @author Qiang Xue diff --git a/framework/web/Sort.php b/framework/web/Sort.php index 85e9ce8..7cfeeca 100644 --- a/framework/web/Sort.php +++ b/framework/web/Sort.php @@ -8,7 +8,7 @@ namespace yii\web; use Yii; -use yii\util\Html; +use yii\helpers\Html; /** * Sort represents information relevant to sorting. diff --git a/framework/widgets/ActiveForm.php b/framework/widgets/ActiveForm.php index 2d47253..2c965e7 100644 --- a/framework/widgets/ActiveForm.php +++ b/framework/widgets/ActiveForm.php @@ -11,8 +11,8 @@ use Yii; use yii\base\InvalidParamException; use yii\base\Widget; use yii\base\Model; -use yii\util\Html; -use yii\util\ArrayHelper; +use yii\helpers\Html; +use yii\helpers\ArrayHelper; /** * ActiveForm ... @@ -23,7 +23,7 @@ use yii\util\ArrayHelper; class ActiveForm extends Widget { /** - * @param array|string $action the form action URL. This parameter will be processed by [[\yii\util\Html::url()]]. + * @param array|string $action the form action URL. This parameter will be processed by [[\yii\helpers\Html::url()]]. */ public $action = ''; /** diff --git a/tests/unit/framework/util/ArrayHelperTest.php b/tests/unit/framework/util/ArrayHelperTest.php index a36ce68..117c702 100644 --- a/tests/unit/framework/util/ArrayHelperTest.php +++ b/tests/unit/framework/util/ArrayHelperTest.php @@ -2,7 +2,7 @@ namespace yiiunit\framework\util; -use yii\util\ArrayHelper; +use yii\helpers\ArrayHelper; class ArrayHelperTest extends \yii\test\TestCase { diff --git a/tests/unit/framework/util/HtmlTest.php b/tests/unit/framework/util/HtmlTest.php index a887f29..eba1a20 100644 --- a/tests/unit/framework/util/HtmlTest.php +++ b/tests/unit/framework/util/HtmlTest.php @@ -3,7 +3,7 @@ namespace yiiunit\framework\util; use Yii; -use yii\util\Html; +use yii\helpers\Html; use yii\web\Application; class HtmlTest extends \yii\test\TestCase diff --git a/tests/web/app/protected/controllers/SiteController.php b/tests/web/app/protected/controllers/SiteController.php index 38efce3..050bf90 100644 --- a/tests/web/app/protected/controllers/SiteController.php +++ b/tests/web/app/protected/controllers/SiteController.php @@ -1,6 +1,6 @@ Date: Sun, 24 Mar 2013 22:34:45 -0400 Subject: [PATCH 095/117] Finished new message translation implementation. --- framework/base/ActionFilter.php | 45 ++++++ framework/base/Application.php | 1 - framework/base/View.php | 80 +++++----- framework/i18n/I18N.php | 114 +++++++------- framework/i18n/PhpMessageSource.php | 23 ++- framework/logging/Target.php | 3 +- framework/widgets/FragmentCache.php | 294 ++++++++++++++++++++++++++++++++++++ todo.md | 1 - 8 files changed, 457 insertions(+), 104 deletions(-) create mode 100644 framework/base/ActionFilter.php create mode 100644 framework/widgets/FragmentCache.php diff --git a/framework/base/ActionFilter.php b/framework/base/ActionFilter.php new file mode 100644 index 0000000..2655c5a --- /dev/null +++ b/framework/base/ActionFilter.php @@ -0,0 +1,45 @@ + + * @since 2.0 + */ +class Filter extends Behavior +{ + /** + * Declares event handlers for the [[owner]]'s events. + * @return array events (array keys) and the corresponding event handler methods (array values). + */ + public function events() + { + return array( + 'beforeAction' => 'beforeAction', + 'afterAction' => 'afterAction', + ); + } + + /** + * @param ActionEvent $event + * @return boolean + */ + public function beforeAction($event) + { + return $event->isValid; + } + + /** + * @param ActionEvent $event + * @return boolean + */ + public function afterAction($event) + { + + } +} \ No newline at end of file diff --git a/framework/base/Application.php b/framework/base/Application.php index 3dcbb26..31087e2 100644 --- a/framework/base/Application.php +++ b/framework/base/Application.php @@ -92,7 +92,6 @@ class Application extends Module private $_runtimePath; private $_ended = false; - private $_language; /** * @var string Used to reserve memory for fatal error handler. This memory diff --git a/framework/base/View.php b/framework/base/View.php index 36c90ad..baa1d10 100644 --- a/framework/base/View.php +++ b/framework/base/View.php @@ -322,46 +322,42 @@ class View extends Component $this->endWidget(); } - // - // /** - // * Begins fragment caching. - // * This method will display cached content if it is available. - // * If not, it will start caching and would expect an [[endCache()]] - // * call to end the cache and save the content into cache. - // * A typical usage of fragment caching is as follows, - // * - // * ~~~ - // * if($this->beginCache($id)) { - // * // ...generate content here - // * $this->endCache(); - // * } - // * ~~~ - // * - // * @param string $id a unique ID identifying the fragment to be cached. - // * @param array $properties initial property values for [[yii\widgets\OutputCache]] - // * @return boolean whether we need to generate content for caching. False if cached version is available. - // * @see endCache - // */ - // public function beginCache($id, $properties = array()) - // { - // $properties['id'] = $id; - // $cache = $this->beginWidget('yii\widgets\OutputCache', $properties); - // if ($cache->getIsContentCached()) { - // $this->endCache(); - // return false; - // } else { - // return true; - // } - // } - // - // /** - // * Ends fragment caching. - // * This is an alias to [[endWidget()]] - // * @see beginCache - // */ - // public function endCache() - // { - // $this->endWidget(); - // } - // + /** + * Begins fragment caching. + * This method will display cached content if it is available. + * If not, it will start caching and would expect an [[endCache()]] + * call to end the cache and save the content into cache. + * A typical usage of fragment caching is as follows, + * + * ~~~ + * if($this->beginCache($id)) { + * // ...generate content here + * $this->endCache(); + * } + * ~~~ + * + * @param string $id a unique ID identifying the fragment to be cached. + * @param array $properties initial property values for [[\yii\widgets\OutputCache]] + * @return boolean whether you should generate the content for caching. + * False if the cached version is available. + */ + public function beginCache($id, $properties = array()) + { + $properties['id'] = $id; + $cache = $this->beginWidget('yii\widgets\OutputCache', $properties); + if ($cache->getIsContentCached()) { + $this->endCache(); + return false; + } else { + return true; + } + } + + /** + * Ends fragment caching. + */ + public function endCache() + { + $this->endWidget(); + } } \ No newline at end of file diff --git a/framework/i18n/I18N.php b/framework/i18n/I18N.php index ab87dfc..0409da3 100644 --- a/framework/i18n/I18N.php +++ b/framework/i18n/I18N.php @@ -4,91 +4,93 @@ namespace yii\i18n; use Yii; use yii\base\Component; +use yii\base\InvalidConfigException; class I18N extends Component { + /** + * @var array list of [[MessageSource]] configurations or objects. The array keys are message + * 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'. + */ + public $translations; + + public function init() + { + if (!isset($this->translations['yii'])) { + $this->translations['yii'] = array( + 'class' => 'yii\i18n\PhpMessageSource', + 'sourceLanguage' => 'en_US', + 'basePath' => '@yii/messages', + ); + } + if (!isset($this->translations['app'])) { + $this->translations['app'] = array( + 'class' => 'yii\i18n\PhpMessageSource', + 'sourceLanguage' => 'en_US', + 'basePath' => '@app/messages', + ); + } + } + public function translate($message, $params = array(), $language = null) { if ($language === null) { $language = Yii::$app->language; } - if (strpos($message, '|') !== false && preg_match('/^([\w\-\.]+)\|(.*)/', $message, $matches)) { + // allow chars for category: word chars, ".", "-", "/","\" + if (strpos($message, '|') !== false && preg_match('/^([\w\-\\/\.\\\\]+)\|(.*)/', $message, $matches)) { $category = $matches[1]; $message = $matches[2]; } else { $category = 'app'; } -// $message = $this->getMessageSource($category)->translate($category, $message, $language); -// -// if (!is_array($params)) { -// $params = array($params); -// } -// -// if (isset($params[0])) { -// $message = $this->getPluralFormat($message, $params[0], $language); -// if (!isset($params['{n}'])) { -// $params['{n}'] = $params[0]; -// } -// unset($params[0]); -// } + $message = $this->getMessageSource($category)->translate($category, $message, $language); - return $params === array() ? $message : strtr($message, $params); - } + if (!is_array($params)) { + $params = array($params); + } - public function getLocale($language) - { + if (isset($params[0])) { + $message = $this->getPluralForm($message, $params[0], $language); + if (!isset($params['{n}'])) { + $params['{n}'] = $params[0]; + } + unset($params[0]); + } + return $params === array() ? $message : strtr($message, $params); } public function getMessageSource($category) { - return $category === 'yii' ? $this->getMessages() : $this->getCoreMessages(); - } - - private $_coreMessages; - private $_messages; - - public function getCoreMessages() - { - if (is_object($this->_coreMessages)) { - return $this->_coreMessages; - } elseif ($this->_coreMessages === null) { - return $this->_coreMessages = new PhpMessageSource(array( - 'sourceLanguage' => 'en_US', - 'basePath' => '@yii/messages', - )); + if (isset($this->translations[$category])) { + $source = $this->translations[$category]; } else { - return $this->_coreMessages = Yii::createObject($this->_coreMessages); + // try wildcard matching + foreach ($this->translations as $pattern => $config) { + if (substr($pattern, -1) === '*' && strpos($category, rtrim($pattern, '*')) === 0) { + $source = $config; + break; + } + } } - } - - public function setCoreMessages($config) - { - $this->_coreMessages = $config; - } - - public function getMessages() - { - if (is_object($this->_messages)) { - return $this->_messages; - } elseif ($this->_messages === null) { - return $this->_messages = new PhpMessageSource(array( - 'sourceLanguage' => 'en_US', - 'basePath' => '@app/messages', - )); + if (isset($source)) { + return $source instanceof MessageSource ? $source : Yii::createObject($source); } else { - return $this->_messages = Yii::createObject($this->_messages); + throw new InvalidConfigException("Unable to locate message source for category '$category'."); } } - public function setMessages($config) + public function getLocale($language) { - $this->_messages = $config; + } - protected function getPluralFormat($message, $number, $language) + protected function getPluralForm($message, $number, $language) { if (strpos($message, '|') === false) { return $message; @@ -96,7 +98,7 @@ class I18N extends Component $chunks = explode('|', $message); $rules = $this->getLocale($language)->getPluralRules(); foreach ($rules as $i => $rule) { - if (isset($chunks[$i]) && self::evaluate($rule, $number)) { + if (isset($chunks[$i]) && $this->evaluate($rule, $number)) { return $chunks[$i]; } } @@ -110,7 +112,7 @@ class I18N extends Component * @param mixed $n the number value * @return boolean the expression result */ - protected static function evaluate($expression, $n) + protected function evaluate($expression, $n) { return @eval("return $expression;"); } diff --git a/framework/i18n/PhpMessageSource.php b/framework/i18n/PhpMessageSource.php index 5c7374a..6b12353 100644 --- a/framework/i18n/PhpMessageSource.php +++ b/framework/i18n/PhpMessageSource.php @@ -36,6 +36,18 @@ class PhpMessageSource extends MessageSource * the "messages" subdirectory of the application directory (e.g. "protected/messages"). */ public $basePath = '@app/messages'; + /** + * @var array mapping between message categories and the corresponding message file paths. + * The file paths are relative to [[basePath]]. For example, + * + * ~~~ + * array( + * 'core' => 'core.php', + * 'ext' => 'extensions.php', + * ) + * ~~~ + */ + public $fileMap; /** * Loads the message translation for the specified language and category. @@ -45,7 +57,14 @@ class PhpMessageSource extends MessageSource */ protected function loadMessages($category, $language) { - $messageFile = Yii::getAlias($this->basePath) . "/$language/$category.php"; + $messageFile = Yii::getAlias($this->basePath) . "/$language/"; + if (isset($this->fileMap[$category])) { + $messageFile .= $this->fileMap[$category]; + } elseif (($pos = strrpos($category, '\\')) !== false) { + $messageFile .= (substr($category, $pos) . '.php'); + } else { + $messageFile .= "$category.php"; + } if (is_file($messageFile)) { $messages = include($messageFile); if (!is_array($messages)) { @@ -53,7 +72,7 @@ class PhpMessageSource extends MessageSource } return $messages; } else { - Yii::error("Message file not found: $messageFile", __CLASS__); + Yii::error("The message file for category '$category' does not exist: $messageFile", __CLASS__); return array(); } } diff --git a/framework/logging/Target.php b/framework/logging/Target.php index 32d12d8..b88e78d 100644 --- a/framework/logging/Target.php +++ b/framework/logging/Target.php @@ -192,8 +192,7 @@ abstract class Target extends \yii\base\Component $matched = empty($this->categories); foreach ($this->categories as $category) { - $prefix = rtrim($category, '*'); - if (strpos($message[2], $prefix) === 0 && ($message[2] === $category || $prefix !== $category)) { + if ($message[2] === $category || substr($category, -1) === '*' && strpos($message[2], rtrim($category, '*')) === 0) { $matched = true; break; } diff --git a/framework/widgets/FragmentCache.php b/framework/widgets/FragmentCache.php new file mode 100644 index 0000000..e6805f5 --- /dev/null +++ b/framework/widgets/FragmentCache.php @@ -0,0 +1,294 @@ + + * @since 2.0 + */ +class FragmentCache extends Widget +{ + /** + * Prefix to the keys for storing cached data + */ + const CACHE_KEY_PREFIX = 'Yii.COutputCache.'; + + /** + * @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 in cache. Defaults to 60 seconds. + * If it is 0, existing cached content would be removed from the cache. + * If it is a negative value, the cache will be disabled (any existing cached content will + * remain in the cache.) + * + * Note, if cache dependency changes or cache space is limited, + * the data may be purged out of cache earlier. + */ + public $duration = 60; + /** + * @var mixed the dependency that the cached content depends on. + * This can be either an object implementing {@link ICacheDependency} interface or an array + * specifying the configuration of the dependency object. For example, + *
                +	 * array(
                +	 *     'class'=>'CDbCacheDependency',
                +	 *     '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 boolean whether the content being cached should be differentiated according to route. + * A route consists of the requested controller ID and action ID. + * Defaults to true. + */ + public $varyByRoute = true; + /** + * @var boolean whether the content being cached should be differentiated according to user's language. + * A language is retrieved via Yii::app()->language. + * Defaults to false. + * @since 1.1.14 + */ + public $varyByLanguage = false; + /** + * @var array list of GET parameters that should participate in cache key calculation. + * By setting this property, the output cache will use different cached data + * for each different set of GET parameter values. + */ + public $varyByParam; + /** + * @var string a PHP expression whose result is used in the cache key calculation. + * By setting this property, the output cache will use different cached data + * for each different expression result. + * The expression can also be a valid PHP callback, + * including class method name (array(ClassName/Object, MethodName)), + * or anonymous function (PHP 5.3.0+). The function/method signature should be as follows: + *
                +	 * function foo($cache) { ... }
                +	 * 
                + * where $cache refers to the output cache component. + */ + public $varyByExpression; + /** + * @var array list of request types (e.g. GET, POST) for which the cache should be enabled only. + * Defaults to null, meaning all request types. + */ + public $requestTypes; + + private $_key; + private $_cache; + private $_contentCached; + private $_content; + private $_actions; + + /** + * 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->getIsContentCached()) { + $this->replayActions(); + } elseif ($this->_cache !== null) { + $this->getController()->getCachingStack()->push($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 ($this->getIsContentCached()) { + if ($this->getController()->isCachingStackEmpty()) { + echo $this->getController()->processDynamicOutput($this->_content); + } else { + echo $this->_content; + } + } elseif ($this->_cache !== null) { + $this->_content = ob_get_clean(); + $this->getController()->getCachingStack()->pop(); + $data = array($this->_content, $this->_actions); + if (is_array($this->dependency)) { + $this->dependency = Yii::createComponent($this->dependency); + } + $this->_cache->set($this->getCacheKey(), $data, $this->duration, $this->dependency); + + if ($this->getController()->isCachingStackEmpty()) { + echo $this->getController()->processDynamicOutput($this->_content); + } else { + echo $this->_content; + } + } + } + + /** + * @return boolean whether the content can be found from cache + */ + public function getIsContentCached() + { + if ($this->_contentCached !== null) { + return $this->_contentCached; + } else { + return $this->_contentCached = $this->checkContentCache(); + } + } + + /** + * Looks for content in cache. + * @return boolean whether the content is found in cache. + */ + protected function checkContentCache() + { + if ((empty($this->requestTypes) || in_array(Yii::app()->getRequest()->getRequestType(), $this->requestTypes)) + && ($this->_cache = $this->getCache()) !== null + ) { + if ($this->duration > 0 && ($data = $this->_cache->get($this->getCacheKey())) !== false) { + $this->_content = $data[0]; + $this->_actions = $data[1]; + return true; + } + if ($this->duration == 0) { + $this->_cache->delete($this->getCacheKey()); + } + if ($this->duration <= 0) { + $this->_cache = null; + } + } + return false; + } + + /** + * @return ICache the cache used for caching the content. + */ + protected function getCache() + { + return Yii::app()->getComponent($this->cacheID); + } + + /** + * Caclulates the base cache key. + * The calculated key will be further variated in {@link getCacheKey}. + * Derived classes may override this method if more variations are needed. + * @return string basic cache key without variations + */ + protected function getBaseCacheKey() + { + return self::CACHE_KEY_PREFIX . $this->getId() . '.'; + } + + /** + * Calculates the cache key. + * The key is calculated based on {@link getBaseCacheKey} and other factors, including + * {@link varyByRoute}, {@link varyByParam}, {@link varyBySession} and {@link varyByLanguage}. + * @return string cache key + */ + protected function getCacheKey() + { + if ($this->_key !== null) { + return $this->_key; + } else { + $key = $this->getBaseCacheKey() . '.'; + if ($this->varyByRoute) { + $controller = $this->getController(); + $key .= $controller->getUniqueId() . '/'; + if (($action = $controller->getAction()) !== null) { + $key .= $action->getId(); + } + } + $key .= '.'; + + if ($this->varyBySession) { + $key .= Yii::app()->getSession()->getSessionID(); + } + $key .= '.'; + + if (is_array($this->varyByParam) && isset($this->varyByParam[0])) { + $params = array(); + foreach ($this->varyByParam as $name) { + if (isset($_GET[$name])) { + $params[$name] = $_GET[$name]; + } else { + $params[$name] = ''; + } + } + $key .= serialize($params); + } + $key .= '.'; + + if ($this->varyByExpression !== null) { + $key .= $this->evaluateExpression($this->varyByExpression); + } + $key .= '.'; + + if ($this->varyByLanguage) { + $key .= Yii::app()->language; + } + $key .= '.'; + + return $this->_key = $key; + } + } + + /** + * Records a method call when this output cache is in effect. + * When the content is served from the output cache, the recorded + * method will be re-invoked. + * @param string $context a property name of the controller. The property should refer to an object + * whose method is being recorded. If empty it means the controller itself. + * @param string $method the method name + * @param array $params parameters passed to the method + */ + public function recordAction($context, $method, $params) + { + $this->_actions[] = array($context, $method, $params); + } + + /** + * Replays the recorded method calls. + */ + protected function replayActions() + { + if (empty($this->_actions)) { + return; + } + $controller = $this->getController(); + $cs = Yii::app()->getClientScript(); + foreach ($this->_actions as $action) { + if ($action[0] === 'clientScript') { + $object = $cs; + } elseif ($action[0] === '') { + $object = $controller; + } else { + $object = $controller->{$action[0]}; + } + if (method_exists($object, $action[1])) { + call_user_func_array(array($object, $action[1]), $action[2]); + } elseif ($action[0] === '' && function_exists($action[1])) { + call_user_func_array($action[1], $action[2]); + } else { + throw new CException(Yii::t('yii', 'Unable to replay the action "{object}.{method}". The method does not exist.', + array('object' => $action[0], + 'method' => $action[1]))); + } + } + } +} \ No newline at end of file diff --git a/todo.md b/todo.md index 4d5343a..f102f41 100644 --- a/todo.md +++ b/todo.md @@ -39,7 +39,6 @@ memo * consider to be released as a separate tool for user app docs - i18n * consider using PHP built-in support and data - * message translations, choice format * formatting: number and date * parsing?? * make dates/date patterns uniform application-wide including JUI, formats etc. From 392b293d33e428a3e7540b9dbc1313a37928e528 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Mon, 25 Mar 2013 14:27:15 -0400 Subject: [PATCH 096/117] FragmentCache WIP --- framework/base/ActionFilter.php | 12 +- framework/base/View.php | 5 +- framework/caching/Cache.php | 18 +-- framework/caching/DbDependency.php | 37 ++--- framework/db/Command.php | 8 +- framework/db/Schema.php | 7 +- framework/helpers/ConsoleColor.php | 8 - framework/web/CacheSession.php | 3 +- framework/widgets/FragmentCache.php | 282 ++++++++++-------------------------- 9 files changed, 138 insertions(+), 242 deletions(-) diff --git a/framework/base/ActionFilter.php b/framework/base/ActionFilter.php index 2655c5a..0ae7ab8 100644 --- a/framework/base/ActionFilter.php +++ b/framework/base/ActionFilter.php @@ -11,9 +11,19 @@ namespace yii\base; * @author Qiang Xue * @since 2.0 */ -class Filter extends Behavior +class ActionFilter extends Behavior { /** + * @var array list of action IDs that this filter should apply to. If this property is not set, + * then the filter applies to all actions, unless they are listed in [[except]]. + */ + public $only; + /** + * @var array list of action IDs that this filter should not apply to. + */ + public $except = array(); + + /** * Declares event handlers for the [[owner]]'s events. * @return array events (array keys) and the corresponding event handler methods (array values). */ diff --git a/framework/base/View.php b/framework/base/View.php index baa1d10..18748f2 100644 --- a/framework/base/View.php +++ b/framework/base/View.php @@ -337,15 +337,16 @@ class View extends Component * ~~~ * * @param string $id a unique ID identifying the fragment to be cached. - * @param array $properties initial property values for [[\yii\widgets\OutputCache]] + * @param array $properties initial property values for [[\yii\widgets\FragmentCache]] * @return boolean whether you should generate the content for caching. * False if the cached version is available. */ public function beginCache($id, $properties = array()) { $properties['id'] = $id; + /** @var $cache \yii\widgets\FragmentCache */ $cache = $this->beginWidget('yii\widgets\OutputCache', $properties); - if ($cache->getIsContentCached()) { + if ($cache->getCachedContent() !== false) { $this->endCache(); return false; } else { diff --git a/framework/caching/Cache.php b/framework/caching/Cache.php index f3f1bc0..70cf8cb 100644 --- a/framework/caching/Cache.php +++ b/framework/caching/Cache.php @@ -8,6 +8,7 @@ namespace yii\caching; use yii\base\Component; +use yii\helpers\StringHelper; /** * Cache is the base class for cache classes supporting different cache storage implementation. @@ -70,13 +71,13 @@ abstract class Cache extends Component implements \ArrayAccess /** - * Builds a normalized cache key from one or multiple parameters. + * Builds a normalized cache key from a given key. * * The generated key contains letters and digits only, and its length is no more than 32. * - * If only one parameter is given and it is already a normalized key, then - * it will be returned back without change. Otherwise, a normalized key - * is generated by serializing all given parameters and applying MD5 hashing. + * If the given key is a string containing alphanumeric characters only and no more than 32 characters, + * then the key will be returned back without change. Otherwise, a normalized key + * is generated by serializing the given key and applying MD5 hashing. * * The following example builds a cache key using three parameters: * @@ -84,16 +85,15 @@ abstract class Cache extends Component implements \ArrayAccess * $key = $cache->buildKey($className, $method, $id); * ~~~ * - * @param string $key the first parameter + * @param array|string $key the key to be normalized * @return string the generated cache key */ public function buildKey($key) { - if (func_num_args() === 1 && ctype_alnum($key) && strlen($key) <= 32) { - return (string)$key; + if (is_string($key)) { + return ctype_alnum($key) && StringHelper::strlen($key) <= 32 ? $key : md5($key); } else { - $params = func_get_args(); - return md5(json_encode($params)); + return md5(json_encode($key)); } } diff --git a/framework/caching/DbDependency.php b/framework/caching/DbDependency.php index 9c6e1f1..247109b 100644 --- a/framework/caching/DbDependency.php +++ b/framework/caching/DbDependency.php @@ -7,15 +7,15 @@ namespace yii\caching; +use Yii; use yii\base\InvalidConfigException; use yii\db\Connection; -use yii\db\Query; /** * DbDependency represents a dependency based on the query result of a SQL statement. * * If the query result changes, the dependency is considered as changed. - * The query is specified via the [[query]] property. + * The query is specified via the [[sql]] property. * * @author Qiang Xue * @since 2.0 @@ -27,23 +27,25 @@ class DbDependency extends Dependency */ public $connectionID = 'db'; /** - * @var Query the SQL query whose result is used to determine if the dependency has been changed. + * @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. */ - public $query; + public $sql; /** - * @var Connection the DB connection instance + * @var array the parameters (name=>value) to be bound to the SQL statement specified by [[sql]]. */ - private $_db; + public $params; /** * Constructor. - * @param Query $query the SQL query whose result is used to determine if the dependency has been changed. + * @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 */ - public function __construct($query = null, $config = array()) + public function __construct($sql, $params = array(), $config = array()) { - $this->query = $query; + $this->sql = $sql; + $this->params = $params; parent::__construct($config); } @@ -66,22 +68,23 @@ class DbDependency extends Dependency protected function generateDependencyData() { $db = $this->getDb(); - /** - * @var \yii\db\Command $command - */ - $command = $this->query->createCommand($db); if ($db->enableQueryCache) { // temporarily disable and re-enable query caching $db->enableQueryCache = false; - $result = $command->queryRow(); + $result = $db->createCommand($this->sql, $this->params)->queryRow(); $db->enableQueryCache = true; } else { - $result = $command->queryRow(); + $result = $db->createCommand($this->sql, $this->params)->queryRow(); } 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. @@ -89,11 +92,11 @@ class DbDependency extends Dependency public function getDb() { if ($this->_db === null) { - $db = \Yii::$app->getComponent($this->connectionID); + $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."); + throw new InvalidConfigException("DbCacheDependency::connectionID must refer to the ID of a DB application component."); } } return $this->_db; diff --git a/framework/db/Command.php b/framework/db/Command.php index 0861b8d..c2b2e05 100644 --- a/framework/db/Command.php +++ b/framework/db/Command.php @@ -389,7 +389,13 @@ class Command extends \yii\base\Component } if (isset($cache)) { - $cacheKey = $cache->buildKey(__CLASS__, $db->dsn, $db->username, $sql, $paramLog); + $cacheKey = $cache->buildKey(array( + __CLASS__, + $db->dsn, + $db->username, + $sql, + $paramLog, + )); if (($result = $cache->get($cacheKey)) !== false) { \Yii::trace('Query result found in cache', __CLASS__); return $result; diff --git a/framework/db/Schema.php b/framework/db/Schema.php index 5a6ffda..5fe6121 100644 --- a/framework/db/Schema.php +++ b/framework/db/Schema.php @@ -109,7 +109,12 @@ abstract class Schema extends \yii\base\Object */ public function getCacheKey($cache, $name) { - return $cache->buildKey(__CLASS__, $this->db->dsn, $this->db->username, $name); + return $cache->buildKey(array( + __CLASS__, + $this->db->dsn, + $this->db->username, + $name, + )); } /** diff --git a/framework/helpers/ConsoleColor.php b/framework/helpers/ConsoleColor.php index c64db24..429aeb1 100644 --- a/framework/helpers/ConsoleColor.php +++ b/framework/helpers/ConsoleColor.php @@ -7,15 +7,7 @@ namespace yii\helpers; -// todo define how subclassing will work -// todo add a run() or render() method // todo test this on all kinds of terminals, especially windows (check out lib ncurses) -// todo not sure if all methods should be static - -// todo subclass DetailView -// todo subclass GridView -// todo more subclasses - /** * Console View is the base class for console view components diff --git a/framework/web/CacheSession.php b/framework/web/CacheSession.php index 25524a0..d7882a6 100644 --- a/framework/web/CacheSession.php +++ b/framework/web/CacheSession.php @@ -45,6 +45,7 @@ class CacheSession extends Session { return true; } + /** * Returns the cache instance used for storing session data. * @return Cache the cache instance @@ -114,6 +115,6 @@ class CacheSession extends Session */ protected function calculateKey($id) { - return $this->getCache()->buildKey(__CLASS__, $id); + return $this->getCache()->buildKey(array(__CLASS__, $id)); } } diff --git a/framework/widgets/FragmentCache.php b/framework/widgets/FragmentCache.php index e6805f5..38073f0 100644 --- a/framework/widgets/FragmentCache.php +++ b/framework/widgets/FragmentCache.php @@ -7,7 +7,11 @@ namespace yii\widgets; +use Yii; +use yii\base\InvalidConfigException; use yii\base\Widget; +use yii\caching\Cache; +use yii\caching\Dependency; /** * @author Qiang Xue @@ -16,81 +20,47 @@ use yii\base\Widget; class FragmentCache extends Widget { /** - * Prefix to the keys for storing cached data - */ - const CACHE_KEY_PREFIX = 'Yii.COutputCache.'; - - /** * @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 in cache. Defaults to 60 seconds. - * If it is 0, existing cached content would be removed from the cache. - * If it is a negative value, the cache will be disabled (any existing cached content will - * remain in the cache.) - * - * Note, if cache dependency changes or cache space is limited, - * the data may be purged out of cache earlier. + * @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 mixed the dependency that the cached content depends on. - * This can be either an object implementing {@link ICacheDependency} interface or an array - * specifying the configuration of the dependency object. For example, - *
                +	 * @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'=>'CDbCacheDependency',
                -	 *     'sql'=>'SELECT MAX(lastModified) FROM Post',
                +	 *     '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 boolean whether the content being cached should be differentiated according to route. - * A route consists of the requested controller ID and action ID. - * Defaults to true. - */ - public $varyByRoute = true; - /** - * @var boolean whether the content being cached should be differentiated according to user's language. - * A language is retrieved via Yii::app()->language. - * Defaults to false. - * @since 1.1.14 - */ - public $varyByLanguage = false; - /** - * @var array list of GET parameters that should participate in cache key calculation. - * By setting this property, the output cache will use different cached data - * for each different set of GET parameter values. - */ - public $varyByParam; - /** - * @var string a PHP expression whose result is used in the cache key calculation. - * By setting this property, the output cache will use different cached data - * for each different expression result. - * The expression can also be a valid PHP callback, - * including class method name (array(ClassName/Object, MethodName)), - * or anonymous function (PHP 5.3.0+). The function/method signature should be as follows: - *
                -	 * function foo($cache) { ... }
                -	 * 
                - * where $cache refers to the output cache component. + * @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 $varyByExpression; + public $variations; /** - * @var array list of request types (e.g. GET, POST) for which the cache should be enabled only. - * Defaults to null, meaning all request types. + * @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 $requestTypes; - - private $_key; - private $_cache; - private $_contentCached; - private $_content; - private $_actions; + public $enabled = true; /** * Marks the start of content to be cached. @@ -100,10 +70,7 @@ class FragmentCache extends Widget */ public function init() { - if ($this->getIsContentCached()) { - $this->replayActions(); - } elseif ($this->_cache !== null) { - $this->getController()->getCachingStack()->push($this); + if ($this->getCachedContent() === false && $this->getCache() !== null) { ob_start(); ob_implicit_flush(false); } @@ -117,178 +84,89 @@ class FragmentCache extends Widget */ public function run() { - if ($this->getIsContentCached()) { - if ($this->getController()->isCachingStackEmpty()) { - echo $this->getController()->processDynamicOutput($this->_content); - } else { - echo $this->_content; - } - } elseif ($this->_cache !== null) { - $this->_content = ob_get_clean(); - $this->getController()->getCachingStack()->pop(); - $data = array($this->_content, $this->_actions); + if (($content = $this->getCachedContent()) !== false) { + echo $content; + } elseif (($cache = $this->getCache()) !== false) { + $content = ob_get_clean(); if (is_array($this->dependency)) { - $this->dependency = Yii::createComponent($this->dependency); - } - $this->_cache->set($this->getCacheKey(), $data, $this->duration, $this->dependency); - - if ($this->getController()->isCachingStackEmpty()) { - echo $this->getController()->processDynamicOutput($this->_content); - } else { - echo $this->_content; + $this->dependency = Yii::createObject($this->dependency); } + $cache->set($this->calculateKey(), $content, $this->duration, $this->dependency); + echo $content; } } /** - * @return boolean whether the content can be found from cache + * @var string|boolean the cached content. False if the content is not cached. */ - public function getIsContentCached() - { - if ($this->_contentCached !== null) { - return $this->_contentCached; - } else { - return $this->_contentCached = $this->checkContentCache(); - } - } + private $_content; /** - * Looks for content in cache. - * @return boolean whether the content is found in cache. + * Returns the cached content if available. + * @return string|boolean the cached content. False is returned if valid content is not found in the cache. */ - protected function checkContentCache() + public function getCachedContent() { - if ((empty($this->requestTypes) || in_array(Yii::app()->getRequest()->getRequestType(), $this->requestTypes)) - && ($this->_cache = $this->getCache()) !== null - ) { - if ($this->duration > 0 && ($data = $this->_cache->get($this->getCacheKey())) !== false) { - $this->_content = $data[0]; - $this->_actions = $data[1]; - return true; - } - if ($this->duration == 0) { - $this->_cache->delete($this->getCacheKey()); - } - if ($this->duration <= 0) { - $this->_cache = null; + if ($this->_content === null) { + if (($cache = $this->getCache()) !== null) { + $key = $this->calculateKey(); + $this->_content = $cache->get($key); + } else { + $this->_content = false; } } - return false; + return $this->_content; } /** - * @return ICache the cache used for caching the content. + * 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 getCache() + protected function calculateKey() { - return Yii::app()->getComponent($this->cacheID); + $factors = array(__CLASS__, $this->getId()); + if (is_array($this->variations)) { + foreach ($this->variations as $factor) { + $factors[] = $factor; + } + } + return $this->getCache()->buildKey($factors); } /** - * Caclulates the base cache key. - * The calculated key will be further variated in {@link getCacheKey}. - * Derived classes may override this method if more variations are needed. - * @return string basic cache key without variations + * @var Cache */ - protected function getBaseCacheKey() - { - return self::CACHE_KEY_PREFIX . $this->getId() . '.'; - } + private $_cache; /** - * Calculates the cache key. - * The key is calculated based on {@link getBaseCacheKey} and other factors, including - * {@link varyByRoute}, {@link varyByParam}, {@link varyBySession} and {@link varyByLanguage}. - * @return string cache key + * 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. */ - protected function getCacheKey() + public function getCache() { - if ($this->_key !== null) { - return $this->_key; - } else { - $key = $this->getBaseCacheKey() . '.'; - if ($this->varyByRoute) { - $controller = $this->getController(); - $key .= $controller->getUniqueId() . '/'; - if (($action = $controller->getAction()) !== null) { - $key .= $action->getId(); - } - } - $key .= '.'; - - if ($this->varyBySession) { - $key .= Yii::app()->getSession()->getSessionID(); - } - $key .= '.'; - - if (is_array($this->varyByParam) && isset($this->varyByParam[0])) { - $params = array(); - foreach ($this->varyByParam as $name) { - if (isset($_GET[$name])) { - $params[$name] = $_GET[$name]; - } else { - $params[$name] = ''; - } - } - $key .= serialize($params); - } - $key .= '.'; - - if ($this->varyByExpression !== null) { - $key .= $this->evaluateExpression($this->varyByExpression); - } - $key .= '.'; - - if ($this->varyByLanguage) { - $key .= Yii::app()->language; + 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.'); } - $key .= '.'; - - return $this->_key = $key; } + return $this->_cache; } /** - * Records a method call when this output cache is in effect. - * When the content is served from the output cache, the recorded - * method will be re-invoked. - * @param string $context a property name of the controller. The property should refer to an object - * whose method is being recorded. If empty it means the controller itself. - * @param string $method the method name - * @param array $params parameters passed to the method - */ - public function recordAction($context, $method, $params) - { - $this->_actions[] = array($context, $method, $params); - } - - /** - * Replays the recorded method calls. + * Sets the cache instance used by the session component. + * @param Cache $value the cache instance */ - protected function replayActions() + public function setCache($value) { - if (empty($this->_actions)) { - return; - } - $controller = $this->getController(); - $cs = Yii::app()->getClientScript(); - foreach ($this->_actions as $action) { - if ($action[0] === 'clientScript') { - $object = $cs; - } elseif ($action[0] === '') { - $object = $controller; - } else { - $object = $controller->{$action[0]}; - } - if (method_exists($object, $action[1])) { - call_user_func_array(array($object, $action[1]), $action[2]); - } elseif ($action[0] === '' && function_exists($action[1])) { - call_user_func_array($action[1], $action[2]); - } else { - throw new CException(Yii::t('yii', 'Unable to replay the action "{object}.{method}". The method does not exist.', - array('object' => $action[0], - 'method' => $action[1]))); - } - } + $this->_cache = $value; } } \ No newline at end of file From 2f3cc8f16ecd9bfb55e847b5245faf949ce54a84 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Mon, 25 Mar 2013 14:37:25 -0400 Subject: [PATCH 097/117] finished ActionFilter. --- framework/base/ActionFilter.php | 43 +++++++++++++++++++++++++++++++++++++---- todo.md | 17 ---------------- 2 files changed, 39 insertions(+), 21 deletions(-) diff --git a/framework/base/ActionFilter.php b/framework/base/ActionFilter.php index 0ae7ab8..1f82e5d 100644 --- a/framework/base/ActionFilter.php +++ b/framework/base/ActionFilter.php @@ -30,8 +30,8 @@ class ActionFilter extends Behavior public function events() { return array( - 'beforeAction' => 'beforeAction', - 'afterAction' => 'afterAction', + 'beforeAction' => 'beforeFilter', + 'afterAction' => 'afterFilter', ); } @@ -39,8 +39,11 @@ class ActionFilter extends Behavior * @param ActionEvent $event * @return boolean */ - public function beforeAction($event) + public function beforeFilter($event) { + if ($this->isActive($event->action)) { + $event->isValid = $this->beforeAction($event->action); + } return $event->isValid; } @@ -48,8 +51,40 @@ class ActionFilter extends Behavior * @param ActionEvent $event * @return boolean */ - public function afterAction($event) + public function afterFilter($event) { + if ($this->isActive($event->action)) { + $this->afterAction($event->action); + } + } + + /** + * 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) + { + return true; + } + + /** + * 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) + { + } + /** + * Returns a value indicating whether the filer is active for the given action. + * @param Action $action the action being filtered + * @return boolean whether the filer is active for the given action. + */ + protected function isActive($action) + { + return !in_array($action->id, $this->except, true) && (empty($this->only) || in_array($action->id, $this->only, true)); } } \ No newline at end of file diff --git a/todo.md b/todo.md index f102f41..f66d3c1 100644 --- a/todo.md +++ b/todo.md @@ -1,21 +1,4 @@ -- console - * If console is executed using Windows, do not use colors. If not, use colors. Allow to override via console application settings. -- db - * pgsql, sql server, oracle, db2 drivers - * unit tests on different DB drivers - * document-based (should allow storage-specific methods additionally to generic ones) - * mongodb (put it under framework/db/mongodb) - * key-value-based (should allow storage-specific methods additionally to generic ones) - * redis (put it under framework/db/redis or perhaps framework/caching?) -- base - * TwigViewRenderer (Alex) - * SmartyViewRenderer (Alex) -- logging - * WebTarget (TBD after web is in place): should consider using javascript and make it into a toolbar - * ProfileTarget (TBD after web is in place): should consider using javascript and make it into a toolbar - * unit tests - caching - * backend-specific unit tests * dependency unit tests - validators * Refactor validators to add validateValue() for every validator, if possible. Check if value is an array. From 80dbaaca8bb390b735b1384f05aad6a71acfb7c8 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Mon, 25 Mar 2013 19:37:42 -0400 Subject: [PATCH 098/117] bug fixes. --- framework/base/Application.php | 3 +++ framework/base/ErrorHandler.php | 8 ++++---- framework/views/exception.php | 2 +- framework/web/Application.php | 6 ++++++ 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/framework/base/Application.php b/framework/base/Application.php index 31087e2..fd2ecad 100644 --- a/framework/base/Application.php +++ b/framework/base/Application.php @@ -479,6 +479,9 @@ class Application extends Module $msg = (string)$e; $msg .= "\nPrevious exception:\n"; $msg .= (string)$exception; + if (YII_DEBUG) { + echo $msg; + } $msg .= "\n\$_SERVER = " . var_export($_SERVER, true); error_log($msg); exit(1); diff --git a/framework/base/ErrorHandler.php b/framework/base/ErrorHandler.php index a3ab137..f71b8c8 100644 --- a/framework/base/ErrorHandler.php +++ b/framework/base/ErrorHandler.php @@ -78,7 +78,7 @@ class ErrorHandler extends Component if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest') { \Yii::$app->renderException($exception); } else { - $view = new View($this); + $view = new View; if (!YII_DEBUG || $exception instanceof UserException) { $viewName = $this->errorView; } else { @@ -86,7 +86,7 @@ class ErrorHandler extends Component } echo $view->render($viewName, array( 'exception' => $exception, - )); + ), $this); } } else { \Yii::$app->renderException($exception); @@ -255,8 +255,8 @@ class ErrorHandler extends Component { $view = new View; $name = !YII_DEBUG || $exception instanceof HttpException ? $this->errorView : $this->exceptionView; - echo $view->render($this, $name, array( + echo $view->render($name, array( 'exception' => $exception, - )); + ), $this); } } diff --git a/framework/views/exception.php b/framework/views/exception.php index 6257ffe..db29302 100644 --- a/framework/views/exception.php +++ b/framework/views/exception.php @@ -183,7 +183,7 @@ $title = $context->htmlEncode($exception instanceof \yii\base\Exception || $exce
                - versionInfo : ''?> + getVersionInfo() : ''?>
                diff --git a/framework/web/Application.php b/framework/web/Application.php index 61e84a3..6e0cc73 100644 --- a/framework/web/Application.php +++ b/framework/web/Application.php @@ -6,6 +6,7 @@ */ namespace yii\web; + use yii\base\InvalidParamException; /** @@ -17,6 +18,11 @@ use yii\base\InvalidParamException; class Application extends \yii\base\Application { /** + * @var string the default route of this application. Defaults to 'site'. + */ + public $defaultRoute = 'site'; + + /** * Sets default path aliases. */ public function registerDefaultAliases() From 27f5c49bd5e0dd49ce1f53a64c986a44823eb702 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Mon, 25 Mar 2013 19:39:09 -0400 Subject: [PATCH 099/117] fragment cache WIP --- framework/base/View.php | 19 +++++++++++++++++++ framework/widgets/FragmentCache.php | 33 +++++++++++++++++++++++++++++---- 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/framework/base/View.php b/framework/base/View.php index 18748f2..7170251 100644 --- a/framework/base/View.php +++ b/framework/base/View.php @@ -156,6 +156,25 @@ class View extends Component return ob_get_clean(); } + public function renderDynamic($statements) + { + if (!empty($this->cachingStack)) { + $n = count($this->_dynamicOutput); + $placeholder = ""; + foreach ($this->cachingStack as $cache) { + $cache->dynamicPlaceholders[$placeholder] = $statements; + } + return $placeholder; + } else { + return $this->evaluateDynamicContent($statements); + } + } + + public function evaluateDynamicContent($statements) + { + return eval($statements); + } + /** * Finds the view file based on the given view name. * diff --git a/framework/widgets/FragmentCache.php b/framework/widgets/FragmentCache.php index 38073f0..b56b86f 100644 --- a/framework/widgets/FragmentCache.php +++ b/framework/widgets/FragmentCache.php @@ -61,6 +61,16 @@ class FragmentCache extends Widget * 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 sued. If not set, + * the view registered with the application will be used. This is mainly used by dynamic content feature. + */ + public $view; + /** + * @var array + */ + public $dynamicPlaceholders; + /** * Marks the start of content to be cached. @@ -70,7 +80,11 @@ class FragmentCache extends Widget */ public function init() { + if ($this->view === null) { + $this->view = Yii::$app->getView(); + } if ($this->getCachedContent() === false && $this->getCache() !== null) { + array_push($this->view->cachingStack, $this); ob_start(); ob_implicit_flush(false); } @@ -88,10 +102,12 @@ class FragmentCache extends Widget echo $content; } elseif (($cache = $this->getCache()) !== false) { $content = ob_get_clean(); + array_pop($this->view->cachingStack); if (is_array($this->dependency)) { $this->dependency = Yii::createObject($this->dependency); } - $cache->set($this->calculateKey(), $content, $this->duration, $this->dependency); + $data = array($content, $this->dynamicPlaceholders); + $cache->set($this->calculateKey(), $data, $this->duration, $this->dependency); echo $content; } } @@ -108,11 +124,20 @@ class FragmentCache extends Widget public function getCachedContent() { if ($this->_content === null) { + $this->_content = false; if (($cache = $this->getCache()) !== null) { $key = $this->calculateKey(); - $this->_content = $cache->get($key); - } else { - $this->_content = false; + $data = $cache->get($key); + if (is_array($data) && count($data) === 2) { + list ($content, $placeholders) = $data; + if (is_array($placeholders) && count($placeholders) > 0) { + foreach ($placeholders as $name => $statements) { + $placeholders[$name] = $this->view->evaluateDynamicContent($statements); + } + $content = strtr($content, $placeholders); + } + $this->_content = $content; + } } } return $this->_content; From de5c304f64426d0585205469fdf5d0a9a386ecac Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Mon, 25 Mar 2013 22:26:25 -0400 Subject: [PATCH 100/117] finished fragment caching. --- framework/YiiBase.php | 2 +- framework/base/Component.php | 6 ++-- framework/base/Object.php | 8 ++--- framework/base/View.php | 60 ++++++++++++++++++++++++++++++------- framework/widgets/FragmentCache.php | 30 ++++++++++++++----- 5 files changed, 80 insertions(+), 26 deletions(-) diff --git a/framework/YiiBase.php b/framework/YiiBase.php index 678856c..16e237d 100644 --- a/framework/YiiBase.php +++ b/framework/YiiBase.php @@ -298,7 +298,7 @@ class YiiBase } } - if (isset($classFile, $alias)) { + if (isset($classFile, $alias) && is_file($classFile)) { if (!YII_DEBUG || basename(realpath($classFile)) === basename($alias) . '.php') { include($classFile); return true; diff --git a/framework/base/Component.php b/framework/base/Component.php index 515f7e1..2d081d3 100644 --- a/framework/base/Component.php +++ b/framework/base/Component.php @@ -58,7 +58,7 @@ class Component extends \yii\base\Object } } } - throw new UnknownPropertyException('Getting unknown property: ' . get_class($this) . '.' . $name); + throw new UnknownPropertyException('Getting unknown property: ' . get_class($this) . '::' . $name); } /** @@ -105,9 +105,9 @@ class Component extends \yii\base\Object } } if (method_exists($this, 'get' . $name)) { - throw new InvalidCallException('Setting read-only property: ' . get_class($this) . '.' . $name); + throw new InvalidCallException('Setting read-only property: ' . get_class($this) . '::' . $name); } else { - throw new UnknownPropertyException('Setting unknown property: ' . get_class($this) . '.' . $name); + throw new UnknownPropertyException('Setting unknown property: ' . get_class($this) . '::' . $name); } } diff --git a/framework/base/Object.php b/framework/base/Object.php index f589e38..3bd8378 100644 --- a/framework/base/Object.php +++ b/framework/base/Object.php @@ -65,7 +65,7 @@ class Object if (method_exists($this, $getter)) { return $this->$getter(); } else { - throw new UnknownPropertyException('Getting unknown property: ' . get_class($this) . '.' . $name); + throw new UnknownPropertyException('Getting unknown property: ' . get_class($this) . '::' . $name); } } @@ -86,9 +86,9 @@ class Object if (method_exists($this, $setter)) { $this->$setter($value); } elseif (method_exists($this, 'get' . $name)) { - throw new InvalidCallException('Setting read-only property: ' . get_class($this) . '.' . $name); + throw new InvalidCallException('Setting read-only property: ' . get_class($this) . '::' . $name); } else { - throw new UnknownPropertyException('Setting unknown property: ' . get_class($this) . '.' . $name); + throw new UnknownPropertyException('Setting unknown property: ' . get_class($this) . '::' . $name); } } @@ -129,7 +129,7 @@ class Object if (method_exists($this, $setter)) { $this->$setter(null); } elseif (method_exists($this, 'get' . $name)) { - throw new InvalidCallException('Unsetting read-only property: ' . get_class($this) . '.' . $name); + throw new InvalidCallException('Unsetting read-only property: ' . get_class($this) . '::' . $name); } } diff --git a/framework/base/View.php b/framework/base/View.php index 7170251..c7087c1 100644 --- a/framework/base/View.php +++ b/framework/base/View.php @@ -45,11 +45,21 @@ class View extends Component * through this property. */ public $clips; - /** - * @var Widget[] the widgets that are currently not ended + * @var Widget[] the widgets that are currently being rendered (not ended). This property + * is maintained by [[beginWidget()]] and [[endWidget()]] methods. Do not modify it directly. + */ + public $widgetStack = array(); + /** + * @var array a list of currently active fragment cache widgets. This property + * is used internally to implement the content caching feature. Do not modify it. + */ + public $cacheStack = array(); + /** + * @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. */ - private $_widgetStack = array(); + public $dynamicPlaceholders = array(); /** @@ -156,20 +166,47 @@ class View extends Component return ob_get_clean(); } + /** + * Renders dynamic content returned by the given PHP statements. + * This method is mainly used together with content caching (fragment caching and page caching) + * when some portions of the content (called *dynamic content*) should not be cached. + * The dynamic content must be returned by some PHP statements. + * @param string $statements the PHP statements for generating the dynamic content. + * @return string the placeholder of the dynamic content, or the dynamic content if there is no + * active content cache currently. + */ public function renderDynamic($statements) { - if (!empty($this->cachingStack)) { - $n = count($this->_dynamicOutput); + if (!empty($this->cacheStack)) { + $n = count($this->dynamicPlaceholders); $placeholder = ""; - foreach ($this->cachingStack as $cache) { - $cache->dynamicPlaceholders[$placeholder] = $statements; - } + $this->addDynamicPlaceholder($placeholder, $statements); return $placeholder; } else { return $this->evaluateDynamicContent($statements); } } + /** + * Adds a placeholder for dynamic content. + * This method is internally used. + * @param string $placeholder the placeholder name + * @param string $statements the PHP statements for generating the dynamic content + */ + public function addDynamicPlaceholder($placeholder, $statements) + { + foreach ($this->cacheStack as $cache) { + $cache->dynamicPlaceholders[$placeholder] = $statements; + } + $this->dynamicPlaceholders[$placeholder] = $statements; + } + + /** + * Evaluates the given PHP statements. + * This method is mainly used internally to implement dynamic content feature. + * @param string $statements the PHP statements to be evaluated. + * @return mixed the return value of the PHP statements. + */ public function evaluateDynamicContent($statements) { return eval($statements); @@ -267,7 +304,7 @@ class View extends Component public function beginWidget($class, $properties = array()) { $widget = $this->createWidget($class, $properties); - $this->_widgetStack[] = $widget; + $this->widgetStack[] = $widget; return $widget; } @@ -281,7 +318,7 @@ class View extends Component */ public function endWidget() { - $widget = array_pop($this->_widgetStack); + $widget = array_pop($this->widgetStack); if ($widget instanceof Widget) { $widget->run(); return $widget; @@ -363,8 +400,9 @@ 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\OutputCache', $properties); + $cache = $this->beginWidget('yii\widgets\FragmentCache', $properties); if ($cache->getCachedContent() !== false) { $this->endCache(); return false; diff --git a/framework/widgets/FragmentCache.php b/framework/widgets/FragmentCache.php index b56b86f..3bb321e 100644 --- a/framework/widgets/FragmentCache.php +++ b/framework/widgets/FragmentCache.php @@ -67,7 +67,8 @@ class FragmentCache extends Widget */ public $view; /** - * @var array + * @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; @@ -83,8 +84,8 @@ class FragmentCache extends Widget if ($this->view === null) { $this->view = Yii::$app->getView(); } - if ($this->getCachedContent() === false && $this->getCache() !== null) { - array_push($this->view->cachingStack, $this); + if ($this->getCache() !== null && $this->getCachedContent() === false) { + $this->view->cacheStack[] = $this; ob_start(); ob_implicit_flush(false); } @@ -100,14 +101,18 @@ class FragmentCache extends Widget { if (($content = $this->getCachedContent()) !== false) { echo $content; - } elseif (($cache = $this->getCache()) !== false) { + } elseif (($cache = $this->getCache()) !== null) { $content = ob_get_clean(); - array_pop($this->view->cachingStack); + 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; } } @@ -131,10 +136,13 @@ class FragmentCache extends Widget 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) { - $placeholders[$name] = $this->view->evaluateDynamicContent($statements); + $this->view->addDynamicPlaceholder($name, $statements); } - $content = strtr($content, $placeholders); } $this->_content = $content; } @@ -143,6 +151,14 @@ class FragmentCache extends Widget 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]]. From 0ed14a741d2c13f66e2023823975287694845621 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Mon, 25 Mar 2013 23:11:32 -0400 Subject: [PATCH 101/117] Finished PageCache. --- framework/web/PageCache.php | 110 ++++++++++++++++++++++++++++++++++++ framework/widgets/FragmentCache.php | 2 +- 2 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 framework/web/PageCache.php diff --git a/framework/web/PageCache.php b/framework/web/PageCache.php new file mode 100644 index 0000000..24cddea --- /dev/null +++ b/framework/web/PageCache.php @@ -0,0 +1,110 @@ + + * @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(); + } +} \ No newline at end of file diff --git a/framework/widgets/FragmentCache.php b/framework/widgets/FragmentCache.php index 3bb321e..d5185f8 100644 --- a/framework/widgets/FragmentCache.php +++ b/framework/widgets/FragmentCache.php @@ -62,7 +62,7 @@ class FragmentCache extends Widget */ public $enabled = true; /** - * @var \yii\base\View the view object within which this widget is sued. If not set, + * @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; From 1184e1e148b1f4fe1af61774e58581a3ffc2cba3 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Tue, 26 Mar 2013 15:27:15 -0400 Subject: [PATCH 102/117] 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 103/117] 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 104/117] 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 105/117] 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 106/117] 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 107/117] 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 108/117] 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 109/117] 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 110/117] 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 111/117] 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 112/117] 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 113/117] 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 114/117] 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 115/117] 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 116/117] 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 117/117] 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(); + } }