diff --git a/.travis.yml b/.travis.yml index e4b8278..01abd50 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,5 +10,6 @@ env: before_script: - sh -c "if [ '$DB' = 'mysql' ]; then mysql -e 'create database IF NOT EXISTS yiitest;'; fi" - + - psql -U postgres -c 'drop database if exists yiitest;'; + - psql -U postgres -c 'create database yiitest;'; script: phpunit \ No newline at end of file diff --git a/apps/advanced/README.md b/apps/advanced/README.md index c443c90..f2abc1e 100644 --- a/apps/advanced/README.md +++ b/apps/advanced/README.md @@ -10,7 +10,7 @@ if you have a project to be deployed for production soon. Thank you for using Yii 2 Advanced Application Template - an application template that works out-of-box and can be easily customized to fit for your needs. -Yii 2 Advanced Application Template is best suitable for large projects requiring frontend and backstage separation, +Yii 2 Advanced Application Template is best suitable for large projects requiring frontend and backend separation, deployment in different environments, configuration nesting etc. @@ -20,18 +20,18 @@ DIRECTORY STRUCTURE ``` common config/ contains shared configurations - models/ contains model classes used in both backstage and frontend + models/ contains model classes used in both backend and frontend console config/ contains console configurations controllers/ contains console controllers (commands) migrations/ contains database migrations models/ contains console-specific model classes runtime/ contains files generated during runtime -backstage +backend assets/ contains application assets such as JavaScript and CSS - config/ contains backstage configurations + config/ contains backend configurations controllers/ contains Web controller classes - models/ contains backstage-specific model classes + models/ contains backend-specific model classes runtime/ contains files generated during runtime views/ contains view files for the Web application www/ contains the entry script and Web resources @@ -79,6 +79,21 @@ php composer.phar create-project --stability=dev yiisoft/yii2-app-advanced yii-a This is not currently available. We will provide it when Yii 2 is formally released. +### Install from development repository + +If you've cloned the [Yii 2 framework main development repository](https://github.com/yiisoft/yii2) you +can bootstrap your application with: + +~~~ +cd yii2/apps/advanced +php composer.phar create-project +~~~ + +*Note: If the above command fails with `[RuntimeException] Not enough arguments.` run +`php composer.phar self-update` to obtain an updated version of composer which supports creating projects +from local packages.* + + GETTING STARTED --------------- @@ -92,7 +107,7 @@ the installed application. You only need to do these once for all. Now you should be able to access: - the frontend using the URL `http://localhost/yii-advanced/frontend/www/` -- the backstage using the URL `http://localhost/yii-advanced/backstage/www/` +- the backend using the URL `http://localhost/yii-advanced/backend/www/` assuming `yii-advanced` is directly under the document root of your Web server. diff --git a/apps/advanced/backstage/assets/.gitkeep b/apps/advanced/backend/assets/.gitkeep similarity index 100% rename from apps/advanced/backstage/assets/.gitkeep rename to apps/advanced/backend/assets/.gitkeep diff --git a/apps/advanced/backstage/config/.gitignore b/apps/advanced/backend/config/.gitignore similarity index 100% rename from apps/advanced/backstage/config/.gitignore rename to apps/advanced/backend/config/.gitignore diff --git a/apps/advanced/backstage/config/assets.php b/apps/advanced/backend/config/assets.php similarity index 100% rename from apps/advanced/backstage/config/assets.php rename to apps/advanced/backend/config/assets.php diff --git a/apps/advanced/backstage/config/main.php b/apps/advanced/backend/config/main.php similarity index 94% rename from apps/advanced/backstage/config/main.php rename to apps/advanced/backend/config/main.php index 6e55c47..3140cd2 100644 --- a/apps/advanced/backstage/config/main.php +++ b/apps/advanced/backend/config/main.php @@ -13,7 +13,7 @@ return array( 'basePath' => dirname(__DIR__), 'vendorPath' => dirname(dirname(__DIR__)) . '/vendor', 'preload' => array('log'), - 'controllerNamespace' => 'backstage\controllers', + 'controllerNamespace' => 'backend\controllers', 'modules' => array( ), 'components' => array( diff --git a/apps/advanced/backstage/config/params.php b/apps/advanced/backend/config/params.php similarity index 100% rename from apps/advanced/backstage/config/params.php rename to apps/advanced/backend/config/params.php diff --git a/apps/advanced/backstage/controllers/SiteController.php b/apps/advanced/backend/controllers/SiteController.php similarity index 94% rename from apps/advanced/backstage/controllers/SiteController.php rename to apps/advanced/backend/controllers/SiteController.php index d40738a..0306c97 100644 --- a/apps/advanced/backstage/controllers/SiteController.php +++ b/apps/advanced/backend/controllers/SiteController.php @@ -1,6 +1,6 @@ rem @link http://www.yiiframework.com/ @@ -15,6 +15,6 @@ set YII_PATH=%~dp0 if "%PHP_COMMAND%" == "" set PHP_COMMAND=php.exe -"%PHP_COMMAND%" "%YII_PATH%install" %* +"%PHP_COMMAND%" "%YII_PATH%init" %* @endlocal diff --git a/apps/basic/README.md b/apps/basic/README.md index 5300448..2f8f1e8 100644 --- a/apps/basic/README.md +++ b/apps/basic/README.md @@ -59,3 +59,18 @@ assuming `yii-basic` is directly under the document root of your Web server. ### Install from an Archive File This is not currently available. We will provide it when Yii 2 is formally released. + + +### Install from development repository + +If you've cloned the [Yii 2 framework main development repository](https://github.com/yiisoft/yii2) you +can bootstrap your application with: + +~~~ +cd yii2/apps/basic +php composer.phar create-project +~~~ + +*Note: If the above command fails with `[RuntimeException] Not enough arguments.` run +`php composer.phar self-update` to obtain an updated version of composer which supports creating projects +from local packages.* diff --git a/apps/basic/composer.json b/apps/basic/composer.json index 29b05d1..b01c56c 100644 --- a/apps/basic/composer.json +++ b/apps/basic/composer.json @@ -19,10 +19,7 @@ "yiisoft/yii2-composer": "dev-master" }, "scripts": { - "post-install-cmd": [ - "yii\\composer\\InstallHandler::setPermissions" - ], - "post-update-cmd": [ + "post-create-project-cmd": [ "yii\\composer\\InstallHandler::setPermissions" ] }, diff --git a/apps/basic/composer.lock b/apps/basic/composer.lock index fe87399..a1cb48d 100644 --- a/apps/basic/composer.lock +++ b/apps/basic/composer.lock @@ -3,7 +3,7 @@ "This file locks the dependencies of your project to a known state", "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file" ], - "hash": "0411dbbd774aa1c89256c77c68023940", + "hash": "91ba258de768b93025f86071f3bb4b84", "packages": [ { "name": "yiisoft/yii2", @@ -11,12 +11,12 @@ "source": { "type": "git", "url": "https://github.com/yiisoft/yii2-framework.git", - "reference": "2d93f20ba6044ac3f1957300c4ae85fd98ecf47d" + "reference": "3ad6334be076a80df3b2ea0b57f38bd0c6901989" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/yiisoft/yii2-framework/zipball/2d93f20ba6044ac3f1957300c4ae85fd98ecf47d", - "reference": "2d93f20ba6044ac3f1957300c4ae85fd98ecf47d", + "url": "https://api.github.com/repos/yiisoft/yii2-framework/zipball/3ad6334be076a80df3b2ea0b57f38bd0c6901989", + "reference": "3ad6334be076a80df3b2ea0b57f38bd0c6901989", "shasum": "" }, "require": { @@ -97,7 +97,7 @@ "framework", "yii" ], - "time": "2013-05-29 02:55:14" + "time": "2013-06-02 19:19:29" }, { "name": "yiisoft/yii2-composer", diff --git a/apps/basic/config/main.php b/apps/basic/config/main.php index 9adfba6..054259e 100644 --- a/apps/basic/config/main.php +++ b/apps/basic/config/main.php @@ -4,7 +4,6 @@ return array( 'id' => 'bootstrap', 'basePath' => dirname(__DIR__), 'preload' => array('log'), - 'controllerNamespace' => 'app\controllers', 'modules' => array( // 'debug' => array( // 'class' => 'yii\debug\Module', diff --git a/apps/basic/controllers/SiteController.php b/apps/basic/controllers/SiteController.php index ff3b8b4..9d1922b 100644 --- a/apps/basic/controllers/SiteController.php +++ b/apps/basic/controllers/SiteController.php @@ -20,6 +20,7 @@ class SiteController extends Controller public function actionIndex() { + Yii::$app->end(0, false); echo $this->render('index'); } diff --git a/extensions/composer/yii/composer/InstallHandler.php b/extensions/composer/yii/composer/InstallHandler.php index 9e36a35..be4037b 100644 --- a/extensions/composer/yii/composer/InstallHandler.php +++ b/extensions/composer/yii/composer/InstallHandler.php @@ -82,7 +82,7 @@ class InstallHandler throw new Exception("Config file does not exist: $configFile"); } - require(__DIR__ . '/../../../yii2/yii/Yii.php'); + require_once(__DIR__ . '/../../../yii2/yii/Yii.php'); $application = new Application(require($configFile)); $request = $application->getRequest(); diff --git a/extensions/smarty/yii/smarty/ViewRenderer.php b/extensions/smarty/yii/smarty/ViewRenderer.php index d8c5d30..164ae8c 100644 --- a/extensions/smarty/yii/smarty/ViewRenderer.php +++ b/extensions/smarty/yii/smarty/ViewRenderer.php @@ -26,12 +26,12 @@ class ViewRenderer extends BaseViewRenderer /** * @var string the directory or path alias pointing to where Smarty cache will be stored. */ - public $cachePath = '@app/runtime/Smarty/cache'; + public $cachePath = '@runtime/Smarty/cache'; /** * @var string the directory or path alias pointing to where Smarty compiled templates will be stored. */ - public $compilePath = '@app/runtime/Smarty/compile'; + public $compilePath = '@runtime/Smarty/compile'; /** * @var Smarty diff --git a/extensions/twig/yii/twig/ViewRenderer.php b/extensions/twig/yii/twig/ViewRenderer.php index 7498d86..76bee95 100644 --- a/extensions/twig/yii/twig/ViewRenderer.php +++ b/extensions/twig/yii/twig/ViewRenderer.php @@ -25,7 +25,7 @@ class ViewRenderer extends BaseViewRenderer /** * @var string the directory or path alias pointing to where Twig cache will be stored. */ - public $cachePath = '@app/runtime/Twig/cache'; + public $cachePath = '@runtime/Twig/cache'; /** * @var array Twig options diff --git a/framework/yii/YiiBase.php b/framework/yii/YiiBase.php index 4fe0e77..ee75822 100644 --- a/framework/yii/YiiBase.php +++ b/framework/yii/YiiBase.php @@ -606,11 +606,36 @@ class YiiBase public static function t($category, $message, $params = array(), $language = null) { if (self::$app !== null) { - return self::$app->getI18N()->translate($category, $message, $params, $language); + return self::$app->getI18N()->translate($category, $message, $params, $language ?: self::$app->language); } else { return is_array($params) ? strtr($message, $params) : $message; } } + + /** + * Configures an object with the initial property values. + * @param object $object the object to be configured + * @param array $properties the property initial values given in terms of name-value pairs. + */ + public static function configure($object, $properties) + { + foreach ($properties as $name => $value) { + $object->$name = $value; + } + } + + /** + * Returns the public member variables of an object. + * This method is provided such that we can get the public member variables of an object. + * It is different from "get_object_vars()" because the latter will return private + * and protected variables if it is called within the object itself. + * @param object $object the object to be handled + * @return array the public member variables of the object + */ + public static function getObjectVars($object) + { + return get_object_vars($object); + } } YiiBase::$aliases = array( diff --git a/framework/yii/base/Application.php b/framework/yii/base/Application.php index d38f6a9..f5f3d6a 100644 --- a/framework/yii/base/Application.php +++ b/framework/yii/base/Application.php @@ -72,25 +72,17 @@ class Application extends Module public function __construct($config = array()) { Yii::$app = $this; - if (!isset($config['id'])) { throw new InvalidConfigException('The "id" configuration is required.'); } - if (isset($config['basePath'])) { $this->setBasePath($config['basePath']); - Yii::setAlias('@app', $this->getBasePath()); unset($config['basePath']); } else { throw new InvalidConfigException('The "basePath" configuration is required.'); } - - if (isset($config['timeZone'])) { - $this->setTimeZone($config['timeZone']); - unset($config['timeZone']); - } elseif (!ini_get('date.timezone')) { - $this->setTimeZone('UTC'); - } + + $this->preInit($config); $this->registerErrorHandlers(); $this->registerCoreComponents(); @@ -99,6 +91,35 @@ class Application extends Module } /** + * Pre-initializes the application. + * This method is called at the beginning of the application constructor. + * @param array $config the application configuration + */ + public function preInit(&$config) + { + if (isset($config['vendorPath'])) { + $this->setVendorPath($config['vendorPath']); + unset($config['vendorPath']); + } else { + // set "@vendor" + $this->getVendorPath(); + } + if (isset($config['runtimePath'])) { + $this->setRuntimePath($config['runtimePath']); + unset($config['runtimePath']); + } else { + // set "@runtime" + $this->getRuntimePath(); + } + if (isset($config['timeZone'])) { + $this->setTimeZone($config['timeZone']); + unset($config['timeZone']); + } elseif (!ini_get('date.timezone')) { + $this->setTimeZone('UTC'); + } + } + + /** * Registers error handlers. */ public function registerErrorHandlers() @@ -107,6 +128,11 @@ class Application extends Module ini_set('display_errors', 0); set_exception_handler(array($this, 'handleException')); set_error_handler(array($this, 'handleError'), error_reporting()); + // 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); + register_shutdown_function(array($this, 'handleFatalError')); } } @@ -121,11 +147,10 @@ class Application extends Module { if (!$this->_ended) { $this->_ended = true; + $this->getResponse()->end(); $this->afterRequest(); } - $this->handleFatalError(); - if ($exit) { exit($status); } @@ -139,11 +164,10 @@ 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); + $response = $this->getResponse(); + $response->begin(); $status = $this->processRequest(); + $response->end(); $this->afterRequest(); return $status; } @@ -178,7 +202,8 @@ class Application extends Module /** * Returns the directory that stores runtime files. - * @return string the directory that stores runtime files. Defaults to 'protected/runtime'. + * @return string the directory that stores runtime files. + * Defaults to the "runtime" subdirectory under [[basePath]]. */ public function getRuntimePath() { @@ -191,16 +216,11 @@ class Application extends Module /** * Sets the directory that stores runtime files. * @param string $path the directory that stores runtime files. - * @throws InvalidConfigException if the directory does not exist or is not writable */ public function setRuntimePath($path) { - $path = Yii::getAlias($path); - if (is_dir($path) && is_writable($path)) { - $this->_runtimePath = $path; - } else { - throw new InvalidConfigException("Runtime path must be a directory writable by the Web server process: $path"); - } + $this->_runtimePath = Yii::getAlias($path); + Yii::setAlias('@runtime', $this->_runtimePath); } private $_vendorPath; @@ -208,7 +228,7 @@ class Application extends Module /** * Returns the directory that stores vendor files. * @return string the directory that stores vendor files. - * Defaults to 'vendor' directory under applications [[basePath]]. + * Defaults to "vendor" directory under [[basePath]]. */ public function getVendorPath() { @@ -225,6 +245,7 @@ class Application extends Module public function setVendorPath($path) { $this->_vendorPath = Yii::getAlias($path); + Yii::setAlias('@vendor', $this->_vendorPath); } /** @@ -297,6 +318,15 @@ class Application extends Module } /** + * Returns the response component. + * @return \yii\web\Response|\yii\console\Response the response component + */ + public function getResponse() + { + return $this->getComponent('response'); + } + + /** * Returns the view object. * @return View the view object that is used to render various view files. */ diff --git a/framework/yii/base/ErrorHandler.php b/framework/yii/base/ErrorHandler.php index 8dc3fce..4e3e92a 100644 --- a/framework/yii/base/ErrorHandler.php +++ b/framework/yii/base/ErrorHandler.php @@ -82,11 +82,12 @@ class ErrorHandler extends Component } elseif (!(Yii::$app instanceof \yii\web\Application)) { Yii::$app->renderException($exception); } else { + $response = Yii::$app->getResponse(); if (!headers_sent()) { if ($exception instanceof HttpException) { - header('HTTP/1.0 ' . $exception->statusCode . ' ' . $exception->getName()); + $response->setStatusCode($exception->statusCode); } else { - header('HTTP/1.0 500 ' . get_class($exception)); + $response->setStatusCode(500); } } if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest') { @@ -100,13 +101,13 @@ class ErrorHandler extends Component $view = new View(); $request = ''; - foreach (array('GET', 'POST', 'SERVER', 'FILES', 'COOKIE', 'SESSION', 'ENV') as $name) { - if (!empty($GLOBALS['_' . $name])) { - $request .= '$_' . $name . ' = ' . var_export($GLOBALS['_' . $name], true) . ";\n\n"; + foreach (array('_GET', '_POST', '_SERVER', '_FILES', '_COOKIE', '_SESSION', '_ENV') as $name) { + if (!empty($GLOBALS[$name])) { + $request .= '$' . $name . ' = ' . var_export($GLOBALS[$name], true) . ";\n\n"; } } $request = rtrim($request, "\n\n"); - echo $view->renderFile($this->mainView, array( + $response->content = $view->renderFile($this->mainView, array( 'exception' => $exception, 'request' => $request, ), $this); @@ -255,7 +256,7 @@ class ErrorHandler extends Component if (isset($_SERVER['SERVER_SOFTWARE'])) { foreach ($serverUrls as $url => $keywords) { foreach ($keywords as $keyword) { - if (stripos($_SERVER['SERVER_SOFTWARE'], $keyword) !== false ) { + if (stripos($_SERVER['SERVER_SOFTWARE'], $keyword) !== false) { return '' . $this->htmlEncode($_SERVER['SERVER_SOFTWARE']) . ''; } } diff --git a/framework/yii/base/Formatter.php b/framework/yii/base/Formatter.php index d15e5f2..545f570 100644 --- a/framework/yii/base/Formatter.php +++ b/framework/yii/base/Formatter.php @@ -12,7 +12,6 @@ use DateTime; use yii\helpers\HtmlPurifier; use yii\helpers\Html; - /** * Formatter provides a set of commonly used data formatting methods. * @@ -39,17 +38,19 @@ class Formatter extends Component public $datetimeFormat = 'Y/m/d h:i:s A'; /** * @var array the text to be displayed when formatting a boolean value. The first element corresponds - * to the text display for false, the second element for true. Defaults to array('No', 'Yes'). + * to the text display for false, the second element for true. Defaults to `array('No', 'Yes')`. */ public $booleanFormat; /** * @var string the character displayed as the decimal point when formatting a number. + * If not set, "." will be used. */ - public $decimalSeparator = '.'; + public $decimalSeparator; /** * @var string the character displayed as the thousands separator character when formatting a number. + * If not set, "," will be used. */ - public $thousandSeparator = ','; + public $thousandSeparator; /** @@ -273,7 +274,11 @@ class Formatter extends Component */ public function asDouble($value, $decimals = 2) { - return str_replace('.', $this->decimalSeparator, sprintf("%.{$decimals}f", $value)); + if ($this->decimalSeparator === null) { + return sprintf("%.{$decimals}f", $value); + } else { + return str_replace('.', $this->decimalSeparator, sprintf("%.{$decimals}f", $value)); + } } /** @@ -287,6 +292,8 @@ class Formatter extends Component */ public function asNumber($value, $decimals = 0) { - return number_format($value, $decimals, $this->decimalSeparator, $this->thousandSeparator); + $ds = isset($this->decimalSeparator) ? $this->decimalSeparator: '.'; + $ts = isset($this->thousandSeparator) ? $this->thousandSeparator: ','; + return number_format($value, $decimals, $ds, $ts); } } diff --git a/framework/yii/base/HttpException.php b/framework/yii/base/HttpException.php index 4d63764..cce0bb0 100644 --- a/framework/yii/base/HttpException.php +++ b/framework/yii/base/HttpException.php @@ -7,6 +7,7 @@ namespace yii\base; + /** * HttpException represents an exception caused by an improper request of the end-user. * @@ -42,66 +43,8 @@ class HttpException extends UserException */ 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]; + if (isset(\yii\web\Response::$statusTexts[$this->statusCode])) { + return \yii\web\Response::$statusTexts[$this->statusCode]; } else { return 'Error'; } diff --git a/framework/yii/base/InvalidCallException.php b/framework/yii/base/InvalidCallException.php index 9a146d4..73cb4b9 100644 --- a/framework/yii/base/InvalidCallException.php +++ b/framework/yii/base/InvalidCallException.php @@ -23,4 +23,3 @@ class InvalidCallException extends Exception return \Yii::t('yii', 'Invalid Call'); } } - diff --git a/framework/yii/base/InvalidConfigException.php b/framework/yii/base/InvalidConfigException.php index c617381..0a6b4c5 100644 --- a/framework/yii/base/InvalidConfigException.php +++ b/framework/yii/base/InvalidConfigException.php @@ -23,4 +23,3 @@ class InvalidConfigException extends Exception return \Yii::t('yii', 'Invalid Configuration'); } } - diff --git a/framework/yii/base/InvalidParamException.php b/framework/yii/base/InvalidParamException.php index 0262051..44430a8 100644 --- a/framework/yii/base/InvalidParamException.php +++ b/framework/yii/base/InvalidParamException.php @@ -23,4 +23,3 @@ class InvalidParamException extends Exception return \Yii::t('yii', 'Invalid Parameter'); } } - diff --git a/framework/yii/base/InvalidRouteException.php b/framework/yii/base/InvalidRouteException.php index a573636..fbcc087 100644 --- a/framework/yii/base/InvalidRouteException.php +++ b/framework/yii/base/InvalidRouteException.php @@ -23,4 +23,3 @@ class InvalidRouteException extends UserException return \Yii::t('yii', 'Invalid Route'); } } - diff --git a/framework/yii/base/Jsonable.php b/framework/yii/base/Jsonable.php new file mode 100644 index 0000000..e9425a6 --- /dev/null +++ b/framework/yii/base/Jsonable.php @@ -0,0 +1,22 @@ + + * @since 2.0 + */ +interface Jsonable +{ + /** + * @return string the JSON representation of this object + */ + public function toJson(); +} diff --git a/framework/yii/base/Model.php b/framework/yii/base/Model.php index c7432f5..b9b7d2d 100644 --- a/framework/yii/base/Model.php +++ b/framework/yii/base/Model.php @@ -10,6 +10,7 @@ namespace yii\base; use ArrayObject; use ArrayIterator; use yii\helpers\Inflector; +use yii\helpers\Json; use yii\validators\RequiredValidator; use yii\validators\Validator; @@ -41,7 +42,7 @@ use yii\validators\Validator; * @author Qiang Xue * @since 2.0 */ -class Model extends Component implements \IteratorAggregate, \ArrayAccess +class Model extends Component implements \IteratorAggregate, \ArrayAccess, Jsonable { /** * @event ModelEvent an event raised at the beginning of [[validate()]]. You may set @@ -638,6 +639,16 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess } /** + * Returns the JSON representation of this object. + * The default implementation will return [[attributes]]. + * @return string the JSON representation of this object. + */ + public function toJson() + { + return Json::encode($this->getAttributes()); + } + + /** * Returns an iterator for traversing the attributes in the model. * This method is required by the interface IteratorAggregate. * @return ArrayIterator an iterator for traversing the items in the list. diff --git a/framework/yii/base/Module.php b/framework/yii/base/Module.php index ec3001e..cc7c849 100644 --- a/framework/yii/base/Module.php +++ b/framework/yii/base/Module.php @@ -1,650 +1,667 @@ - 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 -{ - /** - * @event ActionEvent an event raised before executing a controller action. - * You may set [[ActionEvent::isValid]] to be false to cancel the action execution. - */ - const EVENT_BEFORE_ACTION = 'beforeAction'; - /** - * @event ActionEvent an event raised after executing a controller action. - */ - const EVENT_AFTER_ACTION = 'afterAction'; - /** - * @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() - { - $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 InvalidParamException if the directory does not exist. - */ - public function setBasePath($path) - { - $path = Yii::getAlias($path); - $p = realpath($path); - if ($p !== false && is_dir($p)) { - $this->_basePath = $p; - } else { - throw new InvalidParamException("The directory does not exist: $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 = Yii::getAlias($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 = Yii::getAlias($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 = Yii::getAlias($path); - } - - /** - * Defines path aliases. - * This method calls [[Yii::setAlias()]] to register the path aliases. - * This method is provided so that you can define path aliases when configuring a module. - * @param array $aliases list of path aliases to be defined. The array keys are alias names - * (must start with '@') and the array values are the corresponding paths or aliases. - * For example, - * - * ~~~ - * array( - * '@models' => '@app/models', // an existing alias - * '@backend' => __DIR__ . '/../backend', // a directory - * ) - * ~~~ - */ - public function setAliases($aliases) - { - foreach ($aliases as $name => $alias) { - Yii::setAlias($name, $alias); - } - } - - /** - * Checks whether the named module exists. - * @param string $id module ID - * @return boolean whether the named module exists. Both loaded and unloaded modules - * are considered. - */ - public function hasModule($id) - { - return isset($this->_modules[$id]); - } - - /** - * Retrieves the named module. - * @param string $id module ID (case-sensitive) - * @param boolean $load whether to load the module if it is not yet loaded. - * @return Module|null the module instance, null if the module - * does not exist. - * @see hasModule() - */ - public function getModule($id, $load = true) - { - if (isset($this->_modules[$id])) { - if ($this->_modules[$id] instanceof Module) { - return $this->_modules[$id]; - } elseif ($load) { - Yii::trace("Loading module: $id", __METHOD__); - return $this->_modules[$id] = Yii::createObject($this->_modules[$id], $id, $this); - } - } - return null; - } - - /** - * Adds a sub-module to this module. - * @param string $id module ID - * @param Module|array|null $module the sub-module to be added to this module. This can - * be one of the followings: - * - * - a [[Module]] object - * - a configuration array: when [[getModule()]] is called initially, the array - * will be used to instantiate the sub-module - * - null: the named sub-module will be removed from this module - */ - public function setModule($id, $module) - { - if ($module === null) { - unset($this->_modules[$id]); - } else { - $this->_modules[$id] = $module; - } - } - - /** - * Returns the sub-modules in this module. - * @param boolean $loadedOnly whether to return the loaded sub-modules only. If this is set false, - * then all sub-modules registered in this module will be returned, whether they are loaded or not. - * Loaded modules will be returned as objects, while unloaded modules as configuration arrays. - * @return array the modules (indexed by their IDs) - */ - public function getModules($loadedOnly = false) - { - if ($loadedOnly) { - $modules = array(); - foreach ($this->_modules as $module) { - if ($module instanceof Module) { - $modules[] = $module; - } - } - return $modules; - } else { - return $this->_modules; - } - } - - /** - * Registers sub-modules in the current module. - * - * Each sub-module should be specified as a name-value pair, where - * name refers to the ID of the module and value the module or a configuration - * array that can be used to create the module. In the latter case, [[Yii::createObject()]] - * will be used to create the module. - * - * If a new sub-module has the same ID as an existing one, the existing one will be overwritten silently. - * - * The following is an example for registering two sub-modules: - * - * ~~~ - * array( - * 'comment' => array( - * 'class' => 'app\modules\CommentModule', - * 'db' => 'db', - * ), - * 'booking' => array( - * 'class' => 'app\modules\BookingModule', - * ), - * ) - * ~~~ - * - * @param array $modules modules (id => module configuration or instances) - */ - public function setModules($modules) - { - foreach ($modules as $id => $module) { - $this->_modules[$id] = $module; - } - } - - /** - * Checks whether the named component exists. - * @param string $id component ID - * @return boolean whether the named component exists. Both loaded and unloaded components - * are considered. - */ - public function hasComponent($id) - { - return isset($this->_components[$id]); - } - - /** - * Retrieves the named component. - * @param string $id component ID (case-sensitive) - * @param boolean $load whether to load the component if it is not yet loaded. - * @return Component|null the component instance, null if the component does not exist. - * @see hasComponent() - */ - public function getComponent($id, $load = true) - { - if (isset($this->_components[$id])) { - if ($this->_components[$id] instanceof Object) { - return $this->_components[$id]; - } elseif ($load) { - Yii::trace("Loading component: $id", __METHOD__); - return $this->_components[$id] = Yii::createObject($this->_components[$id]); - } - } - return null; - } - - /** - * Registers a component with this module. - * @param string $id component ID - * @param Component|array|null $component the component to be registered with the module. This can - * be one of the followings: - * - * - a [[Component]] object - * - a configuration array: when [[getComponent()]] is called initially for this component, the array - * will be used to instantiate the component via [[Yii::createObject()]]. - * - null: the named component will be removed from the module - */ - public function setComponent($id, $component) - { - if ($component === null) { - unset($this->_components[$id]); - } else { - $this->_components[$id] = $component; - } - } - - /** - * Returns the registered components. - * @param boolean $loadedOnly whether to return the loaded components only. If this is set false, - * then all components specified in the configuration will be returned, whether they are loaded or not. - * Loaded components will be returned as objects, while unloaded components as configuration arrays. - * @return array the components (indexed by their IDs) - */ - public function getComponents($loadedOnly = false) - { - if ($loadedOnly) { - $components = array(); - foreach ($this->_components as $component) { - if ($component instanceof Component) { - $components[] = $component; - } - } - return $components; - } else { - return $this->_components; - } - } - - /** - * Registers a set of components in this module. - * - * Each component should be specified as a name-value pair, where - * name refers to the ID of the component and value the component or a configuration - * array that can be used to create the component. In the latter case, [[Yii::createObject()]] - * will be used to create the component. - * - * If a new component has the same ID as an existing one, the existing one will be overwritten silently. - * - * The following is an example for setting two components: - * - * ~~~ - * array( - * 'db' => array( - * 'class' => 'yii\db\Connection', - * 'dsn' => 'sqlite:path/to/file.db', - * ), - * 'cache' => array( - * 'class' => 'yii\caching\DbCache', - * 'db' => 'db', - * ), - * ) - * ~~~ - * - * @param array $components components (id => component configuration or instance) - */ - public function setComponents($components) - { - foreach ($components as $id => $component) { - if (isset($this->_components[$id]['class']) && !isset($component['class'])) { - $component['class'] = $this->_components[$id]['class']; - } - $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 requested action ID. Otherwise false will be returned. - * @throws InvalidConfigException if the controller class and its file do not match. - */ - public function createController($route) - { - if ($route === '') { - $route = $this->defaultRoute; - } - $route = trim($route, '/'); - 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 = str_replace(' ', '', ucwords(implode(' ', explode('-', $id)))) . 'Controller'; - $classFile = $this->controllerPath . DIRECTORY_SEPARATOR . $className . '.php'; - if (!is_file($classFile)) { - return false; - } - $className = ltrim($this->controllerNamespace . '\\' . $className, '\\'); - Yii::$classMap[$className] = $classFile; - if (is_subclass_of($className, 'yii\base\Controller')) { - $controller = new $className($id, $this); - } elseif (YII_DEBUG) { - throw new InvalidConfigException("Controller class must extend from \\yii\\base\\Controller."); - } - } - - return isset($controller) ? array($controller, $route) : false; - } - - /** - * 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) - { - $event = new ActionEvent($action); - $this->trigger(self::EVENT_BEFORE_ACTION, $event); - return $event->isValid; - } - - /** - * 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->trigger(self::EVENT_AFTER_ACTION, new ActionEvent($action)); - } -} + 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 +{ + /** + * @event ActionEvent an event raised before executing a controller action. + * You may set [[ActionEvent::isValid]] to be false to cancel the action execution. + */ + const EVENT_BEFORE_ACTION = 'beforeAction'; + /** + * @event ActionEvent an event raised after executing a controller action. + */ + const EVENT_AFTER_ACTION = 'afterAction'; + /** + * @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. If not set, + * it will use the "controllers" sub-namespace under the namespace of this module. + * For example, if the namespace of this module is "foo\bar", then the default + * controller namespace would be "foo\bar\controllers". + * If the module is an application, it will default to "app\controllers". + */ + 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() + { + $this->preloadComponents(); + if ($this->controllerNamespace === null) { + if ($this instanceof Application) { + $this->controllerNamespace = 'app\\controllers'; + } else { + $class = get_class($this); + if (($pos = strrpos($class, '\\')) !== false) { + $this->controllerNamespace = substr($class, 0, $pos) . '\\controllers'; + } + } + } + } + + /** + * 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 InvalidParamException if the directory does not exist. + */ + public function setBasePath($path) + { + $path = Yii::getAlias($path); + $p = realpath($path); + if ($p !== false && is_dir($p)) { + $this->_basePath = $p; + if ($this instanceof Application) { + Yii::setAlias('@app', $p); + } + } else { + throw new InvalidParamException("The directory does not exist: $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 = Yii::getAlias($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 = Yii::getAlias($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 = Yii::getAlias($path); + } + + /** + * Defines path aliases. + * This method calls [[Yii::setAlias()]] to register the path aliases. + * This method is provided so that you can define path aliases when configuring a module. + * @param array $aliases list of path aliases to be defined. The array keys are alias names + * (must start with '@') and the array values are the corresponding paths or aliases. + * For example, + * + * ~~~ + * array( + * '@models' => '@app/models', // an existing alias + * '@backend' => __DIR__ . '/../backend', // a directory + * ) + * ~~~ + */ + public function setAliases($aliases) + { + foreach ($aliases as $name => $alias) { + Yii::setAlias($name, $alias); + } + } + + /** + * Checks whether the named module exists. + * @param string $id module ID + * @return boolean whether the named module exists. Both loaded and unloaded modules + * are considered. + */ + public function hasModule($id) + { + return isset($this->_modules[$id]); + } + + /** + * Retrieves the named module. + * @param string $id module ID (case-sensitive) + * @param boolean $load whether to load the module if it is not yet loaded. + * @return Module|null the module instance, null if the module + * does not exist. + * @see hasModule() + */ + public function getModule($id, $load = true) + { + if (isset($this->_modules[$id])) { + if ($this->_modules[$id] instanceof Module) { + return $this->_modules[$id]; + } elseif ($load) { + Yii::trace("Loading module: $id", __METHOD__); + return $this->_modules[$id] = Yii::createObject($this->_modules[$id], $id, $this); + } + } + return null; + } + + /** + * Adds a sub-module to this module. + * @param string $id module ID + * @param Module|array|null $module the sub-module to be added to this module. This can + * be one of the followings: + * + * - a [[Module]] object + * - a configuration array: when [[getModule()]] is called initially, the array + * will be used to instantiate the sub-module + * - null: the named sub-module will be removed from this module + */ + public function setModule($id, $module) + { + if ($module === null) { + unset($this->_modules[$id]); + } else { + $this->_modules[$id] = $module; + } + } + + /** + * Returns the sub-modules in this module. + * @param boolean $loadedOnly whether to return the loaded sub-modules only. If this is set false, + * then all sub-modules registered in this module will be returned, whether they are loaded or not. + * Loaded modules will be returned as objects, while unloaded modules as configuration arrays. + * @return array the modules (indexed by their IDs) + */ + public function getModules($loadedOnly = false) + { + if ($loadedOnly) { + $modules = array(); + foreach ($this->_modules as $module) { + if ($module instanceof Module) { + $modules[] = $module; + } + } + return $modules; + } else { + return $this->_modules; + } + } + + /** + * Registers sub-modules in the current module. + * + * Each sub-module should be specified as a name-value pair, where + * name refers to the ID of the module and value the module or a configuration + * array that can be used to create the module. In the latter case, [[Yii::createObject()]] + * will be used to create the module. + * + * If a new sub-module has the same ID as an existing one, the existing one will be overwritten silently. + * + * The following is an example for registering two sub-modules: + * + * ~~~ + * array( + * 'comment' => array( + * 'class' => 'app\modules\comment\CommentModule', + * 'db' => 'db', + * ), + * 'booking' => array( + * 'class' => 'app\modules\booking\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 Object) { + return $this->_components[$id]; + } elseif ($load) { + Yii::trace("Loading component: $id", __METHOD__); + return $this->_components[$id] = Yii::createObject($this->_components[$id]); + } + } + return null; + } + + /** + * Registers a component with this module. + * @param string $id component ID + * @param Component|array|null $component the component to be registered with the module. This can + * be one of the followings: + * + * - a [[Component]] object + * - a configuration array: when [[getComponent()]] is called initially for this component, the array + * will be used to instantiate the component via [[Yii::createObject()]]. + * - null: the named component will be removed from the module + */ + public function setComponent($id, $component) + { + if ($component === null) { + unset($this->_components[$id]); + } else { + $this->_components[$id] = $component; + } + } + + /** + * Returns the registered components. + * @param boolean $loadedOnly whether to return the loaded components only. If this is set false, + * then all components specified in the configuration will be returned, whether they are loaded or not. + * Loaded components will be returned as objects, while unloaded components as configuration arrays. + * @return array the components (indexed by their IDs) + */ + public function getComponents($loadedOnly = false) + { + if ($loadedOnly) { + $components = array(); + foreach ($this->_components as $component) { + if ($component instanceof Component) { + $components[] = $component; + } + } + return $components; + } else { + return $this->_components; + } + } + + /** + * Registers a set of components in this module. + * + * Each component should be specified as a name-value pair, where + * name refers to the ID of the component and value the component or a configuration + * array that can be used to create the component. In the latter case, [[Yii::createObject()]] + * will be used to create the component. + * + * If a new component has the same ID as an existing one, the existing one will be overwritten silently. + * + * The following is an example for setting two components: + * + * ~~~ + * array( + * 'db' => array( + * 'class' => 'yii\db\Connection', + * 'dsn' => 'sqlite:path/to/file.db', + * ), + * 'cache' => array( + * 'class' => 'yii\caching\DbCache', + * 'db' => 'db', + * ), + * ) + * ~~~ + * + * @param array $components components (id => component configuration or instance) + */ + public function setComponents($components) + { + foreach ($components as $id => $component) { + if (isset($this->_components[$id]['class']) && !isset($component['class'])) { + $component['class'] = $this->_components[$id]['class']; + } + $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 requested action ID. Otherwise false will be returned. + * @throws InvalidConfigException if the controller class and its file do not match. + */ + public function createController($route) + { + if ($route === '') { + $route = $this->defaultRoute; + } + $route = trim($route, '/'); + 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 = str_replace(' ', '', ucwords(implode(' ', explode('-', $id)))) . 'Controller'; + $classFile = $this->controllerPath . DIRECTORY_SEPARATOR . $className . '.php'; + if (!is_file($classFile)) { + return false; + } + $className = ltrim($this->controllerNamespace . '\\' . $className, '\\'); + Yii::$classMap[$className] = $classFile; + if (is_subclass_of($className, 'yii\base\Controller')) { + $controller = new $className($id, $this); + } elseif (YII_DEBUG) { + throw new InvalidConfigException("Controller class must extend from \\yii\\base\\Controller."); + } + } + + return isset($controller) ? array($controller, $route) : false; + } + + /** + * 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) + { + $event = new ActionEvent($action); + $this->trigger(self::EVENT_BEFORE_ACTION, $event); + return $event->isValid; + } + + /** + * 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->trigger(self::EVENT_AFTER_ACTION, new ActionEvent($action)); + } +} diff --git a/framework/yii/base/NotSupportedException.php b/framework/yii/base/NotSupportedException.php index 8a93e14..33f936f 100644 --- a/framework/yii/base/NotSupportedException.php +++ b/framework/yii/base/NotSupportedException.php @@ -23,4 +23,3 @@ class NotSupportedException extends Exception return \Yii::t('yii', 'Not Supported'); } } - diff --git a/framework/yii/base/Object.php b/framework/yii/base/Object.php index a90a231..79c1f7b 100644 --- a/framework/yii/base/Object.php +++ b/framework/yii/base/Object.php @@ -7,12 +7,15 @@ namespace yii\base; +use Yii; +use yii\helpers\Json; + /** * @include @yii/base/Object.md * @author Qiang Xue * @since 2.0 */ -class Object +class Object implements Jsonable { /** * @return string the fully qualified name of this class. @@ -38,8 +41,8 @@ class Object */ public function __construct($config = array()) { - foreach ($config as $name => $value) { - $this->$name = $value; + if (!empty($config)) { + Yii::configure($this, $config); } $this->init(); } @@ -216,4 +219,14 @@ class Object { return method_exists($this, 'set' . $name) || $checkVar && property_exists($this, $name); } + + /** + * Returns the JSON representation of this object. + * The default implementation will return all public member variables. + * @return string the JSON representation of this object. + */ + public function toJson() + { + return Json::encode(Yii::getObjectVars($this)); + } } diff --git a/framework/yii/base/Response.php b/framework/yii/base/Response.php index 396b073..b89b537 100644 --- a/framework/yii/base/Response.php +++ b/framework/yii/base/Response.php @@ -13,6 +13,9 @@ namespace yii\base; */ class Response extends Component { + const EVENT_BEGIN_RESPONSE = 'beginResponse'; + const EVENT_END_RESPONSE = 'endResponse'; + /** * Starts output buffering */ @@ -56,4 +59,14 @@ class Response extends Component ob_end_clean(); } } + + public function begin() + { + $this->trigger(self::EVENT_BEGIN_RESPONSE); + } + + public function end() + { + $this->trigger(self::EVENT_END_RESPONSE); + } } diff --git a/framework/yii/base/Theme.php b/framework/yii/base/Theme.php index ca1efcd..91c32dc 100644 --- a/framework/yii/base/Theme.php +++ b/framework/yii/base/Theme.php @@ -73,7 +73,7 @@ class Theme extends Component */ public function init() { - parent::init(); + parent::init(); if (empty($this->pathMap)) { if ($this->basePath !== null) { $this->basePath = Yii::getAlias($this->basePath); diff --git a/framework/yii/base/UnknownClassException.php b/framework/yii/base/UnknownClassException.php index e4a682a..7b893d4 100644 --- a/framework/yii/base/UnknownClassException.php +++ b/framework/yii/base/UnknownClassException.php @@ -23,4 +23,3 @@ class UnknownClassException extends Exception return \Yii::t('yii', 'Unknown Class'); } } - diff --git a/framework/yii/base/UnknownMethodException.php b/framework/yii/base/UnknownMethodException.php index d8cea34..3b33659 100644 --- a/framework/yii/base/UnknownMethodException.php +++ b/framework/yii/base/UnknownMethodException.php @@ -23,4 +23,3 @@ class UnknownMethodException extends Exception return \Yii::t('yii', 'Unknown Method'); } } - diff --git a/framework/yii/base/UnknownPropertyException.php b/framework/yii/base/UnknownPropertyException.php index b8e93c5..682fdfa 100644 --- a/framework/yii/base/UnknownPropertyException.php +++ b/framework/yii/base/UnknownPropertyException.php @@ -23,4 +23,3 @@ class UnknownPropertyException extends Exception return \Yii::t('yii', 'Unknown Property'); } } - diff --git a/framework/yii/bootstrap/Button.php b/framework/yii/bootstrap/Button.php index 104e700..856c420 100644 --- a/framework/yii/bootstrap/Button.php +++ b/framework/yii/bootstrap/Button.php @@ -6,9 +6,8 @@ */ namespace yii\bootstrap; -use yii\base\InvalidConfigException; -use yii\helpers\Html; +use yii\helpers\Html; /** * Button renders a bootstrap button. diff --git a/framework/yii/bootstrap/ButtonDropdown.php b/framework/yii/bootstrap/ButtonDropdown.php index 4168bd5..fec042e 100644 --- a/framework/yii/bootstrap/ButtonDropdown.php +++ b/framework/yii/bootstrap/ButtonDropdown.php @@ -6,8 +6,8 @@ */ namespace yii\bootstrap; -use yii\helpers\Html; +use yii\helpers\Html; /** * ButtonDropdown renders a group or split button dropdown bootstrap component. diff --git a/framework/yii/bootstrap/ButtonGroup.php b/framework/yii/bootstrap/ButtonGroup.php index f50d6a8..e5bf4e9 100644 --- a/framework/yii/bootstrap/ButtonGroup.php +++ b/framework/yii/bootstrap/ButtonGroup.php @@ -10,7 +10,6 @@ namespace yii\bootstrap; use yii\helpers\base\ArrayHelper; use yii\helpers\Html; - /** * ButtonGroup renders a button group bootstrap component. * diff --git a/framework/yii/bootstrap/Collapse.php b/framework/yii/bootstrap/Collapse.php index a7929e3..fdcaae1 100644 --- a/framework/yii/bootstrap/Collapse.php +++ b/framework/yii/bootstrap/Collapse.php @@ -130,4 +130,4 @@ class Collapse extends Widget return implode("\n", $group); } -} \ No newline at end of file +} diff --git a/framework/yii/bootstrap/Dropdown.php b/framework/yii/bootstrap/Dropdown.php index 2bee0ff..827e6cc 100644 --- a/framework/yii/bootstrap/Dropdown.php +++ b/framework/yii/bootstrap/Dropdown.php @@ -11,7 +11,6 @@ use yii\base\InvalidConfigException; use yii\helpers\ArrayHelper; use yii\helpers\Html; - /** * Dropdown renders a Bootstrap dropdown menu component. * diff --git a/framework/yii/bootstrap/Nav.php b/framework/yii/bootstrap/Nav.php index 548fe19..8069699 100644 --- a/framework/yii/bootstrap/Nav.php +++ b/framework/yii/bootstrap/Nav.php @@ -29,11 +29,17 @@ use yii\helpers\Html; * 'label' => 'Dropdown', * 'items' => array( * array( - * 'label' => 'DropdownA', + * 'label' => 'Level 1 -DropdownA', * 'url' => '#', + * 'items' => array( + * array( + * 'label' => 'Level 2 -DropdownA', + * 'url' => '#', + * ), + * ), * ), * array( - * 'label' => 'DropdownB', + * 'label' => 'Level 1 -DropdownB', * 'url' => '#', * ), * ), @@ -114,25 +120,27 @@ class Nav extends Widget } $label = $this->encodeLabels ? Html::encode($item['label']) : $item['label']; $options = ArrayHelper::getValue($item, 'options', array()); - $dropdown = ArrayHelper::getValue($item, 'dropdown'); + $items = ArrayHelper::getValue($item, 'items'); $url = Html::url(ArrayHelper::getValue($item, 'url', '#')); $linkOptions = ArrayHelper::getValue($item, 'linkOptions', array()); - if(ArrayHelper::getValue($item, 'active')) { + if (ArrayHelper::getValue($item, 'active')) { $this->addCssClass($options, 'active'); } - if ($dropdown !== null) { + if ($items !== null) { $linkOptions['data-toggle'] = 'dropdown'; $this->addCssClass($options, 'dropdown'); $this->addCssClass($urlOptions, 'dropdown-toggle'); $label .= ' ' . Html::tag('b', '', array('class' => 'caret')); - if (is_array($dropdown)) { - $dropdown['clientOptions'] = false; - $dropdown = Dropdown::widget($dropdown); + if (is_array($items)) { + $items = Dropdown::widget(array( + 'items' => $items, + 'clientOptions' => false, + )); } } - return Html::tag('li', Html::a($label, $url, $linkOptions) . $dropdown, $options); + return Html::tag('li', Html::a($label, $url, $linkOptions) . $items, $options); } } diff --git a/framework/yii/bootstrap/Progress.php b/framework/yii/bootstrap/Progress.php index 708c0fe..7c0473e 100644 --- a/framework/yii/bootstrap/Progress.php +++ b/framework/yii/bootstrap/Progress.php @@ -11,7 +11,6 @@ use yii\base\InvalidConfigException; use yii\helpers\ArrayHelper; use yii\helpers\Html; - /** * Progress renders a bootstrap progress bar component. * diff --git a/framework/yii/bootstrap/Widget.php b/framework/yii/bootstrap/Widget.php index a2e6d77..48b0331 100644 --- a/framework/yii/bootstrap/Widget.php +++ b/framework/yii/bootstrap/Widget.php @@ -11,7 +11,6 @@ use Yii; use yii\base\View; use yii\helpers\Json; - /** * \yii\bootstrap\Widget is the base class for all bootstrap widgets. * diff --git a/framework/yii/caching/DbCache.php b/framework/yii/caching/DbCache.php index 7e5f12d..7571a42 100644 --- a/framework/yii/caching/DbCache.php +++ b/framework/yii/caching/DbCache.php @@ -170,7 +170,7 @@ class DbCache extends Cache } else { return $this->addValue($key, $value, $expire); } - } + } /** * Stores a value identified by a key into cache if the cache does not contain this key. diff --git a/framework/yii/caching/FileCache.php b/framework/yii/caching/FileCache.php index 0c6d119..a15751e 100644 --- a/framework/yii/caching/FileCache.php +++ b/framework/yii/caching/FileCache.php @@ -25,8 +25,9 @@ class FileCache extends Cache { /** * @var string the directory to store cache files. You may use path alias here. + * If not set, it will use the "cache" subdirectory under the application runtime path. */ - public $cachePath = '@app/runtime/cache'; + public $cachePath = '@runtime/cache'; /** * @var string cache file suffix. Defaults to '.bin'. */ diff --git a/framework/yii/caching/XCache.php b/framework/yii/caching/XCache.php index 91f483b..1f12f23 100644 --- a/framework/yii/caching/XCache.php +++ b/framework/yii/caching/XCache.php @@ -86,4 +86,3 @@ class XCache extends Cache return true; } } - diff --git a/framework/yii/console/Exception.php b/framework/yii/console/Exception.php index 9e9003e..f272bde 100644 --- a/framework/yii/console/Exception.php +++ b/framework/yii/console/Exception.php @@ -25,4 +25,3 @@ class Exception extends UserException return \Yii::t('yii', 'Error'); } } - diff --git a/framework/yii/console/Response.php b/framework/yii/console/Response.php new file mode 100644 index 0000000..34f105d --- /dev/null +++ b/framework/yii/console/Response.php @@ -0,0 +1,17 @@ + + * @since 2.0 + */ +class Response extends \yii\base\Response +{ + +} diff --git a/framework/yii/console/controllers/AssetController.php b/framework/yii/console/controllers/AssetController.php index 8e3de29..dcd1667 100644 --- a/framework/yii/console/controllers/AssetController.php +++ b/framework/yii/console/controllers/AssetController.php @@ -14,6 +14,20 @@ use yii\console\Controller; /** * This command allows you to combine and compress your JavaScript and CSS files. * + * Usage: + * 1. Create a configuration file using 'template' action: + * yii asset/template /path/to/myapp/config.php + * 2. Edit the created config file, adjusting it for your web application needs. + * 3. Run the 'compress' action, using created config: + * yii asset /path/to/myapp/config.php /path/to/myapp/config/assets_compressed.php + * 4. Adjust your web application config to use compressed assets. + * + * Note: in the console environment some path aliases like '@wwwroot' and '@www' may not exist, + * so corresponding paths inside the configuration should be specified directly. + * + * Note: by default this command relies on an external tools to perform actual files compression, + * check [[jsCompressor]] and [[cssCompressor]] for more details. + * * @property array|\yii\web\AssetManager $assetManager asset manager, which will be used for assets processing. * * @author Qiang Xue @@ -43,7 +57,7 @@ class AssetController extends Controller * ~~~ * 'all' => array( * 'css' => 'all.css', - * 'js' => 'js.css', + * 'js' => 'all.js', * 'depends' => array( ... ), * ) * ~~~ @@ -57,7 +71,7 @@ class AssetController extends Controller */ private $_assetManager = array(); /** - * @var string|callback Java Script file compressor. + * @var string|callback JavaScript file compressor. * If a string, it is treated as shell command template, which should contain * placeholders {from} - source file name - and {to} - output file name. * Otherwise, it is treated as PHP callback, which should perform the compression. @@ -150,7 +164,6 @@ class AssetController extends Controller protected function loadConfiguration($configFile) { echo "Loading configuration from '{$configFile}'...\n"; - foreach (require($configFile) as $name => $value) { if (property_exists($this, $name) || $this->canSetProperty($name)) { $this->$name = $value; @@ -159,7 +172,7 @@ class AssetController extends Controller } } - $this->getAssetManager(); // check asset manager configuration + $this->getAssetManager(); // check if asset manager configuration is correct } /** @@ -207,7 +220,8 @@ class AssetController extends Controller * @param array $result already loaded bundles list. * @throws \yii\console\Exception on failure. */ - protected function loadBundleDependency($name, $bundle, &$result) { + protected function loadBundleDependency($name, $bundle, &$result) + { if (!empty($bundle->depends)) { $assetManager = $this->getAssetManager(); foreach ($bundle->depends as $dependencyName) { @@ -308,7 +322,7 @@ class AssetController extends Controller /** * Builds output asset bundle. * @param \yii\web\AssetBundle $target output asset bundle - * @param string $type either "js" or "css". + * @param string $type either 'js' or 'css'. * @param \yii\web\AssetBundle[] $bundles source asset bundles. * @param integer $timestamp current timestamp. * @throws Exception on failure. @@ -420,24 +434,23 @@ class AssetController extends Controller } $array = var_export($array, true); $version = date('Y-m-d H:i:s', time()); - $bytesWritten = file_put_contents($bundleFile, <<id}" command. * DO NOT MODIFY THIS FILE DIRECTLY. - * @version $version + * @version {$version} */ -return $array; -EOD - ); - if ($bytesWritten <= 0) { +return {$array}; +EOD; + if (!file_put_contents($bundleFile, $bundleFileContent)) { throw new Exception("Unable to write output bundle configuration at '{$bundleFile}'."); } echo "Output bundle configuration created at '{$bundleFile}'.\n"; } /** - * Compresses given Java Script files and combines them into the single one. + * Compresses given JavaScript files and combines them into the single one. * @param array $inputFiles list of source file names. * @param string $outputFile output file name. * @throws \yii\console\Exception on failure @@ -495,9 +508,10 @@ EOD } /** - * Combines Java Script files into a single one. + * Combines JavaScript files into a single one. * @param array $inputFiles source file names. * @param string $outputFile output file name. + * @throws \yii\console\Exception on failure. */ public function combineJsFiles($inputFiles, $outputFile) { @@ -507,13 +521,16 @@ EOD . file_get_contents($file) . "/*** END FILE: $file ***/\n"; } - file_put_contents($outputFile, $content); + if (!file_put_contents($outputFile, $content)) { + throw new Exception("Unable to write output JavaScript file '{$outputFile}'."); + } } /** * Combines CSS files into a single one. * @param array $inputFiles source file names. * @param string $outputFile output file name. + * @throws \yii\console\Exception on failure. */ public function combineCssFiles($inputFiles, $outputFile) { @@ -523,7 +540,9 @@ EOD . $this->adjustCssUrl(file_get_contents($file), dirname($file), dirname($outputFile)) . "/*** END FILE: $file ***/\n"; } - file_put_contents($outputFile, $content); + if (!file_put_contents($outputFile, $content)) { + throw new Exception("Unable to write output CSS file '{$outputFile}'."); + } } /** @@ -554,7 +573,7 @@ EOD $inputFileRelativePathParts = explode('/', $inputFileRelativePath); $outputFileRelativePathParts = explode('/', $outputFileRelativePath); - $callback = function($matches) use ($inputFileRelativePathParts, $outputFileRelativePathParts) { + $callback = function ($matches) use ($inputFileRelativePathParts, $outputFileRelativePathParts) { $fullMatch = $matches[0]; $inputUrl = $matches[1]; @@ -590,18 +609,23 @@ EOD /** * Creates template of configuration file for [[actionCompress]]. * @param string $configFile output file name. + * @throws \yii\console\Exception on failure. */ public function actionTemplate($configFile) { $template = << require('path/to/bundles.php'), - // + // The list of extensions to compress: 'extensions' => require('path/to/namespaces.php'), - // + // Asset bundle for compression output: 'targets' => array( 'all' => array( 'basePath' => __DIR__, @@ -610,7 +634,7 @@ return array( 'css' => 'all-{ts}.css', ), ), - + // Asset manager configuration: 'assetManager' => array( 'basePath' => __DIR__, 'baseUrl' => '/test', @@ -622,9 +646,8 @@ EOD; return; } } - $bytesWritten = file_put_contents($configFile, $template); - if ($bytesWritten<=0) { - echo "Error: unable to write file '{$configFile}'!\n\n"; + if (!file_put_contents($configFile, $template)) { + throw new Exception("Unable to write template file '{$configFile}'."); } else { echo "Configuration file template created at '{$configFile}'.\n\n"; } diff --git a/framework/yii/console/controllers/MessageController.php b/framework/yii/console/controllers/MessageController.php index 715fb5c..44cf1b1 100644 --- a/framework/yii/console/controllers/MessageController.php +++ b/framework/yii/console/controllers/MessageController.php @@ -179,8 +179,7 @@ class MessageController extends Controller } ksort($translated); foreach ($translated as $message => $translation) { - if (!isset($merged[$message]) && !isset($todo[$message]) && !$removeOld) - { + if (!isset($merged[$message]) && !isset($todo[$message]) && !$removeOld) { if (substr($translation, 0, 2) === '@@' && substr($translation, -2) === '@@') { $todo[$message]=$translation; } else { diff --git a/framework/yii/console/controllers/MigrateController.php b/framework/yii/console/controllers/MigrateController.php index d3eb257..eca7787 100644 --- a/framework/yii/console/controllers/MigrateController.php +++ b/framework/yii/console/controllers/MigrateController.php @@ -1,635 +1,635 @@ - - * @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' - * yii migrate/create create_user_table - * - * # applies ALL new migrations - * yii migrate - * - * # reverts the last applied migration - * yii 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($action->id!=='create') { - 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, - * - * ~~~ - * yii migrate # apply all new migrations - * yii 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) - { - $migrations = $this->getNewMigrations(); - if (empty($migrations)) { - 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, - * - * ~~~ - * yii migrate/down # revert the last migration - * yii 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."); - } - - $migrations = $this->getMigrationHistory($limit); - if (empty($migrations)) { - 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, - * - * ~~~ - * yii migrate/redo # redo the last applied migration - * yii 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."); - } - - $migrations = $this->getMigrationHistory($limit); - if (empty($migrations)) { - 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, - * - * ~~~ - * yii migrate/to 101129_185401 # using timestamp - * yii 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. - * - * ~~~ - * yii migrate/mark 101129_185401 # using timestamp - * yii 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, - * - * ~~~ - * yii migrate/history # showing the last 10 migrations - * yii migrate/history 5 # showing the last 5 migrations - * yii 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 (empty($migrations)) { - 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, - * - * ~~~ - * yii migrate/new # showing the first 10 new migrations - * yii migrate/new 5 # showing the first 5 new migrations - * yii 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 (empty($migrations)) { - 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. - * - * ~~~ - * yii 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, true) === 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' + * yii migrate/create create_user_table + * + * # applies ALL new migrations + * yii migrate + * + * # reverts the last applied migration + * yii 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 ($action->id !== 'create') { + 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, + * + * ~~~ + * yii migrate # apply all new migrations + * yii 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) + { + $migrations = $this->getNewMigrations(); + if (empty($migrations)) { + 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, + * + * ~~~ + * yii migrate/down # revert the last migration + * yii 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."); + } + + $migrations = $this->getMigrationHistory($limit); + if (empty($migrations)) { + 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, + * + * ~~~ + * yii migrate/redo # redo the last applied migration + * yii 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."); + } + + $migrations = $this->getMigrationHistory($limit); + if (empty($migrations)) { + 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, + * + * ~~~ + * yii migrate/to 101129_185401 # using timestamp + * yii 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. + * + * ~~~ + * yii migrate/mark 101129_185401 # using timestamp + * yii 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, + * + * ~~~ + * yii migrate/history # showing the last 10 migrations + * yii migrate/history 5 # showing the last 5 migrations + * yii 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 (empty($migrations)) { + 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, + * + * ~~~ + * yii migrate/new # showing the first 10 new migrations + * yii migrate/new 5 # showing the first 5 new migrations + * yii 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 (empty($migrations)) { + 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. + * + * ~~~ + * yii 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, true) === 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/yii/web/Pagination.php b/framework/yii/data/Pagination.php similarity index 98% rename from framework/yii/web/Pagination.php rename to framework/yii/data/Pagination.php index c4a8106..4e25809 100644 --- a/framework/yii/web/Pagination.php +++ b/framework/yii/data/Pagination.php @@ -5,9 +5,10 @@ * @license http://www.yiiframework.com/license/ */ -namespace yii\web; +namespace yii\data; use Yii; +use yii\base\Object; /** * Pagination represents information relevant to pagination of data items. @@ -62,7 +63,7 @@ use Yii; * @author Qiang Xue * @since 2.0 */ -class Pagination extends \yii\base\Object +class Pagination extends Object { /** * @var string name of the parameter storing the current page index. Defaults to 'page'. diff --git a/framework/yii/web/Sort.php b/framework/yii/data/Sort.php similarity index 99% rename from framework/yii/web/Sort.php rename to framework/yii/data/Sort.php index 6014a3e..a46d0ce 100644 --- a/framework/yii/web/Sort.php +++ b/framework/yii/data/Sort.php @@ -5,9 +5,10 @@ * @license http://www.yiiframework.com/license/ */ -namespace yii\web; +namespace yii\data; use Yii; +use yii\base\Object; use yii\helpers\Html; /** @@ -68,7 +69,7 @@ use yii\helpers\Html; * @author Qiang Xue * @since 2.0 */ -class Sort extends \yii\base\Object +class Sort extends Object { /** * Sort ascending diff --git a/framework/yii/db/ActiveRecord.php b/framework/yii/db/ActiveRecord.php index 58411f0..6faebbf 100644 --- a/framework/yii/db/ActiveRecord.php +++ b/framework/yii/db/ActiveRecord.php @@ -275,10 +275,16 @@ class ActiveRecord extends Model /** * Returns the schema information of the DB table associated with this AR class. * @return TableSchema the schema information of the DB table associated with this AR class. + * @throws InvalidConfigException if the table for the AR class does not exist. */ public static function getTableSchema() { - return static::getDb()->getTableSchema(static::tableName()); + $schema = static::getDb()->getTableSchema(static::tableName()); + if ($schema !== null) { + return $schema; + } else { + throw new InvalidConfigException("The table does not exist: " . static::tableName()); + } } /** diff --git a/framework/yii/db/Connection.php b/framework/yii/db/Connection.php index 3a4d0ad..0dd47d8 100644 --- a/framework/yii/db/Connection.php +++ b/framework/yii/db/Connection.php @@ -305,8 +305,7 @@ class Connection extends Component $this->pdo = $this->createPdoInstance(); $this->initConnection(); Yii::endProfile($token, __METHOD__); - } - catch (\PDOException $e) { + } catch (\PDOException $e) { Yii::endProfile($token, __METHOD__); Yii::error("Failed to open DB connection ({$this->dsn}): " . $e->getMessage(), __METHOD__); $message = YII_DEBUG ? 'Failed to open DB connection: ' . $e->getMessage() : 'Failed to open DB connection.'; @@ -508,7 +507,7 @@ class Connection extends Component { $db = $this; return preg_replace_callback('/(\\{\\{([%\w\-\. ]+)\\}\\}|\\[\\[([\w\-\. ]+)\\]\\])/', - function($matches) use($db) { + function ($matches) use ($db) { if (isset($matches[3])) { return $db->quoteColumnName($matches[3]); } else { diff --git a/framework/yii/db/QueryBuilder.php b/framework/yii/db/QueryBuilder.php index c0b4223..04f1969 100644 --- a/framework/yii/db/QueryBuilder.php +++ b/framework/yii/db/QueryBuilder.php @@ -464,6 +464,12 @@ class QueryBuilder extends \yii\base\Object * the first part will be converted, and the rest of the parts will be appended to the converted result. * For example, 'string NOT NULL' is converted to 'varchar(255) NOT NULL'. * + * For some of the abstract types you can also specify a length or precision constraint + * by prepending it in round brackets directly to the type. + * For example `string(32)` will be converted into "varchar(32)" on a MySQL database. + * If the underlying DBMS does not support these kind of constraints for a type it will + * be ignored. + * * If a type cannot be found in [[typeMap]], it will be returned without any change. * @param string $type abstract column type * @return string physical column type. @@ -472,6 +478,10 @@ class QueryBuilder extends \yii\base\Object { if (isset($this->typeMap[$type])) { return $this->typeMap[$type]; + } elseif (preg_match('/^(\w+)\((.+?)\)(.*)$/', $type, $matches)) { + if (isset($this->typeMap[$matches[1]])) { + return preg_replace('/\(.+\)/', '(' . $matches[2] . ')', $this->typeMap[$matches[1]]) . $matches[3]; + } } elseif (preg_match('/^(\w+)\s+/', $type, $matches)) { if (isset($this->typeMap[$matches[1]])) { return preg_replace('/^\w+/', $this->typeMap[$matches[1]], $type); diff --git a/framework/yii/db/mssql/QueryBuilder.php b/framework/yii/db/mssql/QueryBuilder.php index 45a7507..e7f8f80 100644 --- a/framework/yii/db/mssql/QueryBuilder.php +++ b/framework/yii/db/mssql/QueryBuilder.php @@ -28,7 +28,7 @@ class QueryBuilder extends \yii\db\QueryBuilder Schema::TYPE_INTEGER => 'int(11)', Schema::TYPE_BIGINT => 'bigint(20)', Schema::TYPE_FLOAT => 'float', - Schema::TYPE_DECIMAL => 'decimal', + Schema::TYPE_DECIMAL => 'decimal(10,0)', Schema::TYPE_DATETIME => 'datetime', Schema::TYPE_TIMESTAMP => 'timestamp', Schema::TYPE_TIME => 'time', diff --git a/framework/yii/db/mysql/QueryBuilder.php b/framework/yii/db/mysql/QueryBuilder.php index 70c6d64..4b35e24 100644 --- a/framework/yii/db/mysql/QueryBuilder.php +++ b/framework/yii/db/mysql/QueryBuilder.php @@ -29,7 +29,7 @@ class QueryBuilder extends \yii\db\QueryBuilder Schema::TYPE_INTEGER => 'int(11)', Schema::TYPE_BIGINT => 'bigint(20)', Schema::TYPE_FLOAT => 'float', - Schema::TYPE_DECIMAL => 'decimal', + Schema::TYPE_DECIMAL => 'decimal(10,0)', Schema::TYPE_DATETIME => 'datetime', Schema::TYPE_TIMESTAMP => 'timestamp', Schema::TYPE_TIME => 'time', diff --git a/framework/yii/db/mysql/Schema.php b/framework/yii/db/mysql/Schema.php index 501149a..b42ef15 100644 --- a/framework/yii/db/mysql/Schema.php +++ b/framework/yii/db/mysql/Schema.php @@ -178,6 +178,7 @@ class Schema extends \yii\db\Schema * Collects the metadata of table columns. * @param TableSchema $table the table metadata * @return boolean whether the table exists in the database + * @throws \Exception if DB query fails */ protected function findColumns($table) { @@ -185,7 +186,12 @@ class Schema extends \yii\db\Schema try { $columns = $this->db->createCommand($sql)->queryAll(); } catch (\Exception $e) { - return false; + $previous = $e->getPrevious(); + if ($previous instanceof \PDOException && $previous->getCode() == '42S02') { + // table does not exist + return false; + } + throw $e; } foreach ($columns as $info) { $column = $this->loadColumnSchema($info); diff --git a/framework/yii/db/pgsql/QueryBuilder.php b/framework/yii/db/pgsql/QueryBuilder.php new file mode 100644 index 0000000..3417ad9 --- /dev/null +++ b/framework/yii/db/pgsql/QueryBuilder.php @@ -0,0 +1,41 @@ + + * @since 2.0 + */ +class QueryBuilder extends \yii\db\QueryBuilder +{ + + /** + * @var array mapping from abstract column types (keys) to physical column types (values). + */ + public $typeMap = array( + Schema::TYPE_PK => 'serial not null primary key', + Schema::TYPE_STRING => 'varchar', + Schema::TYPE_TEXT => 'text', + Schema::TYPE_SMALLINT => 'smallint', + Schema::TYPE_INTEGER => 'integer', + Schema::TYPE_BIGINT => 'bigint', + Schema::TYPE_FLOAT => 'double precision', + Schema::TYPE_DECIMAL => 'numeric', + Schema::TYPE_DATETIME => 'timestamp', + Schema::TYPE_TIMESTAMP => 'timestamp', + Schema::TYPE_TIME => 'time', + Schema::TYPE_DATE => 'date', + Schema::TYPE_BINARY => 'bytea', + Schema::TYPE_BOOLEAN => 'boolean', + Schema::TYPE_MONEY => 'numeric(19,4)', + ); + +} diff --git a/framework/yii/db/pgsql/Schema.php b/framework/yii/db/pgsql/Schema.php new file mode 100644 index 0000000..94f845f --- /dev/null +++ b/framework/yii/db/pgsql/Schema.php @@ -0,0 +1,288 @@ + + * @since 2.0 + */ +class Schema extends \yii\db\Schema +{ + + /** + * The default schema used for the current session. + * @var string + */ + public $defaultSchema = 'public'; + + /** + * @var array mapping from physical column types (keys) to abstract + * column types (values) + */ + public $typeMap = array( + 'abstime' => self::TYPE_TIMESTAMP, + 'bit' => self::TYPE_STRING, + 'boolean' => self::TYPE_BOOLEAN, + 'box' => self::TYPE_STRING, + 'character' => self::TYPE_STRING, + 'bytea' => self::TYPE_BINARY, + 'char' => self::TYPE_STRING, + 'cidr' => self::TYPE_STRING, + 'circle' => self::TYPE_STRING, + 'date' => self::TYPE_DATE, + 'real' => self::TYPE_FLOAT, + 'double precision' => self::TYPE_DECIMAL, + 'inet' => self::TYPE_STRING, + 'smallint' => self::TYPE_SMALLINT, + 'integer' => self::TYPE_INTEGER, + 'bigint' => self::TYPE_BIGINT, + 'interval' => self::TYPE_STRING, + 'json' => self::TYPE_STRING, + 'line' => self::TYPE_STRING, + 'macaddr' => self::TYPE_STRING, + 'money' => self::TYPE_MONEY, + 'name' => self::TYPE_STRING, + 'numeric' => self::TYPE_STRING, + 'numrange' => self::TYPE_DECIMAL, + 'oid' => self::TYPE_BIGINT, // should not be used. it's pg internal! + 'path' => self::TYPE_STRING, + 'point' => self::TYPE_STRING, + 'polygon' => self::TYPE_STRING, + 'text' => self::TYPE_TEXT, + 'time without time zone' => self::TYPE_TIME, + 'timestamp without time zone' => self::TYPE_TIMESTAMP, + 'timestamp with time zone' => self::TYPE_TIMESTAMP, + 'time with time zone' => self::TYPE_TIMESTAMP, + 'unknown' => self::TYPE_STRING, + 'uuid' => self::TYPE_STRING, + 'bit varying' => self::TYPE_STRING, + 'character varying' => self::TYPE_STRING, + 'xml' => self::TYPE_STRING + ); + + /** + * Creates a query builder for the PostgreSQL database. + * @return QueryBuilder query builder instance + */ + public function createQueryBuilder() + { + return new QueryBuilder($this->db); + } + + /** + * Resolves the table name and schema name (if any). + * @param TableSchema $table the table metadata object + * @param string $name the table name + */ + protected function resolveTableNames($table, $name) + { + $parts = explode('.', str_replace('"', '', $name)); + if (isset($parts[1])) { + $table->schemaName = $parts[0]; + $table->name = $parts[1]; + } else { + $table->name = $parts[0]; + } + if ($table->schemaName === null) { + $table->schemaName = $this->defaultSchema; + } + } + + /** + * Quotes a table name for use in a query. + * A simple table name has no schema prefix. + * @param string $name table name + * @return string the properly quoted table name + */ + public function quoteSimpleTableName($name) + { + return strpos($name, '"') !== false ? $name : '"' . $name . '"'; + } + + /** + * Loads the metadata for the specified table. + * @param string $name table name + * @return TableSchema|null driver dependent table metadata. Null if the table does not exist. + */ + public function loadTableSchema($name) + { + $table = new TableSchema(); + $this->resolveTableNames($table, $name); + if ($this->findColumns($table)) { + $this->findConstraints($table); + return $table; + } else { + return null; + } + } + + /** + * Collects the foreign key column details for the given table. + * @param TableSchema $table the table metadata + */ + protected function findConstraints($table) + { + + $tableName = $this->quoteValue($table->name); + $tableSchema = $this->quoteValue($table->schemaName); + + //We need to extract the constraints de hard way since: + //http://www.postgresql.org/message-id/26677.1086673982@sss.pgh.pa.us + + $sql = <<db->createCommand($sql)->queryAll(); + foreach ($constraints as $constraint) { + $columns = explode(',', $constraint['columns']); + $fcolumns = explode(',', $constraint['foreign_columns']); + if ($constraint['foreign_table_schema'] !== $this->defaultSchema) { + $foreignTable = $constraint['foreign_table_schema'] . '.' . $constraint['foreign_table_name']; + } else { + $foreignTable = $constraint['foreign_table_name']; + } + $citem = array($foreignTable); + foreach ($columns as $idx => $column) { + $citem[] = array($fcolumns[$idx] => $column); + } + $table->foreignKeys[] = $citem; + } + } + + /** + * Collects the metadata of table columns. + * @param TableSchema $table the table metadata + * @return boolean whether the table exists in the database + */ + protected function findColumns($table) + { + $tableName = $this->db->quoteValue($table->name); + $schemaName = $this->db->quoteValue($table->schemaName); + $sql = <<> 16) & 65535 + END + WHEN 700 /*float4*/ THEN 24 /*FLT_MANT_DIG*/ + WHEN 701 /*float8*/ THEN 53 /*DBL_MANT_DIG*/ + ELSE null + END AS numeric_precision, + CASE + WHEN atttypid IN (21, 23, 20) THEN 0 + WHEN atttypid IN (1700) THEN + CASE + WHEN atttypmod = -1 THEN null + ELSE (atttypmod - 4) & 65535 + END + ELSE null + END AS numeric_scale, + CAST( + information_schema._pg_char_max_length(information_schema._pg_truetypid(a, t), information_schema._pg_truetypmod(a, t)) + AS numeric + ) AS size, + a.attnum = any (ct.conkey) as is_pkey +FROM + pg_class c + LEFT JOIN pg_attribute a ON a.attrelid = c.oid + LEFT JOIN pg_attrdef ad ON a.attrelid = ad.adrelid AND a.attnum = ad.adnum + LEFT JOIN pg_type t ON a.atttypid = t.oid + LEFT JOIN pg_namespace d ON d.oid = c.relnamespace + LEFT join pg_constraint ct on ct.conrelid=c.oid and ct.contype='p' +WHERE + a.attnum > 0 + and c.relname = {$tableName} + and d.nspname = {$schemaName} +ORDER BY + a.attnum; +SQL; + + $columns = $this->db->createCommand($sql)->queryAll(); + foreach ($columns as $column) { + $column = $this->loadColumnSchema($column); + $table->columns[$column->name] = $column; + if ($column->isPrimaryKey === true) { + $table->primaryKey[] = $column->name; + if ($table->sequenceName === null && preg_match("/nextval\('\w+'(::regclass)?\)/", $column->defaultValue) === 1) { + $table->sequenceName = preg_replace(array('/nextval/', '/::/', '/regclass/', '/\'\)/', '/\(\'/'), '', $column->defaultValue); + } + } + } + return true; + } + + /** + * Loads the column information into a [[ColumnSchema]] object. + * @param array $info column information + * @return ColumnSchema the column schema object + */ + protected function loadColumnSchema($info) + { + $column = new ColumnSchema(); + $column->allowNull = $info['is_nullable']; + $column->autoIncrement = $info['is_autoinc']; + $column->comment = $info['column_comment']; + $column->dbType = $info['data_type']; + $column->defaultValue = $info['column_default']; + $column->enumValues = explode(',', str_replace(array("''"), array("'"), $info['enum_values'])); + $column->unsigned = false; // has no meanining in PG + $column->isPrimaryKey = $info['is_pkey']; + $column->name = $info['column_name']; + $column->precision = $info['numeric_precision']; + $column->scale = $info['numeric_scale']; + $column->size = $info['size']; + + if (isset($this->typeMap[$column->dbType])) { + $column->type = $this->typeMap[$column->dbType]; + } else { + $column->type = self::TYPE_STRING; + } + $column->phpType = $this->getColumnPhpType($column); + return $column; + } +} diff --git a/framework/yii/db/sqlite/QueryBuilder.php b/framework/yii/db/sqlite/QueryBuilder.php index 72d48f4..52c101b 100644 --- a/framework/yii/db/sqlite/QueryBuilder.php +++ b/framework/yii/db/sqlite/QueryBuilder.php @@ -30,13 +30,13 @@ class QueryBuilder extends \yii\db\QueryBuilder Schema::TYPE_INTEGER => 'integer', Schema::TYPE_BIGINT => 'bigint', Schema::TYPE_FLOAT => 'float', - Schema::TYPE_DECIMAL => 'decimal', + Schema::TYPE_DECIMAL => 'decimal(10,0)', Schema::TYPE_DATETIME => 'datetime', Schema::TYPE_TIMESTAMP => 'timestamp', Schema::TYPE_TIME => 'time', Schema::TYPE_DATE => 'date', Schema::TYPE_BINARY => 'blob', - Schema::TYPE_BOOLEAN => 'tinyint(1)', + Schema::TYPE_BOOLEAN => 'boolean', Schema::TYPE_MONEY => 'decimal(19,4)', ); diff --git a/framework/yii/debug/Module.php b/framework/yii/debug/Module.php index 3421d95..a680f53 100644 --- a/framework/yii/debug/Module.php +++ b/framework/yii/debug/Module.php @@ -14,4 +14,4 @@ namespace yii\debug; class Module extends \yii\base\Module { public $controllerNamespace = 'yii\debug\controllers'; -} \ No newline at end of file +} diff --git a/framework/yii/debug/controllers/DefaultController.php b/framework/yii/debug/controllers/DefaultController.php index 4d686ee..f1160b1 100644 --- a/framework/yii/debug/controllers/DefaultController.php +++ b/framework/yii/debug/controllers/DefaultController.php @@ -31,4 +31,4 @@ class DefaultController extends Controller echo "Unable to find debug data tagged with '$tag'."; } } -} \ No newline at end of file +} diff --git a/framework/yii/helpers/Json.php b/framework/yii/helpers/Json.php index 5e77c3f..117db1f 100644 --- a/framework/yii/helpers/Json.php +++ b/framework/yii/helpers/Json.php @@ -14,5 +14,4 @@ namespace yii\helpers; */ class Json extends base\Json { - } diff --git a/framework/yii/helpers/base/Console.php b/framework/yii/helpers/base/Console.php index 6ad0b7b..e3acbd9 100644 --- a/framework/yii/helpers/base/Console.php +++ b/framework/yii/helpers/base/Console.php @@ -286,7 +286,7 @@ class Console * You can pass any of the FG_*, BG_* and TEXT_* constants and also [[xtermFgColor]] and [[xtermBgColor]]. * @return string */ - public static function ansiFormat($string, $format=array()) + public static function ansiFormat($string, $format = array()) { $code = implode(';', $format); return "\033[0m" . ($code !== '' ? "\033[" . $code . "m" : '') . $string . "\033[0m"; @@ -589,11 +589,10 @@ class Console if (static::isRunningOnWindows()) { $output = array(); exec('mode con', $output); - if(isset($output) && strpos($output[1], 'CON')!==false) { + if (isset($output) && strpos($output[1], 'CON') !== false) { return $size = array((int)preg_replace('~[^0-9]~', '', $output[3]), (int)preg_replace('~[^0-9]~', '', $output[4])); } } else { - // try stty if available $stty = array(); if (exec('stty -a 2>&1', $stty) && preg_match('/rows\s+(\d+);\s*columns\s+(\d+);/mi', implode(' ', $stty), $matches)) { diff --git a/framework/yii/helpers/base/Html.php b/framework/yii/helpers/base/Html.php index 9a0001c..47385e2 100644 --- a/framework/yii/helpers/base/Html.php +++ b/framework/yii/helpers/base/Html.php @@ -1479,5 +1479,4 @@ class Html $name = strtolower(static::getInputName($model, $attribute)); return str_replace(array('[]', '][', '[', ']', ' '), array('', '-', '-', '', '-'), $name); } - } diff --git a/framework/yii/helpers/base/Inflector.php b/framework/yii/helpers/base/Inflector.php index 7454e4e..113d350 100644 --- a/framework/yii/helpers/base/Inflector.php +++ b/framework/yii/helpers/base/Inflector.php @@ -498,7 +498,7 @@ class Inflector if (in_array(($number % 100), range(11, 13))) { return $number . 'th'; } - switch (($number % 10)) { + switch ($number % 10) { case 1: return $number . 'st'; case 2: return $number . 'nd'; case 3: return $number . 'rd'; diff --git a/framework/yii/helpers/base/Json.php b/framework/yii/helpers/base/Json.php index 8de55f9..41d5bf0 100644 --- a/framework/yii/helpers/base/Json.php +++ b/framework/yii/helpers/base/Json.php @@ -8,6 +8,7 @@ namespace yii\helpers\base; use yii\base\InvalidParamException; +use yii\base\Jsonable; use yii\web\JsExpression; /** @@ -90,16 +91,19 @@ class Json $token = '!{[' . count($expressions) . ']}!'; $expressions['"' . $token . '"'] = $data->expression; return $token; - } - $result = array(); - foreach ($data as $key => $value) { - if (is_array($value) || is_object($value)) { - $result[$key] = static::processData($value, $expressions); - } else { - $result[$key] = $value; + } elseif ($data instanceof Jsonable) { + return $data->toJson(); + } else { + $result = array(); + foreach ($data as $key => $value) { + if (is_array($value) || is_object($value)) { + $result[$key] = static::processData($value, $expressions); + } else { + $result[$key] = $value; + } } + return $result; } - return $result; } else { return $data; } diff --git a/framework/yii/helpers/base/SecurityHelper.php b/framework/yii/helpers/base/SecurityHelper.php index 3f69fee..f646a24 100644 --- a/framework/yii/helpers/base/SecurityHelper.php +++ b/framework/yii/helpers/base/SecurityHelper.php @@ -131,15 +131,30 @@ class SecurityHelper $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); + $keys[$name] = static::generateRandomKey($length); file_put_contents($keyFile, " + * @since 2.0 + */ +class DbMessageSource extends MessageSource +{ + /** + * Prefix which would be used when generating cache key. + */ + const CACHE_KEY_PREFIX = 'DbMessageSource'; + + /** + * @var Connection|string the DB connection object or the application component ID of the DB connection. + * After the DbMessageSource object is created, if you want to change this property, you should only assign + * it with a DB connection object. + */ + public $db = 'db'; + /** + * @var Cache|string the cache object or the application component ID of the cache object. + * The messages data will be cached using this cache object. Note, this property has meaning only + * in case [[cachingDuration]] set to non-zero value. + * After the DbMessageSource object is created, if you want to change this property, you should only assign + * it with a cache object. + */ + public $cache = 'cache'; + /** + * @var string the name of the source message table. + */ + public $sourceMessageTable = 'tbl_source_message'; + /** + * @var string the name of the translated message table. + */ + public $messageTable = 'tbl_message'; + /** + * @var integer the time in seconds that the messages can remain valid in cache. + * Use 0 to indicate that the cached data will never expire. + * @see enableCaching + */ + public $cachingDuration = 0; + /** + * @var boolean whether to enable caching translated messages + */ + public $enableCaching = false; + + /** + * Initializes the DbMessageSource component. + * This method will initialize the [[db]] property to make sure it refers to a valid DB connection. + * Configured [[cache]] component would also be initialized. + * @throws InvalidConfigException if [[db]] is invalid or [[cache]] is invalid. + */ + 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("DbMessageSource::db must be either a DB connection instance or the application component ID of a DB connection."); + } + if ($this->enableCaching) { + if (is_string($this->cache)) { + $this->cache = Yii::$app->getComponent($this->cache); + } + if (!$this->cache instanceof Cache) { + throw new InvalidConfigException("DbMessageSource::cache must be either a cache object or the application component ID of the cache object."); + } + } + } + + /** + * 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. The keys are original messages, and the values + * are translated messages. + */ + protected function loadMessages($category, $language) + { + if ($this->enableCaching) { + $key = array( + __CLASS__, + $category, + $language, + ); + $messages = $this->cache->get($key); + if ($messages === false) { + $messages = $this->loadMessagesFromDb($category, $language); + $this->cache->set($key, $messages, $this->cachingDuration); + } + return $messages; + } else { + return $this->loadMessagesFromDb($category, $language); + } + } + + /** + * Loads the messages from database. + * You may override this method to customize the message storage in the database. + * @param string $category the message category. + * @param string $language the target language. + * @return array the messages loaded from database. + */ + protected function loadMessagesFromDb($category, $language) + { + $query = new Query(); + $messages = $query->select(array('t1.message message', 't2.translation translation')) + ->from(array($this->sourceMessageTable . ' t1', $this->messageTable . ' t2')) + ->where('t1.id = t2.id AND t1.category = :category AND t2.language = :language') + ->params(array(':category' => $category, ':language' => $language)) + ->createCommand($this->db) + ->queryAll(); + return ArrayHelper::map($messages, 'message', 'translation'); + } +} diff --git a/framework/yii/i18n/Formatter.php b/framework/yii/i18n/Formatter.php index d688a15..948e277 100644 --- a/framework/yii/i18n/Formatter.php +++ b/framework/yii/i18n/Formatter.php @@ -30,21 +30,40 @@ class Formatter extends \yii\base\Formatter */ public $locale; /** - * @var string the default format string to be used to format a date using PHP date() function. + * @var string the default format string to be used to format a date. + * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths. + * It can also be a custom format as specified in the [ICU manual](http://userguide.icu-project.org/formatparse/datetime). */ public $dateFormat = 'short'; /** - * @var string the default format string to be used to format a time using PHP date() function. + * @var string the default format string to be used to format a time. + * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths. + * It can also be a custom format as specified in the [ICU manual](http://userguide.icu-project.org/formatparse/datetime). */ public $timeFormat = 'short'; /** - * @var string the default format string to be used to format a date and time using PHP date() function. + * @var string the default format string to be used to format a date and time. + * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths. + * It can also be a custom format as specified in the [ICU manual](http://userguide.icu-project.org/formatparse/datetime). */ public $datetimeFormat = 'short'; /** * @var array the options to be set for the NumberFormatter objects. Please refer to + * [PHP manual](http://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants.unumberformatattribute) + * for the possible options. This property is used by [[createNumberFormatter]] when + * creating a new number formatter to format decimals, currencies, etc. */ public $numberFormatOptions = array(); + /** + * @var string the character displayed as the decimal point when formatting a number. + * If not set, the decimal separator corresponding to [[locale]] will be used. + */ + public $decimalSeparator; + /** + * @var string the character displayed as the thousands separator character when formatting a number. + * If not set, the thousand separator corresponding to [[locale]] will be used. + */ + public $thousandSeparator; /** @@ -61,6 +80,16 @@ class Formatter extends \yii\base\Formatter if ($this->locale === null) { $this->locale = Yii::$app->language; } + if ($this->decimalSeparator === null || $this->thousandSeparator === null) { + $formatter = new NumberFormatter($this->locale, NumberFormatter::DECIMAL); + if ($this->decimalSeparator === null) { + $this->decimalSeparator = $formatter->getSymbol(NumberFormatter::DECIMAL_SEPARATOR_SYMBOL); + } + if ($this->thousandSeparator === null) { + $this->thousandSeparator = $formatter->getSymbol(NumberFormatter::GROUPING_SEPARATOR_SYMBOL); + } + } + parent::init(); } @@ -81,8 +110,11 @@ class Formatter extends \yii\base\Formatter * - a PHP DateTime object * * @param string $format the format used to convert the value into a date string. - * If null, [[dateFormat]] will be used. The format string should be the one - * that can be recognized by the PHP `date()` function. + * If null, [[dateFormat]] will be used. + * + * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths. + * It can also be a custom format as specified in the [ICU manual](http://userguide.icu-project.org/formatparse/datetime). + * * @return string the formatted result * @see dateFormat */ @@ -111,8 +143,11 @@ class Formatter extends \yii\base\Formatter * - a PHP DateTime object * * @param string $format the format used to convert the value into a date string. - * If null, [[dateFormat]] will be used. The format string should be the one - * that can be recognized by the PHP `date()` function. + * If null, [[dateFormat]] will be used. + * + * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths. + * It can also be a custom format as specified in the [ICU manual](http://userguide.icu-project.org/formatparse/datetime). + * * @return string the formatted result * @see timeFormat */ @@ -141,8 +176,11 @@ class Formatter extends \yii\base\Formatter * - a PHP DateTime object * * @param string $format the format used to convert the value into a date string. - * If null, [[dateFormat]] will be used. The format string should be the one - * that can be recognized by the PHP `date()` function. + * If null, [[dateFormat]] will be used. + * + * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths. + * It can also be a custom format as specified in the [ICU manual](http://userguide.icu-project.org/formatparse/datetime). + * * @return string the formatted result * @see datetimeFormat */ @@ -213,7 +251,7 @@ class Formatter extends \yii\base\Formatter /** * Creates a number formatter based on the given type and format. * @param integer $type the type of the number formatter - * @param string $format the format to be used + * @param string $format the format to be used. Please refer to [ICU manual](http://www.icu-project.org/apiref/icu4c/classDecimalFormat.html#_details) * @return NumberFormatter the created formatter instance */ protected function createNumberFormatter($type, $format) diff --git a/framework/yii/i18n/I18N.php b/framework/yii/i18n/I18N.php index a358683..b929f49 100644 --- a/framework/yii/i18n/I18N.php +++ b/framework/yii/i18n/I18N.php @@ -78,16 +78,11 @@ class I18N extends Component * @param string $category the message category. * @param string $message the message to be translated. * @param array $params the parameters that will be used to replace the corresponding placeholders in the message. - * @param string $language the language code (e.g. `en_US`, `en`). If this is null, the current - * [[\yii\base\Application::language|application language]] will be used. + * @param string $language the language code (e.g. `en_US`, `en`). * @return string the translated message. */ - public function translate($category, $message, $params = array(), $language = null) + public function translate($category, $message, $params, $language) { - if ($language === null) { - $language = Yii::$app->language; - } - $message = $this->getMessageSource($category)->translate($category, $message, $language); if (!is_array($params)) { diff --git a/framework/yii/i18n/MessageSource.php b/framework/yii/i18n/MessageSource.php index cf23338..90adbfb 100644 --- a/framework/yii/i18n/MessageSource.php +++ b/framework/yii/i18n/MessageSource.php @@ -118,4 +118,3 @@ class MessageSource extends Component } } } - diff --git a/framework/yii/jui/Accordion.php b/framework/yii/jui/Accordion.php index f36c981..898649e 100644 --- a/framework/yii/jui/Accordion.php +++ b/framework/yii/jui/Accordion.php @@ -125,7 +125,7 @@ class Accordion extends Widget $items[] = Html::tag($headerTag, $item['header'], $headerOptions); $options = array_merge($this->itemOptions, ArrayHelper::getValue($item, 'options', array())); $tag = ArrayHelper::remove($options, 'tag', 'div'); - $items[] = Html::tag($tag, $item['content'], $options);; + $items[] = Html::tag($tag, $item['content'], $options); } return implode("\n", $items); diff --git a/framework/yii/jui/Menu.php b/framework/yii/jui/Menu.php index d4e390c..83523e7 100644 --- a/framework/yii/jui/Menu.php +++ b/framework/yii/jui/Menu.php @@ -10,7 +10,6 @@ namespace yii\jui; use Yii; use yii\helpers\Json; - /** * Menu renders a menu jQuery UI widget. * diff --git a/framework/yii/jui/Widget.php b/framework/yii/jui/Widget.php index d34a8bd..5724919 100644 --- a/framework/yii/jui/Widget.php +++ b/framework/yii/jui/Widget.php @@ -10,7 +10,6 @@ namespace yii\jui; use Yii; use yii\helpers\Json; - /** * \yii\jui\Widget is the base class for all jQuery UI widgets. * diff --git a/framework/yii/logging/ProfileTarget.php b/framework/yii/logging/ProfileTarget.php index f4522ac..be1d73d 100644 --- a/framework/yii/logging/ProfileTarget.php +++ b/framework/yii/logging/ProfileTarget.php @@ -70,7 +70,7 @@ class CProfileLogRoute extends CWebLogRoute public function processLogs($logs) { $app = \Yii::$app; - if (!($app instanceof CWebApplication) || $app->getRequest()->getIsAjaxRequest()) + if (!($app instanceof \yii\web\Application) || $app->getRequest()->getIsAjax()) return; if ($this->getReport() === 'summary') diff --git a/framework/yii/logging/Target.php b/framework/yii/logging/Target.php index fac8b53..7be7001 100644 --- a/framework/yii/logging/Target.php +++ b/framework/yii/logging/Target.php @@ -204,11 +204,9 @@ abstract class Target extends Component if ($matched) { foreach ($this->except as $category) { $prefix = rtrim($category, '*'); - foreach ($messages as $i => $message) { - if (strpos($message[2], $prefix) === 0 && ($message[2] === $category || $prefix !== $category)) { - $matched = false; - break; - } + if (strpos($message[2], $prefix) === 0 && ($message[2] === $category || $prefix !== $category)) { + $matched = false; + break; } } } diff --git a/framework/yii/rbac/DbManager.php b/framework/yii/rbac/DbManager.php index b7a5d4e..8d3bea2 100644 --- a/framework/yii/rbac/DbManager.php +++ b/framework/yii/rbac/DbManager.php @@ -493,8 +493,9 @@ class DbManager extends Manager 'bizRule' => $row['biz_rule'], 'data' => $data, )); - } else + } else { return null; + } } /** diff --git a/framework/yii/rbac/PhpManager.php b/framework/yii/rbac/PhpManager.php index 7a476e0..8ecc75c 100644 --- a/framework/yii/rbac/PhpManager.php +++ b/framework/yii/rbac/PhpManager.php @@ -468,7 +468,7 @@ class PhpManager extends Manager 'bizRule' => $assignment['bizRule'], 'data' => $assignment['data'], )); - } + } } } } diff --git a/framework/yii/requirements/requirements.php b/framework/yii/requirements/requirements.php index 63aa70d..670544d 100644 --- a/framework/yii/requirements/requirements.php +++ b/framework/yii/requirements/requirements.php @@ -45,4 +45,4 @@ return array( 'by' => 'Internationalization support', 'memo' => 'PHP Intl extension 1.0.2 or higher is required when you want to use IDN-feature of EmailValidator or UrlValidator or the yii\i18n\Formatter class.' ), -); \ No newline at end of file +); diff --git a/framework/yii/validators/CaptchaValidator.php b/framework/yii/validators/CaptchaValidator.php index dbc263e..01870d3 100644 --- a/framework/yii/validators/CaptchaValidator.php +++ b/framework/yii/validators/CaptchaValidator.php @@ -117,4 +117,3 @@ class CaptchaValidator extends Validator return 'yii.validation.captcha(value, messages, ' . json_encode($options) . ');'; } } - diff --git a/framework/yii/validators/CompareValidator.php b/framework/yii/validators/CompareValidator.php index b8e8a50..c94577c 100644 --- a/framework/yii/validators/CompareValidator.php +++ b/framework/yii/validators/CompareValidator.php @@ -58,7 +58,7 @@ class CompareValidator extends Validator * - `<`: validates to see if the value being validated is less than the value being compared with. * - `<=`: validates to see if the value being validated is less than or equal to the value being compared with. */ - public $operator = '='; + public $operator = '=='; /** * @var string the user-defined error message. It may contain the following placeholders which * will be replaced accordingly by the validator: diff --git a/framework/yii/validators/DateValidator.php b/framework/yii/validators/DateValidator.php index 7370b78..2f9e18b 100644 --- a/framework/yii/validators/DateValidator.php +++ b/framework/yii/validators/DateValidator.php @@ -58,7 +58,7 @@ class DateValidator extends Validator $date = DateTime::createFromFormat($this->format, $value); if ($date === false) { $this->addError($object, $attribute, $this->message); - } elseif ($this->timestampAttribute !== false) { + } elseif ($this->timestampAttribute !== null) { $object->{$this->timestampAttribute} = $date->getTimestamp(); } } @@ -73,4 +73,3 @@ class DateValidator extends Validator return DateTime::createFromFormat($this->format, $value) !== false; } } - diff --git a/framework/yii/validators/DefaultValueValidator.php b/framework/yii/validators/DefaultValueValidator.php index 185dbd4..20df5fd 100644 --- a/framework/yii/validators/DefaultValueValidator.php +++ b/framework/yii/validators/DefaultValueValidator.php @@ -40,4 +40,3 @@ class DefaultValueValidator extends Validator } } } - diff --git a/framework/yii/validators/ExistValidator.php b/framework/yii/validators/ExistValidator.php index 7c45491..9c74890 100644 --- a/framework/yii/validators/ExistValidator.php +++ b/framework/yii/validators/ExistValidator.php @@ -99,4 +99,3 @@ class ExistValidator extends Validator return $query->exists(); } } - diff --git a/framework/yii/validators/FilterValidator.php b/framework/yii/validators/FilterValidator.php index 72a9a9d..560feb1 100644 --- a/framework/yii/validators/FilterValidator.php +++ b/framework/yii/validators/FilterValidator.php @@ -6,6 +6,7 @@ */ namespace yii\validators; + use yii\base\InvalidConfigException; /** diff --git a/framework/yii/validators/RangeValidator.php b/framework/yii/validators/RangeValidator.php index a915275..90256df 100644 --- a/framework/yii/validators/RangeValidator.php +++ b/framework/yii/validators/RangeValidator.php @@ -35,7 +35,7 @@ class RangeValidator extends Validator * @var boolean whether to invert the validation logic. Defaults to false. If set to true, * the attribute value should NOT be among the list of values defined via [[range]]. **/ - public $not = false; + public $not = false; /** * Initializes the validator. diff --git a/framework/yii/validators/RegularExpressionValidator.php b/framework/yii/validators/RegularExpressionValidator.php index 417f2bc..72a5a74 100644 --- a/framework/yii/validators/RegularExpressionValidator.php +++ b/framework/yii/validators/RegularExpressionValidator.php @@ -32,7 +32,7 @@ class RegularExpressionValidator extends Validator * the regular expression defined via [[pattern]] should NOT match the attribute value. * @throws InvalidConfigException if the "pattern" is not a valid regular expression **/ - public $not = false; + public $not = false; /** * Initializes the validator. diff --git a/framework/yii/validators/StringValidator.php b/framework/yii/validators/StringValidator.php index abe4634..e06354b 100644 --- a/framework/yii/validators/StringValidator.php +++ b/framework/yii/validators/StringValidator.php @@ -174,4 +174,3 @@ class StringValidator extends Validator return 'yii.validation.string(value, messages, ' . json_encode($options) . ');'; } } - diff --git a/framework/yii/validators/UrlValidator.php b/framework/yii/validators/UrlValidator.php index 6cf12c1..18f2f45 100644 --- a/framework/yii/validators/UrlValidator.php +++ b/framework/yii/validators/UrlValidator.php @@ -99,7 +99,7 @@ class UrlValidator extends Validator } if ($this->enableIDN) { - $value = preg_replace_callback('/:\/\/([^\/]+)/', function($matches) { + $value = preg_replace_callback('/:\/\/([^\/]+)/', function ($matches) { return '://' . idn_to_ascii($matches[1]); }, $value); } diff --git a/framework/yii/validators/Validator.php b/framework/yii/validators/Validator.php index 6b103bf..2629002 100644 --- a/framework/yii/validators/Validator.php +++ b/framework/yii/validators/Validator.php @@ -179,7 +179,7 @@ abstract class Validator extends Component } foreach ($attributes as $attribute) { $skip = $this->skipOnError && $object->hasErrors($attribute) - || $this->skipOnEmpty && $this->isEmpty($object->$attribute); + || $this->skipOnEmpty && $this->isEmpty($object->$attribute); if (!$skip) { $this->validateAttribute($object, $attribute); } diff --git a/framework/yii/web/AccessRule.php b/framework/yii/web/AccessRule.php index 3897769..9bd52ce 100644 --- a/framework/yii/web/AccessRule.php +++ b/framework/yii/web/AccessRule.php @@ -99,7 +99,7 @@ class AccessRule extends Component if ($this->matchAction($action) && $this->matchRole($user) && $this->matchIP($request->getUserIP()) - && $this->matchVerb($request->getRequestMethod()) + && $this->matchVerb($request->getMethod()) && $this->matchController($action->controller) && $this->matchCustom($action) ) { diff --git a/framework/yii/web/CaptchaAction.php b/framework/yii/web/CaptchaAction.php index cff2314..1ed1fb0 100644 --- a/framework/yii/web/CaptchaAction.php +++ b/framework/yii/web/CaptchaAction.php @@ -277,11 +277,8 @@ class CaptchaAction extends Action imagecolordeallocate($image, $foreColor); - header('Pragma: public'); - header('Expires: 0'); - header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); - header('Content-Transfer-Encoding: binary'); - header("Content-type: image/png"); + $this->sendHttpHeaders(); + imagepng($image); imagedestroy($image); } @@ -319,12 +316,21 @@ class CaptchaAction extends Action $x += (int)($fontMetrics['textWidth']) + $this->offset; } - header('Pragma: public'); - header('Expires: 0'); - header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); - header('Content-Transfer-Encoding: binary'); - header("Content-type: image/png"); $image->setImageFormat('png'); - echo $image; + Yii::$app->getResponse()->content = (string)$image; + $this->sendHttpHeaders(); + } + + /** + * Sends the HTTP headers needed by image response. + */ + protected function sendHttpHeaders() + { + Yii::$app->getResponse()->getHeaders() + ->set('Pragma', 'public') + ->set('Expires', '0') + ->set('Cache-Control', 'must-revalidate, post-check=0, pre-check=0') + ->set('Content-Transfer-Encoding', 'binary') + ->set('Content-type', 'image/png'); } } diff --git a/framework/yii/web/Cookie.php b/framework/yii/web/Cookie.php index 610e5aa..8cbb412 100644 --- a/framework/yii/web/Cookie.php +++ b/framework/yii/web/Cookie.php @@ -45,7 +45,7 @@ class Cookie extends \yii\base\Object * By setting this property to true, the cookie will not be accessible by scripting languages, * such as JavaScript, which can effectively help to reduce identity theft through XSS attacks. */ - public $httponly = false; + public $httpOnly = false; /** * Magic method to turn a cookie object into a string without having to explicitly access [[value]]. diff --git a/framework/yii/web/CookieCollection.php b/framework/yii/web/CookieCollection.php index 97726c7..3e22092 100644 --- a/framework/yii/web/CookieCollection.php +++ b/framework/yii/web/CookieCollection.php @@ -9,7 +9,8 @@ namespace yii\web; use Yii; use ArrayIterator; -use yii\helpers\SecurityHelper; +use yii\base\InvalidCallException; +use yii\base\Object; /** * CookieCollection maintains the cookies available in the current request. @@ -19,17 +20,12 @@ use yii\helpers\SecurityHelper; * @author Qiang Xue * @since 2.0 */ -class CookieCollection extends \yii\base\Object implements \IteratorAggregate, \ArrayAccess, \Countable +class CookieCollection extends Object implements \IteratorAggregate, \ArrayAccess, \Countable { /** - * @var boolean whether to enable cookie validation. By setting this property to true, - * if a cookie is tampered on the client side, it will be ignored when received on the server side. + * @var boolean whether this collection is read only. */ - public $enableValidation = true; - /** - * @var string the secret key used for cookie validation. If not set, a random key will be generated and used. - */ - public $validationKey; + public $readOnly = false; /** * @var Cookie[] the cookies in this collection (indexed by the cookie names) @@ -38,12 +34,14 @@ class CookieCollection extends \yii\base\Object implements \IteratorAggregate, \ /** * Constructor. + * @param array $cookies the cookies that this collection initially contains. This should be + * an array of name-value pairs.s * @param array $config name-value pairs that will be used to initialize the object properties */ - public function __construct($config = array()) + public function __construct($cookies = array(), $config = array()) { + $this->_cookies = $cookies; parent::__construct($config); - $this->_cookies = $this->loadCookies(); } /** @@ -101,53 +99,66 @@ class CookieCollection extends \yii\base\Object implements \IteratorAggregate, \ } /** + * Returns whether there is a cookie with the specified name. + * @param string $name the cookie name + * @return boolean whether the named cookie exists + */ + public function has($name) + { + return isset($this->_cookies[$name]); + } + + /** * Adds a cookie to the collection. * If there is already a cookie with the same name in the collection, it will be removed first. * @param Cookie $cookie the cookie to be added + * @throws InvalidCallException if the cookie collection is read only */ public function add($cookie) { - if (isset($this->_cookies[$cookie->name])) { - $c = $this->_cookies[$cookie->name]; - setcookie($c->name, '', 0, $c->path, $c->domain, $c->secure, $c->httponly); - } - - $value = $cookie->value; - if ($this->enableValidation) { - if ($this->validationKey === null) { - $key = SecurityHelper::getSecretKey(__CLASS__ . '/' . Yii::$app->id); - } else { - $key = $this->validationKey; - } - $value = SecurityHelper::hashData(serialize($value), $key); + if ($this->readOnly) { + throw new InvalidCallException('The cookie collection is read only.'); } - - setcookie($cookie->name, $value, $cookie->expire, $cookie->path, $cookie->domain, $cookie->secure, $cookie->httponly); $this->_cookies[$cookie->name] = $cookie; } /** - * Removes a cookie from the collection. + * Removes a cookie. + * If `$removeFromBrowser` is true, the cookie will be removed from the browser. + * In this case, a cookie with outdated expiry will be added to the collection. * @param Cookie|string $cookie the cookie object or the name of the cookie to be removed. + * @param boolean $removeFromBrowser whether to remove the cookie from browser + * @throws InvalidCallException if the cookie collection is read only */ - public function remove($cookie) + public function remove($cookie, $removeFromBrowser = true) { - if (is_string($cookie) && isset($this->_cookies[$cookie])) { - $cookie = $this->_cookies[$cookie]; + if ($this->readOnly) { + throw new InvalidCallException('The cookie collection is read only.'); } if ($cookie instanceof Cookie) { - setcookie($cookie->name, '', 0, $cookie->path, $cookie->domain, $cookie->secure, $cookie->httponly); + $cookie->expire = 1; + $cookie->value = ''; + } else { + $cookie = new Cookie(array( + 'name' => $cookie, + 'expire' => 1, + )); + } + if ($removeFromBrowser) { + $this->_cookies[$cookie->name] = $cookie; + } else { unset($this->_cookies[$cookie->name]); } } /** * Removes all cookies. + * @throws InvalidCallException if the cookie collection is read only */ public function removeAll() { - foreach ($this->_cookies as $cookie) { - setcookie($cookie->name, '', 0, $cookie->path, $cookie->domain, $cookie->secure, $cookie->httponly); + if ($this->readOnly) { + throw new InvalidCallException('The cookie collection is read only.'); } $this->_cookies = array(); } @@ -172,7 +183,7 @@ class CookieCollection extends \yii\base\Object implements \IteratorAggregate, \ */ public function offsetExists($name) { - return isset($this->_cookies[$name]); + return $this->has($name); } /** @@ -212,36 +223,4 @@ class CookieCollection extends \yii\base\Object implements \IteratorAggregate, \ { $this->remove($name); } - - /** - * Returns the current cookies in terms of [[Cookie]] objects. - * @return Cookie[] list of current cookies - */ - protected function loadCookies() - { - $cookies = array(); - if ($this->enableValidation) { - if ($this->validationKey === null) { - $key = SecurityHelper::getSecretKey(__CLASS__ . '/' . Yii::$app->id); - } else { - $key = $this->validationKey; - } - foreach ($_COOKIE as $name => $value) { - if (is_string($value) && ($value = SecurityHelper::validateData($value, $key)) !== false) { - $cookies[$name] = new Cookie(array( - 'name' => $name, - 'value' => @unserialize($value), - )); - } - } - } else { - foreach ($_COOKIE as $name => $value) { - $cookies[$name] = new Cookie(array( - 'name' => $name, - 'value' => $value, - )); - } - } - return $cookies; - } } diff --git a/framework/yii/web/HeaderCollection.php b/framework/yii/web/HeaderCollection.php new file mode 100644 index 0000000..ed9ec6f --- /dev/null +++ b/framework/yii/web/HeaderCollection.php @@ -0,0 +1,201 @@ + + * @since 2.0 + */ +class HeaderCollection extends Object implements \IteratorAggregate, \ArrayAccess, \Countable +{ + /** + * @var array the headers in this collection (indexed by the header names) + */ + private $_headers = array(); + + /** + * Returns an iterator for traversing the headers in the collection. + * This method is required by the SPL interface `IteratorAggregate`. + * It will be implicitly called when you use `foreach` to traverse the collection. + * @return ArrayIterator an iterator for traversing the headers in the collection. + */ + public function getIterator() + { + return new ArrayIterator($this->_headers); + } + + /** + * Returns the number of headers in the collection. + * This method is required by the SPL `Countable` interface. + * It will be implicitly called when you use `count($collection)`. + * @return integer the number of headers in the collection. + */ + public function count() + { + return $this->getCount(); + } + + /** + * Returns the number of headers in the collection. + * @return integer the number of headers in the collection. + */ + public function getCount() + { + return count($this->_headers); + } + + /** + * Returns the named header(s). + * @param string $name the name of the header to return + * @param mixed $default the value to return in case the named header does not exist + * @param boolean $first whether to only return the first header of the specified name. + * If false, all headers of the specified name will be returned. + * @return string|array the named header(s). If `$first` is true, a string will be returned; + * If `$first` is false, an array will be returned. + */ + public function get($name, $default = null, $first = true) + { + $name = strtolower($name); + if (isset($this->_headers[$name])) { + return $first ? reset($this->_headers[$name]) : $this->_headers[$name]; + } else { + return $default; + } + } + + /** + * Adds a new header. + * If there is already a header with the same name, it will be replaced. + * @param string $name the name of the header + * @param string $value the value of the header + * @return HeaderCollection the collection object itself + */ + public function set($name, $value = '') + { + $name = strtolower($name); + $this->_headers[$name] = (array)$value; + return $this; + } + + /** + * Adds a new header. + * If there is already a header with the same name, the new one will + * be appended to it instead of replacing it. + * @param string $name the name of the header + * @param string $value the value of the header + * @return HeaderCollection the collection object itself + */ + public function add($name, $value) + { + $name = strtolower($name); + $this->_headers[$name][] = $value; + return $this; + } + + /** + * Returns a value indicating whether the named header exists. + * @param string $name the name of the header + * @return boolean whether the named header exists + */ + public function has($name) + { + $name = strtolower($name); + return isset($this->_headers[$name]); + } + + /** + * Removes a header. + * @param string $name the name of the header to be removed. + * @return string the value of the removed header. Null is returned if the header does not exist. + */ + public function remove($name) + { + $name = strtolower($name); + if (isset($this->_headers[$name])) { + $value = $this->_headers[$name]; + unset($this->_headers[$name]); + return $value; + } else { + return null; + } + } + + /** + * Removes all headers. + */ + public function removeAll() + { + $this->_headers = array(); + } + + /** + * Returns the collection as a PHP array. + * @return array the array representation of the collection. + * The array keys are header names, and the array values are the corresponding header values. + */ + public function toArray() + { + return $this->_headers; + } + + /** + * Returns whether there is a header with the specified name. + * This method is required by the SPL interface `ArrayAccess`. + * It is implicitly called when you use something like `isset($collection[$name])`. + * @param string $name the header name + * @return boolean whether the named header exists + */ + public function offsetExists($name) + { + return $this->has($name); + } + + /** + * Returns the header with the specified name. + * This method is required by the SPL interface `ArrayAccess`. + * It is implicitly called when you use something like `$header = $collection[$name];`. + * This is equivalent to [[get()]]. + * @param string $name the header name + * @return string the header value with the specified name, null if the named header does not exist. + */ + public function offsetGet($name) + { + return $this->get($name); + } + + /** + * Adds the header to the collection. + * This method is required by the SPL interface `ArrayAccess`. + * It is implicitly called when you use something like `$collection[$name] = $header;`. + * This is equivalent to [[add()]]. + * @param string $name the header name + * @param string $value the header value to be added + */ + public function offsetSet($name, $value) + { + $this->set($name, $value); + } + + /** + * Removes the named header. + * This method is required by the SPL interface `ArrayAccess`. + * It is implicitly called when you use something like `unset($collection[$name])`. + * This is equivalent to [[remove()]]. + * @param string $name the header name + */ + public function offsetUnset($name) + { + $this->remove($name); + } +} diff --git a/framework/yii/web/HttpCache.php b/framework/yii/web/HttpCache.php index 0a3bb86..cc9e6ed 100644 --- a/framework/yii/web/HttpCache.php +++ b/framework/yii/web/HttpCache.php @@ -50,7 +50,7 @@ class HttpCache extends ActionFilter /** * @var string HTTP cache control header. If null, the header will not be sent. */ - public $cacheControlHeader = 'Cache-Control: max-age=3600, public'; + public $cacheControlHeader = 'max-age=3600, public'; /** * This method is invoked right before an action is to be executed (after all possible filters.) @@ -60,7 +60,7 @@ class HttpCache extends ActionFilter */ public function beforeAction($action) { - $verb = Yii::$app->request->getRequestMethod(); + $verb = Yii::$app->getRequest()->getMethod(); if ($verb !== 'GET' && $verb !== 'HEAD' || $this->lastModified === null && $this->etagSeed === null) { return true; } @@ -75,17 +75,18 @@ class HttpCache extends ActionFilter } $this->sendCacheControlHeader(); + $response = Yii::$app->getResponse(); if ($etag !== null) { - header("ETag: $etag"); + $response->getHeaders()->set('Etag', $etag); } if ($this->validateCache($lastModified, $etag)) { - header('HTTP/1.1 304 Not Modified'); + $response->setStatusCode(304); return false; } if ($lastModified !== null) { - header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $lastModified) . ' GMT'); + $response->getHeaders()->set('Last-Modified', gmdate('D, d M Y H:i:s', $lastModified) . ' GMT'); } return true; } @@ -113,9 +114,10 @@ class HttpCache extends ActionFilter protected function sendCacheControlHeader() { session_cache_limiter('public'); - header('Pragma:', true); + $headers = Yii::$app->getResponse()->getHeaders(); + $headers->set('Pragma'); if ($this->cacheControlHeader !== null) { - header($this->cacheControlHeader, true); + $headers->set('Cache-Control', $this->cacheControlHeader); } } diff --git a/framework/yii/web/PageCache.php b/framework/yii/web/PageCache.php index 2fe36b3..8b28e62 100644 --- a/framework/yii/web/PageCache.php +++ b/framework/yii/web/PageCache.php @@ -1,104 +1,104 @@ - - * @since 2.0 - */ -class PageCache extends ActionFilter -{ - /** - * @var boolean whether the content being cached should be differentiated according to the route. - * A route consists of the requested controller ID and action ID. Defaults to true. - */ - public $varyByRoute = true; - /** - * @var 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(); - } + + * @since 2.0 + */ +class PageCache extends ActionFilter +{ + /** + * @var boolean whether the content being cached should be differentiated according to the route. + * A route consists of the requested controller ID and action ID. Defaults to true. + */ + public $varyByRoute = true; + /** + * @var string the application component ID of the [[\yii\caching\Cache|cache]] object. + */ + public $cache = 'cache'; + /** + * @var integer number of seconds that the data can remain valid in cache. + * Use 0 to indicate that the cached data will never expire. + */ + public $duration = 60; + /** + * @var array|Dependency the dependency that the cached content depends on. + * This can be either a [[Dependency]] object or a configuration array for creating the dependency object. + * For example, + * + * ~~~ + * array( + * 'class' => 'yii\caching\DbDependency', + * 'sql' => 'SELECT MAX(lastModified) FROM Post', + * ) + * ~~~ + * + * would make the output cache depends on the last modified time of all posts. + * If any post has its modification time changed, the cached content would be invalidated. + */ + public $dependency; + /** + * @var array list of factors that would cause the variation of the content being cached. + * Each factor is a string representing a variation (e.g. the language, a GET parameter). + * The following variation setting will cause the content to be cached in different versions + * according to the current application language: + * + * ~~~ + * array( + * Yii::$app->language, + * ) + */ + public $variations; + /** + * @var boolean whether to enable the fragment cache. You may use this property to turn on and off + * the fragment cache according to specific setting (e.g. enable fragment cache only for GET requests). + */ + public $enabled = true; + + + public function init() + { + parent::init(); + if ($this->view === null) { + $this->view = Yii::$app->getView(); + } + } + + /** + * This method is invoked right before an action is to be executed (after all possible filters.) + * You may override this method to do last-minute preparation for the action. + * @param Action $action the action to be executed. + * @return boolean whether the action should continue to be executed. + */ + public function beforeAction($action) + { + $properties = array(); + foreach (array('cache', 'duration', 'dependency', 'variations', 'enabled') as $name) { + $properties[$name] = $this->$name; + } + $id = $this->varyByRoute ? $action->getUniqueId() : __CLASS__; + return $this->view->beginCache($id, $properties); + } + + /** + * This method is invoked right after an action is executed. + * You may override this method to do some postprocessing for the action. + * @param Action $action the action just executed. + */ + public function afterAction($action) + { + $this->view->endCache(); + } } diff --git a/framework/yii/web/Request.php b/framework/yii/web/Request.php index a857926..6f5cdb5 100644 --- a/framework/yii/web/Request.php +++ b/framework/yii/web/Request.php @@ -10,6 +10,7 @@ namespace yii\web; use Yii; use yii\base\HttpException; use yii\base\InvalidConfigException; +use yii\helpers\SecurityHelper; /** * @author Qiang Xue @@ -37,19 +38,15 @@ class Request extends \yii\base\Request * @var array the configuration of the CSRF cookie. This property is used only when [[enableCsrfValidation]] is true. * @see Cookie */ - public $csrfCookie = array('httponly' => true); + public $csrfCookie = array('httpOnly' => true); /** * @var boolean whether cookies should be validated to ensure they are not tampered. Defaults to true. */ public $enableCookieValidation = true; /** - * @var string the secret key used for cookie validation. If not set, a random key will be generated and used. - */ - public $cookieValidationKey; - /** * @var string|boolean the name of the POST parameter that is used to indicate if a request is a PUT or DELETE * request tunneled through POST. Default to '_method'. - * @see getRequestMethod + * @see getMethod * @see getRestParams */ public $restVar = '_method'; @@ -81,7 +78,7 @@ class Request extends \yii\base\Request * @return string request method, such as GET, POST, HEAD, PUT, DELETE. * The value returned is turned into upper case. */ - public function getRequestMethod() + public function getMethod() { if (isset($_POST[$this->restVar])) { return strtoupper($_POST[$this->restVar]); @@ -94,34 +91,34 @@ class Request extends \yii\base\Request * Returns whether this is a POST request. * @return boolean whether this is a POST request. */ - public function getIsPostRequest() + public function getIsPost() { - return $this->getRequestMethod() === 'POST'; + return $this->getMethod() === 'POST'; } /** * Returns whether this is a DELETE request. * @return boolean whether this is a DELETE request. */ - public function getIsDeleteRequest() + public function getIsDelete() { - return $this->getRequestMethod() === 'DELETE'; + return $this->getMethod() === 'DELETE'; } /** * Returns whether this is a PUT request. * @return boolean whether this is a PUT request. */ - public function getIsPutRequest() + public function getIsPut() { - return $this->getRequestMethod() === 'PUT'; + return $this->getMethod() === 'PUT'; } /** * Returns whether this is an AJAX (XMLHttpRequest) request. * @return boolean whether this is an AJAX (XMLHttpRequest) request. */ - public function getIsAjaxRequest() + public function getIsAjax() { return isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest'; } @@ -130,7 +127,7 @@ class Request extends \yii\base\Request * Returns whether this is an Adobe Flash or Flex request. * @return boolean whether this is an Adobe Flash or Adobe Flex request. */ - public function getIsFlashRequest() + public function getIsFlash() { return isset($_SERVER['HTTP_USER_AGENT']) && (stripos($_SERVER['HTTP_USER_AGENT'], 'Shockwave') !== false || stripos($_SERVER['HTTP_USER_AGENT'], 'Flash') !== false); @@ -141,7 +138,7 @@ class Request extends \yii\base\Request /** * Returns the request parameters for the RESTful request. * @return array the RESTful request parameters - * @see getRequestMethod + * @see getMethod */ public function getRestParams() { @@ -203,7 +200,7 @@ class Request extends \yii\base\Request * @return mixed the GET parameter value * @see getPost */ - public function getParam($name, $defaultValue = null) + public function get($name, $defaultValue = null) { return isset($_GET[$name]) ? $_GET[$name] : $defaultValue; } @@ -229,7 +226,7 @@ class Request extends \yii\base\Request */ public function getDelete($name, $defaultValue = null) { - return $this->getIsDeleteRequest() ? $this->getRestParam($name, $defaultValue) : null; + return $this->getIsDelete() ? $this->getRestParam($name, $defaultValue) : null; } /** @@ -240,7 +237,7 @@ class Request extends \yii\base\Request */ public function getPut($name, $defaultValue = null) { - return $this->getIsPutRequest() ? $this->getRestParam($name, $defaultValue) : null; + return $this->getIsPut() ? $this->getRestParam($name, $defaultValue) : null; } private $_hostInfo; @@ -717,14 +714,64 @@ class Request extends \yii\base\Request public function getCookies() { if ($this->_cookies === null) { - $this->_cookies = new CookieCollection(array( - 'enableValidation' => $this->enableCookieValidation, - 'validationKey' => $this->cookieValidationKey, + $this->_cookies = new CookieCollection($this->loadCookies(), array( + 'readOnly' => true, )); } return $this->_cookies; } + /** + * Converts `$_COOKIE` into an array of [[Cookie]]. + * @return array the cookies obtained from request + */ + protected function loadCookies() + { + $cookies = array(); + if ($this->enableCookieValidation) { + $key = $this->getCookieValidationKey(); + foreach ($_COOKIE as $name => $value) { + if (is_string($value) && ($value = SecurityHelper::validateData($value, $key)) !== false) { + $cookies[$name] = new Cookie(array( + 'name' => $name, + 'value' => @unserialize($value), + )); + } + } + } else { + foreach ($_COOKIE as $name => $value) { + $cookies[$name] = new Cookie(array( + 'name' => $name, + 'value' => $value, + )); + } + } + return $cookies; + } + + private $_cookieValidationKey; + + /** + * @return string the secret key used for cookie validation. If it was set previously, + * a random key will be generated and used. + */ + public function getCookieValidationKey() + { + if ($this->_cookieValidationKey === null) { + $this->_cookieValidationKey = SecurityHelper::getSecretKey(__CLASS__ . '/' . Yii::$app->id); + } + return $this->_cookieValidationKey; + } + + /** + * Sets the secret key used for cookie validation. + * @param string $value the secret key used for cookie validation. + */ + public function setCookieValidationKey($value) + { + $this->_cookieValidationKey = $value; + } + private $_csrfToken; /** @@ -772,7 +819,7 @@ class Request extends \yii\base\Request if (!$this->enableCsrfValidation) { return; } - $method = $this->getRequestMethod(); + $method = $this->getMethod(); if ($method === 'POST' || $method === 'PUT' || $method === 'DELETE') { $cookies = $this->getCookies(); switch ($method) { @@ -792,4 +839,3 @@ class Request extends \yii\base\Request } } } - diff --git a/framework/yii/web/Response.php b/framework/yii/web/Response.php index d37c66a..f337fbb 100644 --- a/framework/yii/web/Response.php +++ b/framework/yii/web/Response.php @@ -9,8 +9,11 @@ namespace yii\web; use Yii; use yii\base\HttpException; +use yii\base\InvalidParamException; use yii\helpers\FileHelper; use yii\helpers\Html; +use yii\helpers\Json; +use yii\helpers\SecurityHelper; use yii\helpers\StringHelper; /** @@ -27,6 +30,237 @@ class Response extends \yii\base\Response * @see redirect */ public $ajaxRedirectCode = 278; + /** + * @var string + */ + public $content; + /** + * @var string + */ + public $statusText; + /** + * @var string the charset to use. If not set, [[\yii\base\Application::charset]] will be used. + */ + public $charset; + /** + * @var string the version of the HTTP protocol to use + */ + public $version = '1.0'; + + /** + * @var array list of HTTP status codes and the corresponding texts + */ + public static $statusTexts = 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', + 208 => 'Already Reported', + 210 => 'Content Different', + 226 => 'IM Used', + 300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', + 303 => 'See Other', + 304 => 'Not Modified', + 305 => 'Use Proxy', + 306 => 'Reserved', + 307 => 'Temporary Redirect', + 308 => 'Permanent 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', + 428 => 'Precondition Required', + 429 => 'Too Many Requests', + 431 => 'Request Header Fields Too Large', + 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', + 508 => 'Loop Detected', + 509 => 'Bandwidth Limit Exceeded', + 510 => 'Not Extended', + 511 => 'Network Authentication Required', + ); + + private $_statusCode; + /** + * @var HeaderCollection + */ + private $_headers; + + + public function init() + { + if ($this->charset === null) { + $this->charset = Yii::$app->charset; + } + } + + public function begin() + { + parent::begin(); + $this->beginOutput(); + } + + public function end() + { + $this->content .= $this->endOutput(); + $this->send(); + parent::end(); + } + + public function getStatusCode() + { + return $this->_statusCode; + } + + public function setStatusCode($value, $text = null) + { + $this->_statusCode = (int)$value; + if ($this->isInvalid()) { + throw new InvalidParamException("The HTTP status code is invalid: $value"); + } + if ($text === null) { + $this->statusText = isset(self::$statusTexts[$this->_statusCode]) ? self::$statusTexts[$this->_statusCode] : ''; + } else { + $this->statusText = $text; + } + } + + /** + * Returns the header collection. + * The header collection contains the currently registered HTTP headers. + * @return HeaderCollection the header collection + */ + public function getHeaders() + { + if ($this->_headers === null) { + $this->_headers = new HeaderCollection; + } + return $this->_headers; + } + + public function renderJson($data) + { + $this->getHeaders()->set('content-type', 'application/json'); + $this->content = Json::encode($data); + } + + public function renderJsonp($data, $callbackName) + { + $this->getHeaders()->set('content-type', 'text/javascript'); + $data = Json::encode($data); + $this->content = "$callbackName($data);"; + } + + /** + * Sends the response to the client. + * @return boolean true if the response was sent + */ + public function send() + { + $this->sendHeaders(); + $this->sendContent(); + } + + public function reset() + { + $this->_headers = null; + $this->_statusCode = null; + $this->statusText = null; + $this->content = null; + } + + /** + * Sends the response headers to the client + */ + protected function sendHeaders() + { + if (headers_sent()) { + return; + } + $statusCode = $this->getStatusCode(); + if ($statusCode !== null) { + header("HTTP/{$this->version} $statusCode {$this->statusText}"); + } + if ($this->_headers) { + $headers = $this->getHeaders(); + foreach ($headers as $name => $values) { + foreach ($values as $value) { + header("$name: $value", false); + } + } + $headers->removeAll(); + } + $this->sendCookies(); + } + + /** + * Sends the cookies to the client. + */ + protected function sendCookies() + { + if ($this->_cookies === null) { + return; + } + $request = Yii::$app->getRequest(); + if ($request->enableCookieValidation) { + $validationKey = $request->getCookieValidationKey(); + } + foreach ($this->getCookies() as $cookie) { + $value = $cookie->value; + if ($cookie->expire != 1 && isset($validationKey)) { + $value = SecurityHelper::hashData(serialize($value), $validationKey); + } + setcookie($cookie->name, $value, $cookie->expire, $cookie->path, $cookie->domain, $cookie->secure, $cookie->httpOnly); + } + $this->getCookies()->removeAll(); + } + + /** + * Sends the response content to the client + */ + protected function sendContent() + { + echo $this->content; + $this->content = null; + } /** * Sends a file to user. @@ -46,13 +280,15 @@ class Response extends \yii\base\Response $contentStart = 0; $contentEnd = $fileSize - 1; + $headers = $this->getHeaders(); + // tell the client that we accept range requests - header('Accept-Ranges: bytes'); + $headers->set('Accept-Ranges', 'bytes'); if (isset($_SERVER['HTTP_RANGE'])) { // client sent us a multibyte range, can not hold this one for now - if (strpos($_SERVER['HTTP_RANGE'],',') !== false) { - header("Content-Range: bytes $contentStart-$contentEnd/$fileSize"); + if (strpos($_SERVER['HTTP_RANGE'], ',') !== false) { + $headers->set('Content-Range', "bytes $contentStart-$contentEnd/$fileSize"); throw new HttpException(416, 'Requested Range Not Satisfiable'); } @@ -75,31 +311,32 @@ class Response extends \yii\base\Response * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html */ // End bytes can not be larger than $end. - $contentEnd = ($contentEnd > $fileSize) ? $fileSize -1 : $contentEnd; + $contentEnd = ($contentEnd > $fileSize) ? $fileSize - 1 : $contentEnd; // Validate the requested range and return an error if it's not correct. $wrongContentStart = ($contentStart > $contentEnd || $contentStart > $fileSize - 1 || $contentStart < 0); - if ($wrongContentStart) { - header("Content-Range: bytes $contentStart-$contentEnd/$fileSize"); + if ($wrongContentStart) { + $headers->set('Content-Range', "bytes $contentStart-$contentEnd/$fileSize"); throw new HttpException(416, 'Requested Range Not Satisfiable'); } - header('HTTP/1.1 206 Partial Content'); - header("Content-Range: bytes $contentStart-$contentEnd/$fileSize"); + $this->setStatusCode(206); + $headers->set('Content-Range', "bytes $contentStart-$contentEnd/$fileSize"); } else { - header('HTTP/1.1 200 OK'); + $this->setStatusCode(200); } $length = $contentEnd - $contentStart + 1; // Calculate new content length - header('Pragma: public'); - header('Expires: 0'); - header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); - header('Content-Type: ' . $mimeType); - header('Content-Length: ' . $length); - header('Content-Disposition: attachment; filename="' . $fileName . '"'); - header('Content-Transfer-Encoding: binary'); + $headers->set('Pragma', 'public') + ->set('Expires', '0') + ->set('Cache-Control', 'must-revalidate, post-check=0, pre-check=0') + ->set('Content-Type', $mimeType) + ->set('Content-Length', $length) + ->set('Content-Disposition', "attachment; filename=\"$fileName\"") + ->set('Content-Transfer-Encoding', 'binary'); + $content = StringHelper::substr($content, $contentStart, $length); if ($terminate) { @@ -108,10 +345,10 @@ class Response extends \yii\base\Response ob_start(); Yii::$app->end(0, false); ob_end_clean(); - echo $content; + $this->content = $content; exit(0); } else { - echo $content; + $this->content = $content; } } @@ -186,7 +423,7 @@ class Response extends \yii\base\Response } if (!isset($options['mimeType'])) { - if (($options['mimeType'] = CFileHelper::getMimeTypeByExtension($filePath)) === null) { + if (($options['mimeType'] = FileHelper::getMimeTypeByExtension($filePath)) === null) { $options['mimeType'] = 'text/plain'; } } @@ -195,16 +432,18 @@ class Response extends \yii\base\Response $options['xHeader'] = 'X-Sendfile'; } + $headers = $this->getHeaders(); + if ($options['mimeType'] !== null) { - header('Content-type: ' . $options['mimeType']); + $headers->set('Content-Type', $options['mimeType']); } - header('Content-Disposition: ' . $disposition . '; filename="' . $options['saveName'] . '"'); + $headers->set('Content-Disposition', "$disposition; filename=\"{$options['saveName']}\""); if (isset($options['addHeaders'])) { foreach ($options['addHeaders'] as $header => $value) { - header($header . ': ' . $value); + $headers->set($header, $value); } } - header(trim($options['xHeader']) . ': ' . $filePath); + $headers->set(trim($options['xHeader']), $filePath); if (!isset($options['terminate']) || $options['terminate']) { Yii::$app->end(); @@ -243,10 +482,11 @@ class Response extends \yii\base\Response if (strpos($url, '/') === 0 && strpos($url, '//') !== 0) { $url = Yii::$app->getRequest()->getHostInfo() . $url; } - if (Yii::$app->getRequest()->getIsAjaxRequest()) { + if (Yii::$app->getRequest()->getIsAjax()) { $statusCode = $this->ajaxRedirectCode; } - header('Location: ' . $url, true, $statusCode); + $this->getHeaders()->set('Location', $url); + $this->setStatusCode($statusCode); if ($terminate) { Yii::$app->end(); } @@ -265,6 +505,8 @@ class Response extends \yii\base\Response $this->redirect(Yii::$app->getRequest()->getUrl() . $anchor, $terminate); } + private $_cookies; + /** * Returns the cookie collection. * Through the returned cookie collection, you add or remove cookies as follows, @@ -286,6 +528,89 @@ class Response extends \yii\base\Response */ public function getCookies() { - return Yii::$app->getRequest()->getCookies(); + if ($this->_cookies === null) { + $this->_cookies = new CookieCollection; + } + return $this->_cookies; + } + + /** + * @return boolean whether this response has a valid [[statusCode]]. + */ + public function isInvalid() + { + return $this->getStatusCode() < 100 || $this->getStatusCode() >= 600; + } + + /** + * @return boolean whether this response is informational + */ + public function isInformational() + { + return $this->getStatusCode() >= 100 && $this->getStatusCode() < 200; + } + + /** + * @return boolean whether this response is successfully + */ + public function isSuccessful() + { + return $this->getStatusCode() >= 200 && $this->getStatusCode() < 300; + } + + /** + * @return boolean whether this response is a redirection + */ + public function isRedirection() + { + return $this->getStatusCode() >= 300 && $this->getStatusCode() < 400; + } + + /** + * @return boolean whether this response indicates a client error + */ + public function isClientError() + { + return $this->getStatusCode() >= 400 && $this->getStatusCode() < 500; + } + + /** + * @return boolean whether this response indicates a server error + */ + public function isServerError() + { + return $this->getStatusCode() >= 500 && $this->getStatusCode() < 600; + } + + /** + * @return boolean whether this response is OK + */ + public function isOk() + { + return 200 === $this->getStatusCode(); + } + + /** + * @return boolean whether this response indicates the current request is forbidden + */ + public function isForbidden() + { + return 403 === $this->getStatusCode(); + } + + /** + * @return boolean whether this response indicates the currently requested resource is not found + */ + public function isNotFound() + { + return 404 === $this->getStatusCode(); + } + + /** + * @return boolean whether this response is empty + */ + public function isEmpty() + { + return in_array($this->getStatusCode(), array(201, 204, 304)); } } diff --git a/framework/yii/web/Session.php b/framework/yii/web/Session.php index 1b48433..cf1fa21 100644 --- a/framework/yii/web/Session.php +++ b/framework/yii/web/Session.php @@ -63,7 +63,7 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co * @var array parameter-value pairs to override default session cookie parameters */ public $cookieParams = array( - 'httponly' => true + 'httpOnly' => true ); /** @@ -241,26 +241,31 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co */ public function getCookieParams() { - return session_get_cookie_params(); + $params = session_get_cookie_params(); + if (isset($params['httponly'])) { + $params['httpOnly'] = $params['httponly']; + unset($params['httponly']); + } + return $params; } /** * Sets the session cookie parameters. * The effect of this method only lasts for the duration of the script. * Call this method before the session starts. - * @param array $value cookie parameters, valid keys include: lifetime, path, domain, secure and httponly. + * @param array $value cookie parameters, valid keys include: `lifetime`, `path`, `domain`, `secure` and `httpOnly`. * @throws InvalidParamException if the parameters are incomplete. * @see http://us2.php.net/manual/en/function.session-set-cookie-params.php */ public function setCookieParams($value) { - $data = session_get_cookie_params(); + $data = $this->getCookieParams(); extract($data); extract($value); - if (isset($lifetime, $path, $domain, $secure, $httponly)) { - session_set_cookie_params($lifetime, $path, $domain, $secure, $httponly); + if (isset($lifetime, $path, $domain, $secure, $httpOnly)) { + session_set_cookie_params($lifetime, $path, $domain, $secure, $httpOnly); } else { - throw new InvalidParamException('Please make sure these parameters are provided: lifetime, path, domain, secure and httponly.'); + throw new InvalidParamException('Please make sure these parameters are provided: lifetime, path, domain, secure and httpOnly.'); } } diff --git a/framework/yii/web/UploadedFile.php b/framework/yii/web/UploadedFile.php index 6e685a3..a1cd735 100644 --- a/framework/yii/web/UploadedFile.php +++ b/framework/yii/web/UploadedFile.php @@ -7,7 +7,7 @@ namespace yii\web; -use yii\widgets\Html; +use yii\helpers\Html; /** * @author Qiang Xue diff --git a/framework/yii/web/UrlManager.php b/framework/yii/web/UrlManager.php index 47f5c5d..4478a6c 100644 --- a/framework/yii/web/UrlManager.php +++ b/framework/yii/web/UrlManager.php @@ -43,6 +43,31 @@ class UrlManager extends Component * array, one can use the key to represent the pattern and the value the corresponding route. * For example, `'post/' => 'post/view'`. * + * For RESTful routing the mentioned shortcut format also allows you to specify the + * [[UrlRule::verb|HTTP verb]] that the rule should apply for. + * You can do that by prepending it to the pattern, separated by space. + * For example, `'PUT post/' => 'post/update'`. + * You may specify multiple verbs by separating them with comma + * like this: `'POST,PUT post/index' => 'post/create'`. + * The supported verbs in the shortcut format are: GET, HEAD, POST, PUT and DELETE. + * Note that [[UrlRule::mode|mode]] will be set to PARSING_ONLY when specifying verb in this way + * so you normally would not specify a verb for normal GET request. + * + * Here is an example configuration for RESTful CRUD controller: + * + * ~~~php + * array( + * 'dashboard' => 'site/index', + * + * 'POST s' => '/create', + * 's' => '/index', + * + * 'PUT /' => '/update', + * 'DELETE /' => '/delete', + * '/' => '/view', + * ); + * ~~~ + * * Note that if you modify this property after the UrlManager object is created, make sure * you populate the array with rule objects instead of rule configurations. */ @@ -115,9 +140,14 @@ class UrlManager extends Component foreach ($this->rules as $key => $rule) { if (!is_array($rule)) { $rule = array( - 'pattern' => $key, 'route' => $rule, ); + if (preg_match('/^((?:(GET|HEAD|POST|PUT|DELETE),)*(GET|HEAD|POST|PUT|DELETE))\s+(.*)$/', $key, $matches)) { + $rule['verb'] = explode(',', $matches[1]); + $rule['mode'] = UrlRule::PARSING_ONLY; + $key = $matches[4]; + } + $rule['pattern'] = $key; } $rules[] = Yii::createObject(array_merge($this->ruleConfig, $rule)); } @@ -166,7 +196,7 @@ class UrlManager extends Component return array($pathInfo, array()); } else { - $route = $request->getParam($this->routeVar); + $route = $request->get($this->routeVar); if (is_array($route)) { $route = ''; } diff --git a/framework/yii/web/UrlRule.php b/framework/yii/web/UrlRule.php index b1e74da..fc19ea3 100644 --- a/framework/yii/web/UrlRule.php +++ b/framework/yii/web/UrlRule.php @@ -171,7 +171,7 @@ class UrlRule extends Object return false; } - if ($this->verb !== null && !in_array($request->getRequestMethod(), $this->verb, true)) { + if ($this->verb !== null && !in_array($request->getMethod(), $this->verb, true)) { return false; } diff --git a/framework/yii/web/User.php b/framework/yii/web/User.php index 79665ae..7ea561c 100644 --- a/framework/yii/web/User.php +++ b/framework/yii/web/User.php @@ -56,7 +56,7 @@ class User extends Component * @var array the configuration of the identity cookie. This property is used only when [[enableAutoLogin]] is true. * @see Cookie */ - public $identityCookie = array('name' => '_identity', 'httponly' => true); + public $identityCookie = array('name' => '_identity', 'httpOnly' => true); /** * @var integer the number of seconds in which the user will be logged out automatically if he * remains inactive. If this property is not set, the user will be logged out after @@ -221,7 +221,7 @@ class User extends Component if ($destroySession) { Yii::$app->getSession()->destroy(); } - $this->afterLogout($identity); + $this->afterLogout($identity); } } @@ -280,7 +280,7 @@ class User extends Component public function loginRequired() { $request = Yii::$app->getRequest(); - if (!$request->getIsAjaxRequest()) { + if (!$request->getIsAjax()) { $this->setReturnUrl($request->getUrl()); } if ($this->loginUrl !== null) { diff --git a/framework/yii/web/VerbFilter.php b/framework/yii/web/VerbFilter.php index 9b475e3..2b7567f 100644 --- a/framework/yii/web/VerbFilter.php +++ b/framework/yii/web/VerbFilter.php @@ -76,12 +76,12 @@ class VerbFilter extends Behavior { $action = $event->action->id; if (isset($this->actions[$action])) { - $verb = Yii::$app->getRequest()->getRequestMethod(); + $verb = Yii::$app->getRequest()->getMethod(); $allowed = array_map('strtoupper', $this->actions[$action]); if (!in_array($verb, $allowed)) { $event->isValid = false; // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.7 - header('Allow: ' . implode(', ', $allowed)); + Yii::$app->getResponse()->getHeaders()->set('Allow', implode(', ', $allowed)); throw new HttpException(405, 'Method Not Allowed. This url can only handle the following request methods: ' . implode(', ', $allowed)); } } diff --git a/framework/yii/widgets/FragmentCache.php b/framework/yii/widgets/FragmentCache.php index 8445955..0fd8646 100644 --- a/framework/yii/widgets/FragmentCache.php +++ b/framework/yii/widgets/FragmentCache.php @@ -1,174 +1,174 @@ - - * @since 2.0 - */ -class FragmentCache extends Widget -{ - /** - * @var Cache|string the cache object or the application component ID of the cache object. - * After the FragmentCache object is created, if you want to change this property, - * you should only assign it with a cache object. - */ - public $cache = 'cache'; - /** - * @var integer number of seconds that the data can remain valid in cache. - * Use 0 to indicate that the cached data will never expire. - */ - public $duration = 60; - /** - * @var array|Dependency the dependency that the cached content depends on. - * This can be either a [[Dependency]] object or a configuration array for creating the dependency object. - * For example, - * - * ~~~ - * array( - * 'class' => 'yii\caching\DbDependency', - * 'sql' => 'SELECT MAX(lastModified) FROM Post', - * ) - * ~~~ - * - * would make the output cache depends on the last modified time of all posts. - * If any post has its modification time changed, the cached content would be invalidated. - */ - public $dependency; - /** - * @var array list of factors that would cause the variation of the content being cached. - * Each factor is a string representing a variation (e.g. the language, a GET parameter). - * The following variation setting will cause the content to be cached in different versions - * according to the current application language: - * - * ~~~ - * array( - * Yii::$app->language, - * ) - */ - public $variations; - /** - * @var boolean whether to enable the fragment cache. You may use this property to turn on and off - * the fragment cache according to specific setting (e.g. enable fragment cache only for GET requests). - */ - public $enabled = true; - /** - * @var array a list of placeholders for embedding dynamic contents. This property - * is used internally to implement the content caching feature. Do not modify it. - */ - public $dynamicPlaceholders; - - /** - * Initializes the FragmentCache object. - */ - public function init() - { - parent::init(); - - if (!$this->enabled) { - $this->cache = null; - } elseif (is_string($this->cache)) { - $this->cache = Yii::$app->getComponent($this->cache); - } - - if ($this->getCachedContent() === false) { - $this->view->cacheStack[] = $this; - ob_start(); - ob_implicit_flush(false); - } - } - - /** - * Marks the end of content to be cached. - * Content displayed before this method call and after {@link init()} - * will be captured and saved in cache. - * This method does nothing if valid content is already found in cache. - */ - public function run() - { - if (($content = $this->getCachedContent()) !== false) { - echo $content; - } elseif ($this->cache instanceof Cache) { - $content = ob_get_clean(); - array_pop($this->view->cacheStack); - if (is_array($this->dependency)) { - $this->dependency = Yii::createObject($this->dependency); - } - $data = array($content, $this->dynamicPlaceholders); - $this->cache->set($this->calculateKey(), $data, $this->duration, $this->dependency); - - if (empty($this->view->cacheStack) && !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 (empty($this->view->cacheStack)) { - // 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 mixed 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 $factors; - } -} + + * @since 2.0 + */ +class FragmentCache extends Widget +{ + /** + * @var Cache|string the cache object or the application component ID of the cache object. + * After the FragmentCache object is created, if you want to change this property, + * you should only assign it with a cache object. + */ + public $cache = 'cache'; + /** + * @var integer number of seconds that the data can remain valid in cache. + * Use 0 to indicate that the cached data will never expire. + */ + public $duration = 60; + /** + * @var array|Dependency the dependency that the cached content depends on. + * This can be either a [[Dependency]] object or a configuration array for creating the dependency object. + * For example, + * + * ~~~ + * array( + * 'class' => 'yii\caching\DbDependency', + * 'sql' => 'SELECT MAX(lastModified) FROM Post', + * ) + * ~~~ + * + * would make the output cache depends on the last modified time of all posts. + * If any post has its modification time changed, the cached content would be invalidated. + */ + public $dependency; + /** + * @var array list of factors that would cause the variation of the content being cached. + * Each factor is a string representing a variation (e.g. the language, a GET parameter). + * The following variation setting will cause the content to be cached in different versions + * according to the current application language: + * + * ~~~ + * array( + * Yii::$app->language, + * ) + */ + public $variations; + /** + * @var boolean whether to enable the fragment cache. You may use this property to turn on and off + * the fragment cache according to specific setting (e.g. enable fragment cache only for GET requests). + */ + public $enabled = true; + /** + * @var array a list of placeholders for embedding dynamic contents. This property + * is used internally to implement the content caching feature. Do not modify it. + */ + public $dynamicPlaceholders; + + /** + * Initializes the FragmentCache object. + */ + public function init() + { + parent::init(); + + if (!$this->enabled) { + $this->cache = null; + } elseif (is_string($this->cache)) { + $this->cache = Yii::$app->getComponent($this->cache); + } + + if ($this->getCachedContent() === false) { + $this->view->cacheStack[] = $this; + ob_start(); + ob_implicit_flush(false); + } + } + + /** + * Marks the end of content to be cached. + * Content displayed before this method call and after {@link init()} + * will be captured and saved in cache. + * This method does nothing if valid content is already found in cache. + */ + public function run() + { + if (($content = $this->getCachedContent()) !== false) { + echo $content; + } elseif ($this->cache instanceof Cache) { + $content = ob_get_clean(); + array_pop($this->view->cacheStack); + if (is_array($this->dependency)) { + $this->dependency = Yii::createObject($this->dependency); + } + $data = array($content, $this->dynamicPlaceholders); + $this->cache->set($this->calculateKey(), $data, $this->duration, $this->dependency); + + if (empty($this->view->cacheStack) && !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 (empty($this->view->cacheStack)) { + // 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 mixed 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 $factors; + } +} diff --git a/framework/yii/widgets/LinkPager.php b/framework/yii/widgets/LinkPager.php index 2510579..62d99f6 100644 --- a/framework/yii/widgets/LinkPager.php +++ b/framework/yii/widgets/LinkPager.php @@ -11,7 +11,7 @@ use Yii; use yii\base\InvalidConfigException; use yii\helpers\Html; use yii\base\Widget; -use yii\web\Pagination; +use yii\data\Pagination; /** * LinkPager displays a list of hyperlinks that lead to different pages of target. @@ -198,4 +198,4 @@ class LinkPager extends Widget } return array($beginPage, $endPage); } -} \ No newline at end of file +} diff --git a/framework/yii/widgets/ListPager.php b/framework/yii/widgets/ListPager.php index 7b16f7d..30371d3 100644 --- a/framework/yii/widgets/ListPager.php +++ b/framework/yii/widgets/ListPager.php @@ -10,7 +10,7 @@ namespace yii\widgets; use yii\base\InvalidConfigException; use yii\helpers\Html; use yii\base\Widget; -use yii\web\Pagination; +use yii\data\Pagination; /** * ListPager displays a drop-down list that contains options leading to different pages. @@ -91,5 +91,4 @@ class ListPager extends Widget '{page}' => $page + 1, )); } - -} \ No newline at end of file +} diff --git a/tests/unit/TestCase.php b/tests/unit/TestCase.php index 479f85d..efcedf0 100644 --- a/tests/unit/TestCase.php +++ b/tests/unit/TestCase.php @@ -38,14 +38,13 @@ abstract class TestCase extends \yii\test\TestCase * The application will be destroyed on tearDown() automatically. * @param array $config The application configuration, if needed */ - protected function mockApplication($config=array()) + protected function mockApplication($config = array(), $appClass = '\yii\console\Application') { static $defaultConfig = array( 'id' => 'testapp', 'basePath' => __DIR__, ); - $appClass = $this->getParam( 'appClass', '\yii\web\Application' ); new $appClass(array_merge($defaultConfig,$config)); } diff --git a/tests/unit/data/config.php b/tests/unit/data/config.php index 88c8127..036624b 100644 --- a/tests/unit/data/config.php +++ b/tests/unit/data/config.php @@ -1,8 +1,6 @@ '\yii\web\Application', - 'appClass' => '\yii\console\Application', 'databases' => array( 'mysql' => array( 'dsn' => 'mysql:host=127.0.0.1;dbname=yiitest', @@ -20,5 +18,11 @@ return array( 'password' => '', 'fixture' => __DIR__ . '/mssql.sql', ), + 'pgsql' => array( + 'dsn' => 'pgsql:host=localhost;dbname=yiitest;port=5432;', + 'username' => 'postgres', + 'password' => 'postgres', + 'fixture' => __DIR__ . '/postgres.sql', + ) ) ); diff --git a/tests/unit/data/postgres.sql b/tests/unit/data/postgres.sql index e46b284..52fad0f 100644 --- a/tests/unit/data/postgres.sql +++ b/tests/unit/data/postgres.sql @@ -1,165 +1,87 @@ /** * This is the database schema for testing PostgreSQL support of yii Active Record. - * To test this feature, you need to create a database named 'yii' on 'localhost' - * and create an account 'test/test' which owns this test database. + * To test this feature, you need to create a database named 'yiitest' on 'localhost' + * and create an account 'postgres/postgres' which owns this test database. */ -CREATE SCHEMA test; -CREATE TABLE test.users -( - id SERIAL NOT NULL PRIMARY KEY, - username VARCHAR(128) NOT NULL, - password VARCHAR(128) NOT NULL, - email VARCHAR(128) NOT NULL +DROP TABLE IF EXISTS tbl_order_item CASCADE; +DROP TABLE IF EXISTS tbl_item CASCADE; +DROP TABLE IF EXISTS tbl_order CASCADE; +DROP TABLE IF EXISTS tbl_category CASCADE; +DROP TABLE IF EXISTS tbl_customer CASCADE; +DROP TABLE IF EXISTS tbl_type CASCADE; + +CREATE TABLE tbl_customer ( + id serial not null primary key, + email varchar(128) NOT NULL, + name varchar(128) NOT NULL, + address text, + status integer DEFAULT 0 ); -INSERT INTO test.users (username, password, email) VALUES ('user1','pass1','email1'); -INSERT INTO test.users (username, password, email) VALUES ('user2','pass2','email2'); -INSERT INTO test.users (username, password, email) VALUES ('user3','pass3','email3'); +comment on column public.tbl_customer.email is 'someone@example.com'; -CREATE TABLE test.user_friends -( - id INTEGER NOT NULL, - friend INTEGER NOT NULL, - PRIMARY KEY (id, friend), - CONSTRAINT FK_user_id FOREIGN KEY (id) - REFERENCES test.users (id) ON DELETE CASCADE ON UPDATE RESTRICT, - CONSTRAINT FK_friend_id FOREIGN KEY (friend) - REFERENCES test.users (id) ON DELETE CASCADE ON UPDATE RESTRICT +CREATE TABLE tbl_category ( + id serial not null primary key, + name varchar(128) NOT NULL ); -INSERT INTO test.user_friends VALUES (1,2); -INSERT INTO test.user_friends VALUES (1,3); -INSERT INTO test.user_friends VALUES (2,3); - -CREATE TABLE test.profiles -( - id SERIAL NOT NULL PRIMARY KEY, - first_name VARCHAR(128) NOT NULL, - last_name VARCHAR(128) NOT NULL, - user_id INTEGER NOT NULL, - CONSTRAINT FK_profile_user FOREIGN KEY (user_id) - REFERENCES test.users (id) ON DELETE CASCADE ON UPDATE RESTRICT -); - -INSERT INTO test.profiles (first_name, last_name, user_id) VALUES ('first 1','last 1',1); -INSERT INTO test.profiles (first_name, last_name, user_id) VALUES ('first 2','last 2',2); - -CREATE TABLE test.posts -( - id SERIAL NOT NULL PRIMARY KEY, - title VARCHAR(128) NOT NULL, - create_time TIMESTAMP NOT NULL, - author_id INTEGER NOT NULL, - content TEXT, - CONSTRAINT FK_post_author FOREIGN KEY (author_id) - REFERENCES test.users (id) ON DELETE CASCADE ON UPDATE RESTRICT -); - -INSERT INTO test.posts (title, create_time, author_id, content) VALUES ('post 1',TIMESTAMP '2004-10-19 10:23:54',1,'content 1'); -INSERT INTO test.posts (title, create_time, author_id, content) VALUES ('post 2',TIMESTAMP '2004-10-19 10:23:54',2,'content 2'); -INSERT INTO test.posts (title, create_time, author_id, content) VALUES ('post 3',TIMESTAMP '2004-10-19 10:23:54',2,'content 3'); -INSERT INTO test.posts (title, create_time, author_id, content) VALUES ('post 4',TIMESTAMP '2004-10-19 10:23:54',2,'content 4'); -INSERT INTO test.posts (title, create_time, author_id, content) VALUES ('post 5',TIMESTAMP '2004-10-19 10:23:54',3,'content 5'); - -CREATE TABLE test.comments -( - id SERIAL NOT NULL PRIMARY KEY, - content TEXT NOT NULL, - post_id INTEGER NOT NULL, - author_id INTEGER NOT NULL, - CONSTRAINT FK_post_comment FOREIGN KEY (post_id) - REFERENCES test.posts (id) ON DELETE CASCADE ON UPDATE RESTRICT, - CONSTRAINT FK_user_comment FOREIGN KEY (author_id) - REFERENCES test.users (id) ON DELETE CASCADE ON UPDATE RESTRICT +CREATE TABLE tbl_item ( + id serial not null primary key, + name varchar(128) NOT NULL, + category_id integer NOT NULL references tbl_category(id) on UPDATE CASCADE on DELETE CASCADE ); -INSERT INTO test.comments (content, post_id, author_id) VALUES ('comment 1',1, 2); -INSERT INTO test.comments (content, post_id, author_id) VALUES ('comment 2',1, 2); -INSERT INTO test.comments (content, post_id, author_id) VALUES ('comment 3',1, 2); -INSERT INTO test.comments (content, post_id, author_id) VALUES ('comment 4',2, 2); -INSERT INTO test.comments (content, post_id, author_id) VALUES ('comment 5',2, 2); -INSERT INTO test.comments (content, post_id, author_id) VALUES ('comment 6',3, 2); -INSERT INTO test.comments (content, post_id, author_id) VALUES ('comment 7',3, 2); -INSERT INTO test.comments (content, post_id, author_id) VALUES ('comment 8',3, 2); -INSERT INTO test.comments (content, post_id, author_id) VALUES ('comment 9',3, 2); -INSERT INTO test.comments (content, post_id, author_id) VALUES ('comment 10',5, 3); - -CREATE TABLE test.categories -( - id SERIAL NOT NULL PRIMARY KEY, - name VARCHAR(128) NOT NULL, - parent_id INTEGER, - CONSTRAINT FK_category_category FOREIGN KEY (parent_id) - REFERENCES test.categories (id) ON DELETE CASCADE ON UPDATE RESTRICT +CREATE TABLE tbl_order ( + id serial not null primary key, + customer_id integer NOT NULL references tbl_customer(id) on UPDATE CASCADE on DELETE CASCADE, + create_time integer NOT NULL, + total decimal(10,0) NOT NULL ); -INSERT INTO test.categories (name, parent_id) VALUES ('cat 1',NULL); -INSERT INTO test.categories (name, parent_id) VALUES ('cat 2',NULL); -INSERT INTO test.categories (name, parent_id) VALUES ('cat 3',NULL); -INSERT INTO test.categories (name, parent_id) VALUES ('cat 4',1); -INSERT INTO test.categories (name, parent_id) VALUES ('cat 5',1); -INSERT INTO test.categories (name, parent_id) VALUES ('cat 6',5); -INSERT INTO test.categories (name, parent_id) VALUES ('cat 7',5); - -CREATE TABLE test.post_category -( - category_id INTEGER NOT NULL, - post_id INTEGER NOT NULL, - PRIMARY KEY (category_id, post_id), - CONSTRAINT FK_post_category_post FOREIGN KEY (post_id) - REFERENCES test.posts (id) ON DELETE CASCADE ON UPDATE RESTRICT, - CONSTRAINT FK_post_category_category FOREIGN KEY (category_id) - REFERENCES test.categories (id) ON DELETE CASCADE ON UPDATE RESTRICT +CREATE TABLE tbl_order_item ( + order_id integer NOT NULL references tbl_order(id) on UPDATE CASCADE on DELETE CASCADE, + item_id integer NOT NULL references tbl_item(id) on UPDATE CASCADE on DELETE CASCADE, + quantity integer NOT NULL, + subtotal decimal(10,0) NOT NULL, + PRIMARY KEY (order_id,item_id) ); -INSERT INTO test.post_category (category_id, post_id) VALUES (1,1); -INSERT INTO test.post_category (category_id, post_id) VALUES (2,1); -INSERT INTO test.post_category (category_id, post_id) VALUES (3,1); -INSERT INTO test.post_category (category_id, post_id) VALUES (4,2); -INSERT INTO test.post_category (category_id, post_id) VALUES (1,2); -INSERT INTO test.post_category (category_id, post_id) VALUES (1,3); - -CREATE TABLE test.orders -( - key1 INTEGER NOT NULL, - key2 INTEGER NOT NULL, - name VARCHAR(128), - PRIMARY KEY (key1, key2) +CREATE TABLE tbl_type ( + int_col integer NOT NULL, + int_col2 integer DEFAULT '1', + char_col char(100) NOT NULL, + char_col2 varchar(100) DEFAULT 'something', + char_col3 text, + float_col double precision NOT NULL, + float_col2 double precision DEFAULT '1.23', + blob_col bytea, + numeric_col decimal(5,2) DEFAULT '33.22', + time timestamp NOT NULL DEFAULT '2002-01-01 00:00:00', + bool_col smallint NOT NULL, + bool_col2 smallint DEFAULT '1' ); -INSERT INTO test.orders (key1,key2,name) VALUES (1,2,'order 12'); -INSERT INTO test.orders (key1,key2,name) VALUES (1,3,'order 13'); -INSERT INTO test.orders (key1,key2,name) VALUES (2,1,'order 21'); -INSERT INTO test.orders (key1,key2,name) VALUES (2,2,'order 22'); - -CREATE TABLE test.items -( - id SERIAL NOT NULL PRIMARY KEY, - name VARCHAR(128), - col1 INTEGER NOT NULL, - col2 INTEGER NOT NULL, - CONSTRAINT FK_order_item FOREIGN KEY (col1,col2) - REFERENCES test.orders (key1,key2) ON DELETE CASCADE ON UPDATE RESTRICT -); - -INSERT INTO test.items (name,col1,col2) VALUES ('item 1',1,2); -INSERT INTO test.items (name,col1,col2) VALUES ('item 2',1,2); -INSERT INTO test.items (name,col1,col2) VALUES ('item 3',1,3); -INSERT INTO test.items (name,col1,col2) VALUES ('item 4',2,2); -INSERT INTO test.items (name,col1,col2) VALUES ('item 5',2,2); - -CREATE TABLE public.yii_types -( - int_col INT NOT NULL, - int_col2 INTEGER DEFAULT 1, - char_col CHAR(100) NOT NULL, - char_col2 VARCHAR(100) DEFAULT 'something', - char_col3 TEXT, - numeric_col NUMERIC(4,3) NOT NULL, - real_col REAL DEFAULT 1.23, - blob_col BYTEA, - time TIMESTAMP, - bool_col BOOL NOT NULL, - bool_col2 BOOLEAN DEFAULT TRUE -); \ No newline at end of file +INSERT INTO tbl_customer (email, name, address, status) VALUES ('user1@example.com', 'user1', 'address1', 1); +INSERT INTO tbl_customer (email, name, address, status) VALUES ('user2@example.com', 'user2', 'address2', 1); +INSERT INTO tbl_customer (email, name, address, status) VALUES ('user3@example.com', 'user3', 'address3', 2); + +INSERT INTO tbl_category (name) VALUES ('Books'); +INSERT INTO tbl_category (name) VALUES ('Movies'); + +INSERT INTO tbl_item (name, category_id) VALUES ('Agile Web Application Development with Yii1.1 and PHP5', 1); +INSERT INTO tbl_item (name, category_id) VALUES ('Yii 1.1 Application Development Cookbook', 1); +INSERT INTO tbl_item (name, category_id) VALUES ('Ice Age', 2); +INSERT INTO tbl_item (name, category_id) VALUES ('Toy Story', 2); +INSERT INTO tbl_item (name, category_id) VALUES ('Cars', 2); + +INSERT INTO tbl_order (customer_id, create_time, total) VALUES (1, 1325282384, 110.0); +INSERT INTO tbl_order (customer_id, create_time, total) VALUES (2, 1325334482, 33.0); +INSERT INTO tbl_order (customer_id, create_time, total) VALUES (2, 1325502201, 40.0); + +INSERT INTO tbl_order_item (order_id, item_id, quantity, subtotal) VALUES (1, 1, 1, 30.0); +INSERT INTO tbl_order_item (order_id, item_id, quantity, subtotal) VALUES (1, 2, 2, 40.0); +INSERT INTO tbl_order_item (order_id, item_id, quantity, subtotal) VALUES (2, 4, 1, 10.0); +INSERT INTO tbl_order_item (order_id, item_id, quantity, subtotal) VALUES (2, 5, 1, 15.0); +INSERT INTO tbl_order_item (order_id, item_id, quantity, subtotal) VALUES (2, 3, 1, 8.0); +INSERT INTO tbl_order_item (order_id, item_id, quantity, subtotal) VALUES (3, 2, 1, 40.0); diff --git a/tests/unit/framework/console/controllers/AssetControllerTest.php b/tests/unit/framework/console/controllers/AssetControllerTest.php index db6d2a7..9d7dd28 100644 --- a/tests/unit/framework/console/controllers/AssetControllerTest.php +++ b/tests/unit/framework/console/controllers/AssetControllerTest.php @@ -239,6 +239,7 @@ class AssetControllerTest extends TestCase // Then : $this->assertTrue(file_exists($bundleFile), 'Unable to create output bundle file!'); + $this->assertTrue(is_array(require($bundleFile)), 'Output bundle file has incorrect format!'); $compressedCssFileName = $this->testAssetsBasePath . DIRECTORY_SEPARATOR . 'all.css'; $this->assertTrue(file_exists($compressedCssFileName), 'Unable to compress CSS files!'); diff --git a/tests/unit/framework/db/DatabaseTestCase.php b/tests/unit/framework/db/DatabaseTestCase.php index 429d961..7ce9bec 100644 --- a/tests/unit/framework/db/DatabaseTestCase.php +++ b/tests/unit/framework/db/DatabaseTestCase.php @@ -37,6 +37,9 @@ abstract class DatabaseTestCase extends TestCase $db->username = $this->database['username']; $db->password = $this->database['password']; } + if (isset($this->database['attributes'])) { + $db->attributes = $this->database['attributes']; + } if ($open) { $db->open(); $lines = explode(';', file_get_contents($this->database['fixture'])); diff --git a/tests/unit/framework/db/QueryBuilderTest.php b/tests/unit/framework/db/QueryBuilderTest.php new file mode 100644 index 0000000..7dc4731 --- /dev/null +++ b/tests/unit/framework/db/QueryBuilderTest.php @@ -0,0 +1,109 @@ +driverName) + { + case 'mysql': + return new MysqlQueryBuilder($this->getConnection()); + case 'sqlite': + return new SqliteQueryBuilder($this->getConnection()); + case 'mssql': + return new MssqlQueryBuilder($this->getConnection()); + } + throw new \Exception('Test is not implemented for ' . $this->driverName); + } + + /** + * this is not used as a dataprovider for testGetColumnType to speed up the test + * when used as dataprovider every single line will cause a reconnect with the database which is not needed here + */ + public function columnTypes() + { + return array( + array(Schema::TYPE_PK, 'int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY'), + array(Schema::TYPE_PK . '(8)', 'int(8) NOT NULL AUTO_INCREMENT PRIMARY KEY'), + array(Schema::TYPE_PK . ' CHECK (value > 5)', 'int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY CHECK (value > 5)'), + array(Schema::TYPE_PK . '(8) CHECK (value > 5)', 'int(8) NOT NULL AUTO_INCREMENT PRIMARY KEY CHECK (value > 5)'), + array(Schema::TYPE_STRING, 'varchar(255)'), + array(Schema::TYPE_STRING . '(32)', 'varchar(32)'), + array(Schema::TYPE_STRING . ' CHECK (value LIKE "test%")', 'varchar(255) CHECK (value LIKE "test%")'), + array(Schema::TYPE_STRING . '(32) CHECK (value LIKE "test%")', 'varchar(32) CHECK (value LIKE "test%")'), + array(Schema::TYPE_STRING . ' NOT NULL', 'varchar(255) NOT NULL'), + array(Schema::TYPE_TEXT, 'text'), + array(Schema::TYPE_TEXT . '(255)', 'text'), + array(Schema::TYPE_TEXT . ' CHECK (value LIKE "test%")', 'text CHECK (value LIKE "test%")'), + array(Schema::TYPE_TEXT . '(255) CHECK (value LIKE "test%")', 'text CHECK (value LIKE "test%")'), + array(Schema::TYPE_TEXT . ' NOT NULL', 'text NOT NULL'), + array(Schema::TYPE_TEXT . '(255) NOT NULL', 'text NOT NULL'), + array(Schema::TYPE_SMALLINT, 'smallint(6)'), + array(Schema::TYPE_SMALLINT . '(8)', 'smallint(8)'), + array(Schema::TYPE_INTEGER, 'int(11)'), + array(Schema::TYPE_INTEGER . '(8)', 'int(8)'), + array(Schema::TYPE_INTEGER . ' CHECK (value > 5)', 'int(11) CHECK (value > 5)'), + array(Schema::TYPE_INTEGER . '(8) CHECK (value > 5)', 'int(8) CHECK (value > 5)'), + array(Schema::TYPE_INTEGER . ' NOT NULL', 'int(11) NOT NULL'), + array(Schema::TYPE_BIGINT, 'bigint(20)'), + array(Schema::TYPE_BIGINT . '(8)', 'bigint(8)'), + array(Schema::TYPE_BIGINT . ' CHECK (value > 5)', 'bigint(20) CHECK (value > 5)'), + array(Schema::TYPE_BIGINT . '(8) CHECK (value > 5)', 'bigint(8) CHECK (value > 5)'), + array(Schema::TYPE_BIGINT . ' NOT NULL', 'bigint(20) NOT NULL'), + array(Schema::TYPE_FLOAT, 'float'), + array(Schema::TYPE_FLOAT . '(16,5)', 'float'), + array(Schema::TYPE_FLOAT . ' CHECK (value > 5.6)', 'float CHECK (value > 5.6)'), + array(Schema::TYPE_FLOAT . '(16,5) CHECK (value > 5.6)', 'float CHECK (value > 5.6)'), + array(Schema::TYPE_FLOAT . ' NOT NULL', 'float NOT NULL'), + array(Schema::TYPE_DECIMAL, 'decimal(10,0)'), + array(Schema::TYPE_DECIMAL . '(12,4)', 'decimal(12,4)'), + array(Schema::TYPE_DECIMAL . ' CHECK (value > 5.6)', 'decimal(10,0) CHECK (value > 5.6)'), + array(Schema::TYPE_DECIMAL . '(12,4) CHECK (value > 5.6)', 'decimal(12,4) CHECK (value > 5.6)'), + array(Schema::TYPE_DECIMAL . ' NOT NULL', 'decimal(10,0) NOT NULL'), + array(Schema::TYPE_DATETIME, 'datetime'), + array(Schema::TYPE_DATETIME . " CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')", "datetime CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')"), + array(Schema::TYPE_DATETIME . ' NOT NULL', 'datetime NOT NULL'), + array(Schema::TYPE_TIMESTAMP, 'timestamp'), + array(Schema::TYPE_TIMESTAMP . " CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')", "timestamp CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')"), + array(Schema::TYPE_TIMESTAMP . ' NOT NULL', 'timestamp NOT NULL'), + array(Schema::TYPE_TIME, 'time'), + array(Schema::TYPE_TIME . " CHECK(value BETWEEN '12:00:00' AND '13:01:01')", "time CHECK(value BETWEEN '12:00:00' AND '13:01:01')"), + array(Schema::TYPE_TIME . ' NOT NULL', 'time NOT NULL'), + array(Schema::TYPE_DATE, 'date'), + array(Schema::TYPE_DATE . " CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')", "date CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')"), + array(Schema::TYPE_DATE . ' NOT NULL', 'date NOT NULL'), + array(Schema::TYPE_BINARY, 'blob'), + array(Schema::TYPE_BOOLEAN, 'tinyint(1)'), + array(Schema::TYPE_BOOLEAN . ' NOT NULL DEFAULT 1', 'tinyint(1) NOT NULL DEFAULT 1'), + array(Schema::TYPE_MONEY, 'decimal(19,4)'), + array(Schema::TYPE_MONEY . '(16,2)', 'decimal(16,2)'), + array(Schema::TYPE_MONEY . ' CHECK (value > 0.0)', 'decimal(19,4) CHECK (value > 0.0)'), + array(Schema::TYPE_MONEY . '(16,2) CHECK (value > 0.0)', 'decimal(16,2) CHECK (value > 0.0)'), + array(Schema::TYPE_MONEY . ' NOT NULL', 'decimal(19,4) NOT NULL'), + ); + } + + /** + * + */ + public function testGetColumnType() + { + $qb = $this->getQueryBuilder(); + foreach($this->columnTypes() as $item) { + list ($column, $expected) = $item; + $this->assertEquals($expected, $qb->getColumnType($column)); + } + } +} diff --git a/tests/unit/framework/db/pgsql/PostgreSQLActiveRecordTest.php b/tests/unit/framework/db/pgsql/PostgreSQLActiveRecordTest.php new file mode 100644 index 0000000..18bfbd8 --- /dev/null +++ b/tests/unit/framework/db/pgsql/PostgreSQLActiveRecordTest.php @@ -0,0 +1,14 @@ +driverName = 'pgsql'; + parent::setUp(); + } +} diff --git a/tests/unit/framework/db/pgsql/PostgreSQLConnectionTest.php b/tests/unit/framework/db/pgsql/PostgreSQLConnectionTest.php new file mode 100644 index 0000000..678b197 --- /dev/null +++ b/tests/unit/framework/db/pgsql/PostgreSQLConnectionTest.php @@ -0,0 +1,51 @@ +driverName = 'pgsql'; + parent::setUp(); + } + + public function testConnection() { + $connection = $this->getConnection(true); + } + + function testQuoteValue() { + $connection = $this->getConnection(false); + $this->assertEquals(123, $connection->quoteValue(123)); + $this->assertEquals("'string'", $connection->quoteValue('string')); + $this->assertEquals("'It''s interesting'", $connection->quoteValue("It's interesting")); + } + + function testQuoteTableName() + { + $connection = $this->getConnection(false); + $this->assertEquals('"table"', $connection->quoteTableName('table')); + $this->assertEquals('"table"', $connection->quoteTableName('"table"')); + $this->assertEquals('"schema"."table"', $connection->quoteTableName('schema.table')); + $this->assertEquals('"schema"."table"', $connection->quoteTableName('schema."table"')); + $this->assertEquals('"schema"."table"', $connection->quoteTableName('"schema"."table"')); + $this->assertEquals('{{table}}', $connection->quoteTableName('{{table}}')); + $this->assertEquals('(table)', $connection->quoteTableName('(table)')); + } + + function testQuoteColumnName() + { + $connection = $this->getConnection(false); + $this->assertEquals('"column"', $connection->quoteColumnName('column')); + $this->assertEquals('"column"', $connection->quoteColumnName('"column"')); + $this->assertEquals('"table"."column"', $connection->quoteColumnName('table.column')); + $this->assertEquals('"table"."column"', $connection->quoteColumnName('table."column"')); + $this->assertEquals('"table"."column"', $connection->quoteColumnName('"table"."column"')); + $this->assertEquals('[[column]]', $connection->quoteColumnName('[[column]]')); + $this->assertEquals('{{column}}', $connection->quoteColumnName('{{column}}')); + $this->assertEquals('(column)', $connection->quoteColumnName('(column)')); + } + + +} diff --git a/tests/unit/framework/db/sqlite/SqliteQueryBuilderTest.php b/tests/unit/framework/db/sqlite/SqliteQueryBuilderTest.php new file mode 100644 index 0000000..c36628f --- /dev/null +++ b/tests/unit/framework/db/sqlite/SqliteQueryBuilderTest.php @@ -0,0 +1,74 @@ + 5)', 'integer PRIMARY KEY AUTOINCREMENT NOT NULL CHECK (value > 5)'), + array(Schema::TYPE_PK . '(8) CHECK (value > 5)', 'integer PRIMARY KEY AUTOINCREMENT NOT NULL CHECK (value > 5)'), + array(Schema::TYPE_STRING, 'varchar(255)'), + array(Schema::TYPE_STRING . '(32)', 'varchar(32)'), + array(Schema::TYPE_STRING . ' CHECK (value LIKE "test%")', 'varchar(255) CHECK (value LIKE "test%")'), + array(Schema::TYPE_STRING . '(32) CHECK (value LIKE "test%")', 'varchar(32) CHECK (value LIKE "test%")'), + array(Schema::TYPE_STRING . ' NOT NULL', 'varchar(255) NOT NULL'), + array(Schema::TYPE_TEXT, 'text'), + array(Schema::TYPE_TEXT . '(255)', 'text'), + array(Schema::TYPE_TEXT . ' CHECK (value LIKE "test%")', 'text CHECK (value LIKE "test%")'), + array(Schema::TYPE_TEXT . '(255) CHECK (value LIKE "test%")', 'text CHECK (value LIKE "test%")'), + array(Schema::TYPE_TEXT . ' NOT NULL', 'text NOT NULL'), + array(Schema::TYPE_TEXT . '(255) NOT NULL', 'text NOT NULL'), + array(Schema::TYPE_SMALLINT, 'smallint'), + array(Schema::TYPE_SMALLINT . '(8)', 'smallint'), + array(Schema::TYPE_INTEGER, 'integer'), + array(Schema::TYPE_INTEGER . '(8)', 'integer'), + array(Schema::TYPE_INTEGER . ' CHECK (value > 5)', 'integer CHECK (value > 5)'), + array(Schema::TYPE_INTEGER . '(8) CHECK (value > 5)', 'integer CHECK (value > 5)'), + array(Schema::TYPE_INTEGER . ' NOT NULL', 'integer NOT NULL'), + array(Schema::TYPE_BIGINT, 'bigint'), + array(Schema::TYPE_BIGINT . '(8)', 'bigint'), + array(Schema::TYPE_BIGINT . ' CHECK (value > 5)', 'bigint CHECK (value > 5)'), + array(Schema::TYPE_BIGINT . '(8) CHECK (value > 5)', 'bigint CHECK (value > 5)'), + array(Schema::TYPE_BIGINT . ' NOT NULL', 'bigint NOT NULL'), + array(Schema::TYPE_FLOAT, 'float'), + array(Schema::TYPE_FLOAT . '(16,5)', 'float'), + array(Schema::TYPE_FLOAT . ' CHECK (value > 5.6)', 'float CHECK (value > 5.6)'), + array(Schema::TYPE_FLOAT . '(16,5) CHECK (value > 5.6)', 'float CHECK (value > 5.6)'), + array(Schema::TYPE_FLOAT . ' NOT NULL', 'float NOT NULL'), + array(Schema::TYPE_DECIMAL, 'decimal(10,0)'), + array(Schema::TYPE_DECIMAL . '(12,4)', 'decimal(12,4)'), + array(Schema::TYPE_DECIMAL . ' CHECK (value > 5.6)', 'decimal(10,0) CHECK (value > 5.6)'), + array(Schema::TYPE_DECIMAL . '(12,4) CHECK (value > 5.6)', 'decimal(12,4) CHECK (value > 5.6)'), + array(Schema::TYPE_DECIMAL . ' NOT NULL', 'decimal(10,0) NOT NULL'), + array(Schema::TYPE_DATETIME, 'datetime'), + array(Schema::TYPE_DATETIME . " CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')", "datetime CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')"), + array(Schema::TYPE_DATETIME . ' NOT NULL', 'datetime NOT NULL'), + array(Schema::TYPE_TIMESTAMP, 'timestamp'), + array(Schema::TYPE_TIMESTAMP . " CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')", "timestamp CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')"), + array(Schema::TYPE_TIMESTAMP . ' NOT NULL', 'timestamp NOT NULL'), + array(Schema::TYPE_TIME, 'time'), + array(Schema::TYPE_TIME . " CHECK(value BETWEEN '12:00:00' AND '13:01:01')", "time CHECK(value BETWEEN '12:00:00' AND '13:01:01')"), + array(Schema::TYPE_TIME . ' NOT NULL', 'time NOT NULL'), + array(Schema::TYPE_DATE, 'date'), + array(Schema::TYPE_DATE . " CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')", "date CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')"), + array(Schema::TYPE_DATE . ' NOT NULL', 'date NOT NULL'), + array(Schema::TYPE_BINARY, 'blob'), + array(Schema::TYPE_BOOLEAN, 'boolean'), + array(Schema::TYPE_BOOLEAN . ' NOT NULL DEFAULT 1', 'boolean NOT NULL DEFAULT 1'), + array(Schema::TYPE_MONEY, 'decimal(19,4)'), + array(Schema::TYPE_MONEY . '(16,2)', 'decimal(16,2)'), + array(Schema::TYPE_MONEY . ' CHECK (value > 0.0)', 'decimal(19,4) CHECK (value > 0.0)'), + array(Schema::TYPE_MONEY . '(16,2) CHECK (value > 0.0)', 'decimal(16,2) CHECK (value > 0.0)'), + array(Schema::TYPE_MONEY . ' NOT NULL', 'decimal(19,4) NOT NULL'), + ); + } +} \ No newline at end of file diff --git a/tests/unit/framework/helpers/ArrayHelperTest.php b/tests/unit/framework/helpers/ArrayHelperTest.php index 84277d6..cfda9ae 100644 --- a/tests/unit/framework/helpers/ArrayHelperTest.php +++ b/tests/unit/framework/helpers/ArrayHelperTest.php @@ -4,7 +4,7 @@ namespace yiiunit\framework\helpers; use yii\helpers\ArrayHelper; use yii\test\TestCase; -use yii\web\Sort; +use yii\data\Sort; class ArrayHelperTest extends TestCase { diff --git a/tests/unit/framework/web/ResponseTest.php b/tests/unit/framework/web/ResponseTest.php index 2fde63d..74d90cf 100644 --- a/tests/unit/framework/web/ResponseTest.php +++ b/tests/unit/framework/web/ResponseTest.php @@ -1,44 +1,20 @@ reset(); - } - - protected function reset() - { - static::$headers = array(); - static::$httpResponseCode = 200; + $this->mockApplication(); + $this->response = new Response; } public function rightRanges() @@ -59,14 +35,15 @@ class ResponseTest extends \yiiunit\TestCase { $content = $this->generateTestFileContent(); $_SERVER['HTTP_RANGE'] = 'bytes=' . $rangeHeader; - $sent = $this->runSendFile('testFile.txt', $content, null); - - $this->assertEquals($expectedFile, $sent); - $this->assertTrue(in_array('HTTP/1.1 206 Partial Content', static::$headers)); - $this->assertTrue(in_array('Accept-Ranges: bytes', static::$headers)); - $this->assertArrayHasKey('Content-Range: bytes ' . $expectedHeader . '/' . StringHelper::strlen($content), array_flip(static::$headers)); - $this->assertTrue(in_array('Content-Type: text/plain', static::$headers)); - $this->assertTrue(in_array('Content-Length: ' . $length, static::$headers)); + $this->response->sendFile('testFile.txt', $content, null, false); + + $this->assertEquals($expectedFile, $this->response->content); + $this->assertEquals(206, $this->response->statusCode); + $headers = $this->response->headers; + $this->assertEquals("bytes", $headers->get('Accept-Ranges')); + $this->assertEquals("bytes " . $expectedHeader . '/' . StringHelper::strlen($content), $headers->get('Content-Range')); + $this->assertEquals('text/plain', $headers->get('Content-Type')); + $this->assertEquals("$length", $headers->get('Content-Length')); } public function wrongRanges() @@ -90,21 +67,11 @@ class ResponseTest extends \yiiunit\TestCase $content = $this->generateTestFileContent(); $_SERVER['HTTP_RANGE'] = 'bytes=' . $rangeHeader; - $this->runSendFile('testFile.txt', $content, null); + $this->response->sendFile('testFile.txt', $content, null, false); } protected function generateTestFileContent() { return '12ёжик3456798áèabcdefghijklmnopqrstuvwxyz!"§$%&/(ёжик)=?'; } - - protected function runSendFile($fileName, $content, $mimeType) - { - ob_start(); - ob_implicit_flush(false); - $response = new Response(); - $response->sendFile($fileName, $content, $mimeType, false); - $file = ob_get_clean(); - return $file; - } } diff --git a/tests/unit/framework/web/UrlManagerTest.php b/tests/unit/framework/web/UrlManagerTest.php index 0f08790..7da8f34 100644 --- a/tests/unit/framework/web/UrlManagerTest.php +++ b/tests/unit/framework/web/UrlManagerTest.php @@ -248,4 +248,58 @@ class UrlManagerTest extends TestCase $result = $manager->parseRequest($request); $this->assertFalse($result); } + + public function testParseRESTRequest() + { + $manager = new UrlManager(array( + 'cache' => null, + )); + $request = new Request; + + // pretty URL rules + $manager = new UrlManager(array( + 'enablePrettyUrl' => true, + 'cache' => null, + 'rules' => array( + 'PUT,POST post//' => 'post/create', + 'DELETE post/<id>' => 'post/delete', + 'post/<id>/<title>' => 'post/view', + 'POST/GET' => 'post/get', + ), + )); + // matching pathinfo GET request + $_SERVER['REQUEST_METHOD'] = 'GET'; + $request->pathInfo = 'post/123/this+is+sample'; + $result = $manager->parseRequest($request); + $this->assertEquals(array('post/view', array('id' => '123', 'title' => 'this+is+sample')), $result); + // matching pathinfo PUT/POST request + $_SERVER['REQUEST_METHOD'] = 'PUT'; + $request->pathInfo = 'post/123/this+is+sample'; + $result = $manager->parseRequest($request); + $this->assertEquals(array('post/create', array('id' => '123', 'title' => 'this+is+sample')), $result); + $_SERVER['REQUEST_METHOD'] = 'POST'; + $request->pathInfo = 'post/123/this+is+sample'; + $result = $manager->parseRequest($request); + $this->assertEquals(array('post/create', array('id' => '123', 'title' => 'this+is+sample')), $result); + + // no wrong matching + $_SERVER['REQUEST_METHOD'] = 'POST'; + $request->pathInfo = 'POST/GET'; + $result = $manager->parseRequest($request); + $this->assertEquals(array('post/get', array()), $result); + + // createUrl should ignore REST rules + $this->mockApplication(array( + 'components' => array( + 'request' => array( + 'hostInfo' => 'http://localhost/', + 'baseUrl' => '/app' + ) + ) + ), \yii\web\Application::className()); + $this->assertEquals('/app/post/delete?id=123', $manager->createUrl('post/delete', array('id' => 123))); + $this->destroyApplication(); + + unset($_SERVER['REQUEST_METHOD']); + } }