diff --git a/docs/api/db/ActiveRecord.md b/docs/api/db/ActiveRecord.md index da281d8..822c548 100644 --- a/docs/api/db/ActiveRecord.md +++ b/docs/api/db/ActiveRecord.md @@ -300,7 +300,7 @@ foreach ($customers as $customer) { ~~~ How many SQL queries will be performed in the above code, assuming there are more than 100 customers in -the database? 101! The first SQL query brings back 100 customers. Then for each customer, another SQL query +the database? 101! The first SQL query brings back 100 customers. Then for each customer, a SQL query is performed to bring back the customer's orders. To solve the above performance problem, you can use the so-called *eager loading* by calling [[ActiveQuery::with()]]: @@ -318,7 +318,7 @@ foreach ($customers as $customer) { } ~~~ -As you can see, only two SQL queries were needed for the same task. +As you can see, only two SQL queries are needed for the same task. Sometimes, you may want to customize the relational queries on the fly. It can be diff --git a/docs/autoloader.md b/docs/autoloader.md new file mode 100644 index 0000000..b7696d7 --- /dev/null +++ b/docs/autoloader.md @@ -0,0 +1,19 @@ +Yii2 class loader +================= + +Yii 2 class loader is PSR-0 compliant. That means it can handle most of the PHP +libraries and frameworks out there. + +In order to autoload a library you need to set a root alias for it. + +PEAR-style libraries +-------------------- + +```php +\Yii::setAlias('@Twig', '@app/vendors/Twig'); +``` + +References +---------- + +- YiiBase::autoload \ No newline at end of file diff --git a/docs/code_style.md b/docs/code_style.md index dfa475e..92a934b 100644 --- a/docs/code_style.md +++ b/docs/code_style.md @@ -204,7 +204,7 @@ doIt('a', array( ~~~ if ($event === null) { - return new Event($this); + return new Event(); } elseif ($event instanceof CoolEvent) { return $event->instance(); } else { @@ -251,10 +251,8 @@ switch ($this->phpType) { ~~~ class name or directory + private static $_imported = array(); // alias => class name or directory private static $_logger; /** @@ -125,8 +123,8 @@ class YiiBase * * To import a class or a directory, one can use either path alias or class name (can be namespaced): * - * - `@application/components/GoogleMap`: importing the `GoogleMap` class with a path alias; - * - `@application/components/*`: importing the whole `components` directory with a path alias; + * - `@app/components/GoogleMap`: importing the `GoogleMap` class with a path alias; + * - `@app/components/*`: importing the whole `components` directory with a path alias; * - `GoogleMap`: importing the `GoogleMap` class with a class name. [[autoload()]] will be used * when this class is used for the first time. * @@ -161,9 +159,7 @@ class YiiBase return self::$_imported[$alias] = $className; } - if (($path = static::getAlias(dirname($alias))) === false) { - throw new Exception('Invalid path alias: ' . $alias); - } + $path = static::getAlias(dirname($alias)); if ($isClass) { if ($forceInclude) { @@ -193,24 +189,30 @@ class YiiBase * * Note, this method does not ensure the existence of the resulting path. * @param string $alias alias + * @param boolean $throwException whether to throw an exception if the given alias is invalid. + * If this is false and an invalid alias is given, false will be returned by this method. * @return string|boolean path corresponding to the alias, false if the root alias is not previously registered. * @see setAlias */ - public static function getAlias($alias) + public static function getAlias($alias, $throwException = true) { - if (!is_string($alias)) { - return false; - } elseif (isset(self::$aliases[$alias])) { - return self::$aliases[$alias]; - } elseif ($alias === '' || $alias[0] !== '@') { // not an alias - return $alias; - } elseif (($pos = strpos($alias, '/')) !== false) { - $rootAlias = substr($alias, 0, $pos); - if (isset(self::$aliases[$rootAlias])) { - return self::$aliases[$alias] = self::$aliases[$rootAlias] . substr($alias, $pos); + if (is_string($alias)) { + if (isset(self::$aliases[$alias])) { + return self::$aliases[$alias]; + } elseif ($alias === '' || $alias[0] !== '@') { // not an alias + return $alias; + } elseif (($pos = strpos($alias, '/')) !== false || ($pos = strpos($alias, '\\')) !== false) { + $rootAlias = substr($alias, 0, $pos); + if (isset(self::$aliases[$rootAlias])) { + return self::$aliases[$alias] = self::$aliases[$rootAlias] . substr($alias, $pos); + } } } - return false; + if ($throwException) { + throw new InvalidParamException("Invalid path alias: $alias"); + } else { + return false; + } } /** @@ -238,10 +240,8 @@ class YiiBase unset(self::$aliases[$alias]); } elseif ($path[0] !== '@') { self::$aliases[$alias] = rtrim($path, '\\/'); - } elseif (($p = static::getAlias($path)) !== false) { - self::$aliases[$alias] = $p; } else { - throw new Exception('Invalid path: ' . $path); + self::$aliases[$alias] = static::getAlias($path); } } @@ -262,6 +262,7 @@ class YiiBase * * @param string $className class name * @return boolean whether the class has been loaded successfully + * @throws Exception if the class file does not exist */ public static function autoload($className) { @@ -274,14 +275,14 @@ class YiiBase // namespaced class, e.g. yii\base\Component // convert namespace to path alias, e.g. yii\base\Component to @yii/base/Component $alias = '@' . str_replace('\\', '/', ltrim($className, '\\')); - if (($path = static::getAlias($alias)) !== false) { + if (($path = static::getAlias($alias, false)) !== false) { $classFile = $path . '.php'; } } elseif (($pos = strpos($className, '_')) !== false) { // PEAR-styled class, e.g. PHPUnit_Framework_TestCase // convert class name to path alias, e.g. PHPUnit_Framework_TestCase to @PHPUnit/Framework/TestCase $alias = '@' . str_replace('_', '/', $className); - if (($path = static::getAlias($alias)) !== false) { + if (($path = static::getAlias($alias, false)) !== false) { $classFile = $path . '.php'; } } @@ -297,7 +298,7 @@ class YiiBase } } - if (isset($classFile, $alias)) { + if (isset($classFile, $alias) && is_file($classFile)) { if (!YII_DEBUG || basename(realpath($classFile)) === basename($alias) . '.php') { include($classFile); return true; @@ -322,12 +323,12 @@ class YiiBase * the class. For example, * * - `\app\components\GoogleMap`: fully-qualified namespaced class. - * - `@application/components/GoogleMap`: an alias + * - `@app/components/GoogleMap`: an alias * * Below are some usage examples: * * ~~~ - * $object = \Yii::createObject('@application/components/GoogleMap'); + * $object = \Yii::createObject('@app/components/GoogleMap'); * $object = \Yii::createObject(array( * 'class' => '\app\components\GoogleMap', * 'apiKey' => 'xyz', @@ -507,9 +508,6 @@ class YiiBase * i.e., the message returned will be chosen from a few candidates according to the given * number value. This feature is mainly used to solve plural format issue in case * a message has different plural forms in some languages. - * @param string $category message category. Please use only word letters. Note, category 'yii' is - * reserved for Yii framework core code use. See {@link CPhpMessageSource} for - * more interpretation about message category. * @param string $message the original message * @param array $params parameters to be applied to the message using strtr. * The first parameter can be a number without key. @@ -517,62 +515,12 @@ class YiiBase * an appropriate message translation. * You can pass parameter for {@link CChoiceFormat::format} * or plural forms format without wrapping it with array. - * @param string $source which message source application component to use. - * Defaults to null, meaning using 'coreMessages' for messages belonging to - * the 'yii' category and using 'messages' for the rest messages. * @param string $language the target language. If null (default), the {@link CApplication::getLanguage application language} will be used. * @return string the translated message * @see CMessageSource */ - public static function t($category, $message, $params = array(), $source = null, $language = null) + public static function t($message, $params = array(), $language = null) { - // todo; - return $params !== array() ? strtr($message, $params) : $message; - if (self::$application !== null) - { - if ($source === null) - { - $source = $category === 'yii' ? 'coreMessages' : 'messages'; - } - if (($source = self::$application->getComponent($source)) !== null) - { - $message = $source->translate($category, $message, $language); - } - } - if ($params === array()) - { - return $message; - } - if (!is_array($params)) - { - $params = array($params); - } - if (isset($params[0])) // number choice - { - if (strpos($message, '|') !== false) - { - if (strpos($message, '#') === false) - { - $chunks = explode('|', $message); - $expressions = self::$application->getLocale($language)->getPluralRules(); - if ($n = min(count($chunks), count($expressions))) - { - for ($i = 0; $i < $n; $i++) - { - $chunks[$i] = $expressions[$i] . '#' . $chunks[$i]; - } - - $message = implode('|', $chunks); - } - } - $message = CChoiceFormat::format($message, $params[0]); - } - if (!isset($params['{n}'])) - { - $params['{n}'] = $params[0]; - } - unset($params[0]); - } - return $params !== array() ? strtr($message, $params) : $message; + return Yii::$app->getI18N()->translate($message, $params, $language); } } diff --git a/framework/base/Action.php b/framework/base/Action.php index 8d4ec5a..7142539 100644 --- a/framework/base/Action.php +++ b/framework/base/Action.php @@ -1,9 +1,7 @@ controller->getUniqueId() . '/' . $this->id; + } + + /** * Runs this action with the specified parameters. * This method is mainly invoked by the controller. * @param array $params the parameters to be bound to the action's run() method. @@ -67,36 +74,7 @@ class Action extends Component if (!method_exists($this, 'run')) { throw new InvalidConfigException(get_class($this) . ' must define a "run()" method.'); } - $method = new \ReflectionMethod($this, 'run'); - $args = $this->bindActionParams($method, $params); - return (int)$method->invokeArgs($this, $args); - } - - /** - * Binds the given parameters to the action method. - * The returned array contains the parameters that need to be passed to the action method. - * This method calls [[Controller::validateActionParams()]] to check if any exception - * should be raised if there are missing or unknown parameters. - * @param \ReflectionMethod $method the action method reflection object - * @param array $params the supplied parameters - * @return array the parameters that can be passed to the action method - */ - protected function bindActionParams($method, $params) - { - $args = array(); - $missing = array(); - foreach ($method->getParameters() as $param) { - $name = $param->getName(); - if (array_key_exists($name, $params)) { - $args[] = $params[$name]; - unset($params[$name]); - } elseif ($param->isDefaultValueAvailable()) { - $args[] = $param->getDefaultValue(); - } else { - $missing[] = $name; - } - } - $this->controller->validateActionParams($this, $missing, $params); - return $args; + $args = $this->controller->bindActionParams($this, $params); + return (int)call_user_func_array(array($this, 'run'), $args); } } diff --git a/framework/base/ActionEvent.php b/framework/base/ActionEvent.php index ee945a8..7c5a40c 100644 --- a/framework/base/ActionEvent.php +++ b/framework/base/ActionEvent.php @@ -1,9 +1,7 @@ + * @since 2.0 + */ +class ActionFilter extends Behavior +{ + /** + * @var array list of action IDs that this filter should apply to. If this property is not set, + * then the filter applies to all actions, unless they are listed in [[except]]. + */ + public $only; + /** + * @var array list of action IDs that this filter should not apply to. + */ + public $except = array(); + + /** + * Declares event handlers for the [[owner]]'s events. + * @return array events (array keys) and the corresponding event handler methods (array values). + */ + public function events() + { + return array( + 'beforeAction' => 'beforeFilter', + 'afterAction' => 'afterFilter', + ); + } + + /** + * @param ActionEvent $event + * @return boolean + */ + public function beforeFilter($event) + { + if ($this->isActive($event->action)) { + $event->isValid = $this->beforeAction($event->action); + } + return $event->isValid; + } + + /** + * @param ActionEvent $event + * @return boolean + */ + public function afterFilter($event) + { + if ($this->isActive($event->action)) { + $this->afterAction($event->action); + } + } + + /** + * This method is invoked right before an action is to be executed (after all possible filters.) + * You may override this method to do last-minute preparation for the action. + * @param Action $action the action to be executed. + * @return boolean whether the action should continue to be executed. + */ + public function beforeAction($action) + { + return true; + } + + /** + * This method is invoked right after an action is executed. + * You may override this method to do some postprocessing for the action. + * @param Action $action the action just executed. + */ + public function afterAction($action) + { + } + + /** + * Returns a value indicating whether the filer is active for the given action. + * @param Action $action the action being filtered + * @return boolean whether the filer is active for the given action. + */ + protected function isActive($action) + { + return !in_array($action->id, $this->except, true) && (empty($this->only) || in_array($action->id, $this->only, true)); + } +} \ No newline at end of file diff --git a/framework/base/Application.php b/framework/base/Application.php index 40e8437..9be1939 100644 --- a/framework/base/Application.php +++ b/framework/base/Application.php @@ -1,16 +1,14 @@ *
  • {@link getCache cache}: provides caching feature. This application component is * disabled by default.
  • - *
  • {@link getMessages messages}: provides the message source for translating - * application messages. This application component is dynamically loaded when needed.
  • - *
  • {@link getCoreMessages coreMessages}: provides the message source for translating - * Yii framework messages. This application component is dynamically loaded when needed.
  • * * * Application will undergo the following life cycles when processing a user request: @@ -57,29 +51,34 @@ class Application extends Module const EVENT_BEFORE_REQUEST = 'beforeRequest'; const EVENT_AFTER_REQUEST = 'afterRequest'; /** - * @var string the application name. Defaults to 'My Application'. + * @var string the application name. */ public $name = 'My Application'; /** - * @var string the version of this application. Defaults to '1.0'. + * @var string the version of this application. */ public $version = '1.0'; /** - * @var string the charset currently used for the application. Defaults to 'UTF-8'. + * @var string the charset currently used for the application. */ public $charset = 'UTF-8'; /** + * @var string the language that is meant to be used for end users. + * @see sourceLanguage + */ + public $language = 'en_US'; + /** * @var string the language that the application is written in. This mainly refers to - * the language that the messages and view files are in. Defaults to 'en_us' (US English). + * the language that the messages and view files are written in. * @see language */ - public $sourceLanguage = 'en_us'; + public $sourceLanguage = 'en_US'; /** * @var array IDs of the components that need to be loaded when the application starts. */ public $preload = array(); /** - * @var Controller the currently active controller instance + * @var \yii\web\Controller|\yii\console\Controller the currently active controller instance */ public $controller; /** @@ -93,7 +92,12 @@ class Application extends Module private $_runtimePath; private $_ended = false; - private $_language; + + /** + * @var string Used to reserve memory for fatal error handler. This memory + * reserve can be removed if it's OK to write to PHP log only in this particular case. + */ + private $_memoryReserve; /** * Constructor. @@ -104,11 +108,12 @@ class Application extends Module */ public function __construct($id, $basePath, $config = array()) { - Yii::$application = $this; + Yii::$app = $this; $this->id = $id; $this->setBasePath($basePath); if (YII_ENABLE_ERROR_HANDLER) { + ini_set('display_errors', 0); set_exception_handler(array($this, 'handleException')); set_error_handler(array($this, 'handleError'), error_reporting()); } @@ -141,12 +146,64 @@ class Application extends Module $this->_ended = true; $this->afterRequest(); } + + $this->handleFatalError(); + if ($exit) { exit($status); } } /** + * Handles fatal PHP errors + */ + public function handleFatalError() + { + if (YII_ENABLE_ERROR_HANDLER) { + $error = error_get_last(); + + if (ErrorException::isFatalErorr($error)) { + unset($this->_memoryReserve); + $exception = new ErrorException($error['message'], $error['type'], $error['type'], $error['file'], $error['line']); + + if (function_exists('xdebug_get_function_stack')) { + $trace = array_slice(array_reverse(xdebug_get_function_stack()), 4, -1); + foreach ($trace as &$frame) { + if (!isset($frame['function'])) { + $frame['function'] = 'unknown'; + } + + // XDebug < 2.1.1: http://bugs.xdebug.org/view.php?id=695 + if (!isset($frame['type'])) { + $frame['type'] = '::'; + } + + // XDebug has a different key name + $frame['args'] = array(); + if (isset($frame['params']) && !isset($frame['args'])) { + $frame['args'] = $frame['params']; + } + } + + $ref = new \ReflectionProperty('Exception', 'trace'); + $ref->setAccessible(true); + $ref->setValue($exception, $trace); + } + + $this->logException($exception); + + if (($handler = $this->getErrorHandler()) !== null) { + @$handler->handle($exception); + } else { + $this->renderException($exception); + } + + exit(1); + } + } + } + + /** * Runs the application. * This is the main entrance of an application. * @return integer the exit status (0 means normal, non-zero values mean abnormal) @@ -154,6 +211,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); $status = $this->processRequest(); $this->afterRequest(); return $status; @@ -213,29 +274,6 @@ class Application extends Module } /** - * Returns the language that the end user is using. - * @return string the language that the user is using (e.g. 'en_US', 'zh_CN'). - * Defaults to the value of [[sourceLanguage]]. - */ - public function getLanguage() - { - return $this->_language === null ? $this->sourceLanguage : $this->_language; - } - - /** - * Specifies which language the end user is using. - * This is the language that the application should use to display to end users. - * By default, [[language]] and [[sourceLanguage]] are the same. - * Do not set this property unless your application needs to support multiple languages. - * @param string $language the user language (e.g. 'en_US', 'zh_CN'). - * If it is null, the [[sourceLanguage]] will be used. - */ - public function setLanguage($language) - { - $this->_language = $language; - } - - /** * Returns the time zone used by this application. * This is a simple wrapper of PHP function date_default_timezone_get(). * @return string the time zone used by this application. @@ -257,14 +295,6 @@ class Application extends Module date_default_timezone_set($value); } - // /** - // * Returns the security manager component. - // * @return SecurityManager the security manager application component. - // */ - // public function getSecurityManager() - // { - // return $this->getComponent('securityManager'); - // } // // /** // * Returns the locale instance. @@ -295,23 +325,6 @@ class Application extends Module // return $this->getLocale()->getDateFormatter(); // } // - // /** - // * Returns the core message translations component. - // * @return \yii\i18n\MessageSource the core message translations - // */ - // public function getCoreMessages() - // { - // return $this->getComponent('coreMessages'); - // } - // - // /** - // * Returns the application message translations component. - // * @return \yii\i18n\MessageSource the application message translations - // */ - // public function getMessages() - // { - // return $this->getComponent('messages'); - // } /** * Returns the database connection component. @@ -332,15 +345,6 @@ class Application extends Module } /** - * Returns the application theme. - * @return Theme the theme that this application is currently using. - */ - public function getTheme() - { - return $this->getComponent('theme'); - } - - /** * Returns the cache component. * @return \yii\caching\Cache the cache application component. Null if the component is not enabled. */ @@ -351,7 +355,7 @@ class Application extends Module /** * Returns the request component. - * @return Request the request component + * @return \yii\web\Request|\yii\console\Request the request component */ public function getRequest() { @@ -359,12 +363,30 @@ class Application extends Module } /** - * Returns the view renderer. - * @return ViewRenderer the view renderer used by this application. + * Returns the view object. + * @return View the view object that is used to render various view files. + */ + public function getView() + { + return $this->getComponent('view'); + } + + /** + * Returns the URL manager for this application. + * @return \yii\web\UrlManager the URL manager for this application. + */ + public function getUrlManager() + { + return $this->getComponent('urlManager'); + } + + /** + * Returns the internationalization (i18n) component + * @return \yii\i18n\I18N the internationalization component */ - public function getViewRenderer() + public function getI18N() { - return $this->getComponent('viewRenderer'); + return $this->getComponent('i18n'); } /** @@ -372,9 +394,7 @@ class Application extends Module */ public function registerDefaultAliases() { - Yii::$aliases['@application'] = $this->getBasePath(); - Yii::$aliases['@entry'] = dirname($_SERVER['SCRIPT_FILENAME']); - Yii::$aliases['@www'] = ''; + Yii::$aliases['@app'] = $this->getBasePath(); } /** @@ -387,20 +407,15 @@ class Application extends Module 'errorHandler' => array( 'class' => 'yii\base\ErrorHandler', ), - 'coreMessages' => array( - 'class' => 'yii\i18n\PhpMessageSource', - 'language' => 'en_us', - 'basePath' => '@yii/messages', - ), - 'messages' => array( - 'class' => 'yii\i18n\PhpMessageSource', - ), - 'securityManager' => array( - 'class' => 'yii\base\SecurityManager', + 'i18n' => array( + 'class' => 'yii\i18n\I18N', ), 'urlManager' => array( 'class' => 'yii\web\UrlManager', ), + 'view' => array( + 'class' => 'yii\base\View', + ), )); } @@ -413,12 +428,24 @@ class Application extends Module * @param string $message the error message * @param string $file the filename that the error was raised in * @param integer $line the line number the error was raised at - * @throws \ErrorException the error exception + * + * @throws ErrorException */ public function handleError($code, $message, $file, $line) { if (error_reporting() !== 0) { - throw new \ErrorException($message, 0, $code, $file, $line); + $exception = new ErrorException($message, $code, $code, $file, $line); + + // in case error appeared in __toString method we can't throw any exception + $trace = debug_backtrace(false); + array_shift($trace); + foreach ($trace as $frame) { + if ($frame['function'] == '__toString') { + $this->handleException($exception); + } + } + + throw $exception; } } @@ -447,11 +474,14 @@ class Application extends Module $this->end(1); - } catch(\Exception $e) { + } catch (\Exception $e) { // exception could be thrown in end() or ErrorHandler::handle() $msg = (string)$e; $msg .= "\nPrevious exception:\n"; $msg .= (string)$exception; + if (YII_DEBUG) { + echo $msg; + } $msg .= "\n\$_SERVER = " . var_export($_SERVER, true); error_log($msg); exit(1); @@ -464,7 +494,7 @@ class Application extends Module */ public function renderException($exception) { - if ($exception instanceof Exception && ($exception->causedByUser || !YII_DEBUG)) { + if ($exception instanceof Exception && ($exception instanceof UserException || !YII_DEBUG)) { $message = $exception->getName() . ': ' . $exception->getMessage(); } else { $message = YII_DEBUG ? (string)$exception : 'Error: ' . $exception->getMessage(); diff --git a/framework/base/Behavior.php b/framework/base/Behavior.php index 9155097..abe08bb 100644 --- a/framework/base/Behavior.php +++ b/framework/base/Behavior.php @@ -1,9 +1,7 @@ ensureBehaviors(); if (isset($this->_e[$name]) && $this->_e[$name]->getCount()) { if ($event === null) { - $event = new Event($this); + $event = new Event; + } + if ($event->sender === null) { + $event->sender = $this; } $event->handled = false; $event->name = $name; diff --git a/framework/base/Controller.php b/framework/base/Controller.php index 804b339..ff6d8f7 100644 --- a/framework/base/Controller.php +++ b/framework/base/Controller.php @@ -1,23 +1,19 @@ * @since 2.0 */ @@ -72,9 +68,9 @@ class Controller extends Component * * ~~~ * return array( - * 'action1' => '@application/components/Action1', + * 'action1' => '@app/components/Action1', * 'action2' => array( - * 'class' => '@application/components/Action2', + * 'class' => '@app/components/Action2', * 'property1' => 'value1', * 'property2' => 'value2', * ), @@ -139,8 +135,50 @@ class Controller extends Component } elseif ($pos > 0) { return $this->module->runAction($route, $params); } else { - return \Yii::$application->runAction(ltrim($route, '/'), $params); + return \Yii::$app->runAction(ltrim($route, '/'), $params); + } + } + + /** + * Binds the parameters to the action. + * This method is invoked by [[Action]] when it begins to run with the given parameters. + * This method will check the parameter names that the action requires and return + * the provided parameters according to the requirement. If there is any missing parameter, + * an exception will be thrown. + * @param Action $action the action to be bound with parameters + * @param array $params the parameters to be bound to the action + * @return array the valid parameters that the action can run with. + * @throws InvalidRequestException if there are missing parameters. + */ + public function bindActionParams($action, $params) + { + if ($action instanceof InlineAction) { + $method = new \ReflectionMethod($this, $action->actionMethod); + } else { + $method = new \ReflectionMethod($action, 'run'); + } + + $args = array(); + $missing = array(); + foreach ($method->getParameters() as $param) { + $name = $param->getName(); + if (array_key_exists($name, $params)) { + $args[] = $params[$name]; + unset($params[$name]); + } elseif ($param->isDefaultValueAvailable()) { + $args[] = $param->getDefaultValue(); + } else { + $missing[] = $name; + } + } + + if ($missing !== array()) { + throw new InvalidRequestException(Yii::t('yii|Missing required parameters: {params}', array( + '{params}' => implode(', ', $missing), + ))); } + + return $args; } /** @@ -250,34 +288,51 @@ class Controller extends Component */ public function getRoute() { - return $this->action !== null ? $this->getUniqueId() . '/' . $this->action->id : $this->getUniqueId(); + return $this->action !== null ? $this->action->getUniqueId() : $this->getUniqueId(); } /** * Renders a view and applies layout if available. - * - * @param $view - * @param array $params - * @return string + * @param string $view the view name. Please refer to [[findViewFile()]] on how to specify a view name. + * @param array $params the parameters (name-value pairs) that should be made available in the view. + * These parameters will not be available in the layout. + * @return string the rendering result. + * @throws InvalidParamException if the view file or the layout file does not exist. */ public function render($view, $params = array()) { - return $this->createView()->render($view, $params); - } - - public function renderContent($content) - { - return $this->createView()->renderContent($content); + $output = Yii::$app->getView()->render($view, $params, $this); + $layoutFile = $this->findLayoutFile(); + if ($layoutFile !== false) { + return Yii::$app->getView()->renderFile($layoutFile, array('content' => $output), $this); + } else { + return $output; + } } + /** + * Renders a view. + * This method differs from [[render()]] in that it does not apply any layout. + * @param string $view the view name. Please refer to [[findViewFile()]] on how to specify a view name. + * @param array $params the parameters (name-value pairs) that should be made available in the view. + * @return string the rendering result. + * @throws InvalidParamException if the view file does not exist. + */ public function renderPartial($view, $params = array()) { - return $this->createView()->renderPartial($view, $params); + return Yii::$app->getView()->render($view, $params, $this); } - public function createView() + /** + * Renders a view file. + * @param string $file the view file to be rendered. This can be either a file path or a path alias. + * @param array $params the parameters (name-value pairs) that should be made available in the view. + * @return string the rendering result. + * @throws InvalidParamException if the view file does not exist. + */ + public function renderFile($file, $params = array()) { - return new View($this); + return Yii::$app->getView()->renderFile($file, $params, $this); } /** @@ -290,4 +345,63 @@ class Controller extends Component { return $this->module->getViewPath() . DIRECTORY_SEPARATOR . $this->id; } + + /** + * Finds the applicable layout file. + * + * This method locates an applicable layout file via two steps. + * + * In the first step, it determines the layout name and the context module: + * + * - If [[layout]] is specified as a string, use it as the layout name and [[module]] as the context module; + * - If [[layout]] is null, search through all ancestor modules of this controller and find the first + * module whose [[Module::layout|layout]] is not null. The layout and the corresponding module + * are used as the layout name and the context module, respectively. If such a module is not found + * or the corresponding layout is not a string, it will return false, meaning no applicable layout. + * + * In the second step, it determines the actual layout file according to the previously found layout name + * and context module. The layout name can be + * + * - a path alias (e.g. "@app/views/layouts/main"); + * - an absolute path (e.g. "/main"): the layout name starts with a slash. The actual layout file will be + * looked for under the [[Application::layoutPath|layout path]] of the application; + * - a relative path (e.g. "main"): the actual layout layout file will be looked for under the + * [[Module::viewPath|view path]] of the context module. + * + * If the layout name does not contain a file extension, it will use the default one `.php`. + * + * @return string|boolean the layout file path, or false if layout is not needed. + * @throws InvalidParamException if an invalid path alias is used to specify the layout + */ + protected function findLayoutFile() + { + $module = $this->module; + if (is_string($this->layout)) { + $view = $this->layout; + } elseif ($this->layout === null) { + while ($module !== null && $module->layout === null) { + $module = $module->module; + } + if ($module !== null && is_string($module->layout)) { + $view = $module->layout; + } + } + + if (!isset($view)) { + return false; + } + + if (strncmp($view, '@', 1) === 0) { + $file = Yii::getAlias($view); + } elseif (strncmp($view, '/', 1) === 0) { + $file = Yii::$app->getLayoutPath() . DIRECTORY_SEPARATOR . $view; + } else { + $file = $module->getLayoutPath() . DIRECTORY_SEPARATOR . $view; + } + + if (FileHelper::getExtension($file) === '') { + $file .= '.php'; + } + return $file; + } } diff --git a/framework/base/Dictionary.php b/framework/base/Dictionary.php index cc61886..52262cb 100644 --- a/framework/base/Dictionary.php +++ b/framework/base/Dictionary.php @@ -1,15 +1,13 @@ _d) as $key) { @@ -166,7 +164,7 @@ class Dictionary extends Object implements \IteratorAggregate, \ArrayAccess, \Co * @param mixed $key the key * @return boolean whether the dictionary contains an item with the specified key */ - public function contains($key) + public function has($key) { return isset($this->_d[$key]) || array_key_exists($key, $this->_d); } @@ -184,13 +182,13 @@ class Dictionary extends Object implements \IteratorAggregate, \ArrayAccess, \Co * Copies iterable data into the dictionary. * Note, existing data in the dictionary will be cleared first. * @param mixed $data the data to be copied from, must be an array or an object implementing `Traversable` - * @throws InvalidCallException if data is neither an array nor an iterator. + * @throws InvalidParamException if data is neither an array nor an iterator. */ public function copyFrom($data) { if (is_array($data) || $data instanceof \Traversable) { if ($this->_d !== array()) { - $this->clear(); + $this->removeAll(); } if ($data instanceof self) { $data = $data->_d; @@ -199,7 +197,7 @@ class Dictionary extends Object implements \IteratorAggregate, \ArrayAccess, \Co $this->add($key, $value); } } else { - throw new InvalidCallException('Data must be either an array or an object implementing Traversable.'); + throw new InvalidParamException('Data must be either an array or an object implementing Traversable.'); } } @@ -216,7 +214,7 @@ class Dictionary extends Object implements \IteratorAggregate, \ArrayAccess, \Co * * @param array|\Traversable $data the data to be merged with. It must be an array or object implementing Traversable * @param boolean $recursive whether the merging should be recursive. - * @throws InvalidCallException if data is neither an array nor an object implementing `Traversable`. + * @throws InvalidParamException if data is neither an array nor an object implementing `Traversable`. */ public function mergeWith($data, $recursive = true) { @@ -240,7 +238,7 @@ class Dictionary extends Object implements \IteratorAggregate, \ArrayAccess, \Co } } } else { - throw new InvalidCallException('The data to be merged with must be an array or an object implementing Traversable.'); + throw new InvalidParamException('The data to be merged with must be an array or an object implementing Traversable.'); } } @@ -254,7 +252,7 @@ class Dictionary extends Object implements \IteratorAggregate, \ArrayAccess, \Co */ public function offsetExists($offset) { - return $this->contains($offset); + return $this->has($offset); } /** diff --git a/framework/base/DictionaryIterator.php b/framework/base/DictionaryIterator.php index 61f61cf..0d15bb0 100644 --- a/framework/base/DictionaryIterator.php +++ b/framework/base/DictionaryIterator.php @@ -1,9 +1,7 @@ + * @since 2.0 + */ +class ErrorException extends Exception +{ + protected $severity; + + /** + * Constructs the exception + * @link http://php.net/manual/en/errorexception.construct.php + * @param $message [optional] + * @param $code [optional] + * @param $severity [optional] + * @param $filename [optional] + * @param $lineno [optional] + * @param $previous [optional] + */ + public function __construct($message = '', $code = 0, $severity = 1, $filename = __FILE__, $lineno = __LINE__, \Exception $previous = null) + { + parent::__construct($message, $code, $previous); + $this->severity = $severity; + $this->file = $filename; + $this->line = $lineno; + } + + /** + * Gets the exception severity + * @link http://php.net/manual/en/errorexception.getseverity.php + * @return int the severity level of the exception. + */ + final public function getSeverity() + { + return $this->severity; + } + + /** + * Returns if error is one of fatal type + * + * @param array $error error got from error_get_last() + * @return bool if error is one of fatal type + */ + public static function isFatalErorr($error) + { + return isset($error['type']) && in_array($error['type'], array(E_ERROR, E_PARSE, E_CORE_ERROR, E_CORE_WARNING, E_COMPILE_ERROR, E_COMPILE_WARNING)); + } + + /** + * @return string the user-friendly name of this exception + */ + public function getName() + { + $names = array( + E_ERROR => \Yii::t('yii|Fatal Error'), + E_PARSE => \Yii::t('yii|Parse Error'), + E_CORE_ERROR => \Yii::t('yii|Core Error'), + E_COMPILE_ERROR => \Yii::t('yii|Compile Error'), + E_USER_ERROR => \Yii::t('yii|User Error'), + E_WARNING => \Yii::t('yii|Warning'), + E_CORE_WARNING => \Yii::t('yii|Core Warning'), + E_COMPILE_WARNING => \Yii::t('yii|Compile Warning'), + E_USER_WARNING => \Yii::t('yii|User Warning'), + E_STRICT => \Yii::t('yii|Strict'), + E_NOTICE => \Yii::t('yii|Notice'), + E_RECOVERABLE_ERROR => \Yii::t('yii|Recoverable Error'), + E_DEPRECATED => \Yii::t('yii|Deprecated'), + ); + return isset($names[$this->getCode()]) ? $names[$this->getCode()] : \Yii::t('yii|Error'); + } +} diff --git a/framework/base/ErrorHandler.php b/framework/base/ErrorHandler.php index 0b6bf97..f71b8c8 100644 --- a/framework/base/ErrorHandler.php +++ b/framework/base/ErrorHandler.php @@ -1,9 +1,7 @@ * @since 2.0 */ -use yii\util\VarDumper; +use yii\helpers\VarDumper; class ErrorHandler extends Component { @@ -36,7 +34,7 @@ class ErrorHandler extends Component public $discardExistingOutput = true; /** * @var string the route (eg 'site/error') to the controller action that will be used to display external errors. - * Inside the action, it can retrieve the error information by \Yii::$application->errorHandler->error. + * Inside the action, it can retrieve the error information by \Yii::$app->errorHandler->error. * This property defaults to null, meaning ErrorHandler will handle the error display. */ public $errorAction; @@ -71,27 +69,27 @@ class ErrorHandler extends Component protected function render($exception) { if ($this->errorAction !== null) { - \Yii::$application->runAction($this->errorAction); - } elseif (\Yii::$application instanceof \yii\web\Application) { + \Yii::$app->runAction($this->errorAction); + } elseif (\Yii::$app instanceof \yii\web\Application) { if (!headers_sent()) { $errorCode = $exception instanceof HttpException ? $exception->statusCode : 500; header("HTTP/1.0 $errorCode " . get_class($exception)); } if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest') { - \Yii::$application->renderException($exception); + \Yii::$app->renderException($exception); } else { - $view = new View($this); - if (!YII_DEBUG || $exception instanceof Exception && $exception->causedByUser) { + $view = new View; + if (!YII_DEBUG || $exception instanceof UserException) { $viewName = $this->errorView; } else { $viewName = $this->exceptionView; } echo $view->render($viewName, array( 'exception' => $exception, - )); + ), $this); } } else { - \Yii::$application->renderException($exception); + \Yii::$app->renderException($exception); } } @@ -239,7 +237,7 @@ class ErrorHandler extends Component public function htmlEncode($text) { - return htmlspecialchars($text, ENT_QUOTES, \Yii::$application->charset); + return htmlspecialchars($text, ENT_QUOTES, \Yii::$app->charset); } public function clearOutput() @@ -255,15 +253,10 @@ class ErrorHandler extends Component */ public function renderAsHtml($exception) { - $view = new View($this); - if (!YII_DEBUG || $exception instanceof Exception && $exception->causedByUser) { - $viewName = $this->errorView; - } else { - $viewName = $this->exceptionView; - } + $view = new View; $name = !YII_DEBUG || $exception instanceof HttpException ? $this->errorView : $this->exceptionView; echo $view->render($name, array( 'exception' => $exception, - )); + ), $this); } } diff --git a/framework/base/Event.php b/framework/base/Event.php index 540e982..b86ed7c 100644 --- a/framework/base/Event.php +++ b/framework/base/Event.php @@ -1,9 +1,7 @@ sender = $sender; - $this->data = $data; - parent::__construct($config); - } } diff --git a/framework/base/Exception.php b/framework/base/Exception.php index ab681e2..9ee698b 100644 --- a/framework/base/Exception.php +++ b/framework/base/Exception.php @@ -1,9 +1,7 @@ * @since 2.0 */ -class HttpException extends Exception +class HttpException extends UserException { /** * @var integer HTTP status code, such as 403, 404, 500, etc. */ public $statusCode; - /** - * @var boolean whether this exception is caused by end user's mistake (e.g. wrong URL) - */ - public $causedByUser = true; /** * Constructor. @@ -41,4 +35,73 @@ class HttpException extends Exception $this->statusCode = $status; parent::__construct($message, $code); } + + /** + * @return string the user-friendly name of this exception + */ + public function getName() + { + static $httpCodes = array( + 100 => 'Continue', + 101 => 'Switching Protocols', + 102 => 'Processing', + 118 => 'Connection timed out', + 200 => 'OK', + 201 => 'Created', + 202 => 'Accepted', + 203 => 'Non-Authoritative', + 204 => 'No Content', + 205 => 'Reset Content', + 206 => 'Partial Content', + 207 => 'Multi-Status', + 210 => 'Content Different', + 300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', + 303 => 'See Other', + 304 => 'Not Modified', + 305 => 'Use Proxy', + 307 => 'Temporary Redirect', + 310 => 'Too many Redirect', + 400 => 'Bad Request', + 401 => 'Unauthorized', + 402 => 'Payment Required', + 403 => 'Forbidden', + 404 => 'Not Found', + 405 => 'Method Not Allowed', + 406 => 'Not Acceptable', + 407 => 'Proxy Authentication Required', + 408 => 'Request Time-out', + 409 => 'Conflict', + 410 => 'Gone', + 411 => 'Length Required', + 412 => 'Precondition Failed', + 413 => 'Request Entity Too Large', + 414 => 'Request-URI Too Long', + 415 => 'Unsupported Media Type', + 416 => 'Requested range unsatisfiable', + 417 => 'Expectation failed', + 418 => 'I’m a teapot', + 422 => 'Unprocessable entity', + 423 => 'Locked', + 424 => 'Method failure', + 425 => 'Unordered Collection', + 426 => 'Upgrade Required', + 449 => 'Retry With', + 450 => 'Blocked by Windows Parental Controls', + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway ou Proxy Error', + 503 => 'Service Unavailable', + 504 => 'Gateway Time-out', + 505 => 'HTTP Version not supported', + 507 => 'Insufficient storage', + 509 => 'Bandwidth Limit Exceeded', + ); + + if(isset($httpCodes[$this->statusCode])) + return $httpCodes[$this->statusCode]; + else + return \Yii::t('yii|Error'); + } } diff --git a/framework/base/InlineAction.php b/framework/base/InlineAction.php index 4cd5413..c5afe28 100644 --- a/framework/base/InlineAction.php +++ b/framework/base/InlineAction.php @@ -1,9 +1,7 @@ controller, $this->actionMethod); - $args = $this->bindActionParams($method, $params); - return (int)$method->invokeArgs($this->controller, $args); + $args = $this->controller->bindActionParams($this, $params); + return (int)call_user_func_array(array($this->controller, $this->actionMethod), $args); } } diff --git a/framework/base/InvalidCallException.php b/framework/base/InvalidCallException.php index a1df021..9aefe14 100644 --- a/framework/base/InvalidCallException.php +++ b/framework/base/InvalidCallException.php @@ -1,9 +1,7 @@ * @since 2.0 */ -class InvalidCallException extends \Exception +class InvalidCallException extends Exception { /** * @return string the user-friendly name of this exception */ public function getName() { - return \Yii::t('yii', 'Invalid Call'); + return \Yii::t('yii|Invalid Call'); } } diff --git a/framework/base/InvalidConfigException.php b/framework/base/InvalidConfigException.php index 3c100d1..389737c 100644 --- a/framework/base/InvalidConfigException.php +++ b/framework/base/InvalidConfigException.php @@ -1,9 +1,7 @@ * @since 2.0 */ -class InvalidConfigException extends \Exception +class InvalidConfigException extends Exception { /** * @return string the user-friendly name of this exception */ public function getName() { - return \Yii::t('yii', 'Invalid Configuration'); + return \Yii::t('yii|Invalid Configuration'); } } diff --git a/framework/base/InvalidParamException.php b/framework/base/InvalidParamException.php new file mode 100644 index 0000000..a8c96fd --- /dev/null +++ b/framework/base/InvalidParamException.php @@ -0,0 +1,26 @@ + + * @since 2.0 + */ +class InvalidParamException extends Exception +{ + /** + * @return string the user-friendly name of this exception + */ + public function getName() + { + return \Yii::t('yii|Invalid Parameter'); + } +} + diff --git a/framework/base/InvalidRequestException.php b/framework/base/InvalidRequestException.php index fd468a1..6663e29 100644 --- a/framework/base/InvalidRequestException.php +++ b/framework/base/InvalidRequestException.php @@ -1,9 +1,7 @@ * @since 2.0 */ -class InvalidRequestException extends \Exception +class InvalidRequestException extends UserException { /** - * @var boolean whether this exception is caused by end user's mistake (e.g. wrong URL) - */ - public $causedByUser = true; - - /** * @return string the user-friendly name of this exception */ public function getName() { - return \Yii::t('yii', 'Invalid Request'); + return \Yii::t('yii|Invalid Request'); } } diff --git a/framework/base/InvalidRouteException.php b/framework/base/InvalidRouteException.php index e20b2b7..6d2256e 100644 --- a/framework/base/InvalidRouteException.php +++ b/framework/base/InvalidRouteException.php @@ -1,9 +1,7 @@ * @since 2.0 */ -class InvalidRouteException extends \Exception +class InvalidRouteException extends UserException { /** - * @var boolean whether this exception is caused by end user's mistake (e.g. wrong URL) - */ - public $causedByUser = true; - - /** * @return string the user-friendly name of this exception */ public function getName() { - return \Yii::t('yii', 'Invalid Route'); + return \Yii::t('yii|Invalid Route'); } } diff --git a/framework/base/Model.php b/framework/base/Model.php index 30cbcfa..13e567d 100644 --- a/framework/base/Model.php +++ b/framework/base/Model.php @@ -1,15 +1,13 @@ trigger(self::EVENT_BEFORE_VALIDATE, $event); return $event->isValid; } @@ -331,7 +329,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess foreach ($this->rules() as $rule) { if ($rule instanceof Validator) { $validators->add($rule); - } elseif (isset($rule[0], $rule[1])) { // attributes, validator type + } elseif (is_array($rule) && isset($rule[0], $rule[1])) { // attributes, validator type $validator = Validator::createValidator($rule[1], $this, $rule[0], array_slice($rule, 2)); $validators->add($validator); } else { @@ -422,12 +420,31 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess } /** + * Returns the first error of every attribute in the model. + * @return array the first errors. An empty array will be returned if there is no error. + */ + public function getFirstErrors() + { + if (empty($this->_errors)) { + return array(); + } else { + $errors = array(); + foreach ($this->_errors as $attributeErrors) { + if (isset($attributeErrors[0])) { + $errors[] = $attributeErrors[0]; + } + } + } + return $errors; + } + + /** * Returns the first error of the specified attribute. * @param string $attribute attribute name. * @return string the error message. Null is returned if no error. * @see getErrors */ - public function getError($attribute) + public function getFirstError($attribute) { return isset($this->_errors[$attribute]) ? reset($this->_errors[$attribute]) : null; } @@ -443,25 +460,6 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess } /** - * Adds a list of errors. - * @param array $errors a list of errors. The array keys must be attribute names. - * The array values should be error messages. If an attribute has multiple errors, - * these errors must be given in terms of an array. - */ - public function addErrors($errors) - { - foreach ($errors as $attribute => $error) { - if (is_array($error)) { - foreach ($error as $e) { - $this->_errors[$attribute][] = $e; - } - } else { - $this->_errors[$attribute][] = $error; - } - } - } - - /** * Removes errors for all attributes or a single attribute. * @param string $attribute attribute name. Use null to remove errors for all attribute. */ @@ -543,7 +541,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess public function onUnsafeAttribute($name, $value) { if (YII_DEBUG) { - \Yii::warning("Failed to set unsafe attribute '$name' in '" . get_class($this) . "'."); + \Yii::info("Failed to set unsafe attribute '$name' in '" . get_class($this) . "'.", __CLASS__); } } @@ -658,13 +656,13 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess } /** - * Unsets the element at the specified offset. + * Sets the element value at the specified offset to null. * This method is required by the SPL interface `ArrayAccess`. * It is implicitly called when you use something like `unset($model[$offset])`. * @param mixed $offset the offset to unset element */ public function offsetUnset($offset) { - unset($this->$offset); + $this->$offset = null; } } diff --git a/framework/base/ModelEvent.php b/framework/base/ModelEvent.php index e7b6a2e..57e41f9 100644 --- a/framework/base/ModelEvent.php +++ b/framework/base/ModelEvent.php @@ -1,9 +1,7 @@ configuration). - * @property array $components The components (indexed by their IDs) registered within this module. - * @property array $import List of aliases to be imported. This property is write-only. - * @property array $aliases List of aliases to be defined. This property is write-only. - * - * @author Qiang Xue - * @since 2.0 - */ -abstract class Module extends Component -{ - /** - * @var array custom module parameters (name => value). - */ - public $params = array(); - /** - * @var array the IDs of the components that should be preloaded when this module is created. - */ - public $preload = array(); - /** - * @var string an ID that uniquely identifies this module among other modules which have the same [[module|parent]]. - */ - public $id; - /** - * @var Module the parent module of this module. Null if this module does not have a parent. - */ - public $module; - /** - * @var string|boolean the layout that should be applied for views within this module. This refers to a view name - * relative to [[layoutPath]]. If this is not set, it means the layout value of the [[module|parent module]] - * will be taken. If this is false, layout will be disabled within this module. - */ - public $layout; - /** - * @var array mapping from controller ID to controller configurations. - * Each name-value pair specifies the configuration of a single controller. - * A controller configuration can be either a string or an array. - * If the former, the string should be the class name or path alias of the controller. - * If the latter, the array must contain a 'class' element which specifies - * the controller's class name or path alias, and the rest of the name-value pairs - * in the array are used to initialize the corresponding controller properties. For example, - * - * ~~~ - * array( - * 'account' => '@application/controllers/UserController', - * 'article' => array( - * 'class' => '@application/controllers/PostController', - * 'pageTitle' => 'something new', - * ), - * ) - * ~~~ - */ - public $controllerMap = array(); - /** - * @var string the namespace that controller classes are in. Default is to use global namespace. - */ - public $controllerNamespace; - /** - * @return string the default route of this module. Defaults to 'default'. - * The route may consist of child module ID, controller ID, and/or action ID. - * For example, `help`, `post/create`, `admin/post/create`. - * If action ID is not given, it will take the default value as specified in - * [[Controller::defaultAction]]. - */ - public $defaultRoute = 'default'; - /** - * @var string the root directory of the module. - */ - private $_basePath; - /** - * @var string the root directory that contains view files for this module - */ - private $_viewPath; - /** - * @var string the root directory that contains layout view files for this module. - */ - private $_layoutPath; - /** - * @var string the directory containing controller classes in the module. - */ - private $_controllerPath; - /** - * @var array child modules of this module - */ - private $_modules = array(); - /** - * @var array components registered under this module - */ - private $_components = array(); - - /** - * Constructor. - * @param string $id the ID of this module - * @param Module $parent the parent module (if any) - * @param array $config name-value pairs that will be used to initialize the object properties - */ - public function __construct($id, $parent = null, $config = array()) - { - $this->id = $id; - $this->module = $parent; - parent::__construct($config); - } - - /** - * Getter magic method. - * This method is overridden to support accessing components - * like reading module properties. - * @param string $name component or property name - * @return mixed the named property value - */ - public function __get($name) - { - if ($this->hasComponent($name)) { - return $this->getComponent($name); - } else { - return parent::__get($name); - } - } - - /** - * Checks if a property value is null. - * This method overrides the parent implementation by checking - * if the named component is loaded. - * @param string $name the property name or the event name - * @return boolean whether the property value is null - */ - public function __isset($name) - { - if ($this->hasComponent($name)) { - return $this->getComponent($name) !== null; - } else { - return parent::__isset($name); - } - } - - /** - * Initializes the module. - * This method is called after the module is created and initialized with property values - * given in configuration. The default implement will create a path alias using the module [[id]] - * and then call [[preloadComponents()]] to load components that are declared in [[preload]]. - */ - public function init() - { - Yii::setAlias('@' . $this->id, $this->getBasePath()); - $this->preloadComponents(); - } - - /** - * Returns an ID that uniquely identifies this module among all modules within the current application. - * Note that if the module is an application, an empty string will be returned. - * @return string the unique ID of the module. - */ - public function getUniqueId() - { - if ($this instanceof Application) { - return ''; - } elseif ($this->module) { - return $this->module->getUniqueId() . '/' . $this->id; - } else { - return $this->id; - } - } - - /** - * Returns the root directory of the module. - * It defaults to the directory containing the module class file. - * @return string the root directory of the module. - */ - public function getBasePath() - { - if ($this->_basePath === null) { - $class = new \ReflectionClass($this); - $this->_basePath = dirname($class->getFileName()); - } - return $this->_basePath; - } - - /** - * Sets the root directory of the module. - * This method can only be invoked at the beginning of the constructor. - * @param string $path the root directory of the module. This can be either a directory name or a path alias. - * @throws Exception if the directory does not exist. - */ - public function setBasePath($path) - { - $this->_basePath = FileHelper::ensureDirectory($path); - } - - /** - * Returns the directory that contains the controller classes. - * Defaults to "[[basePath]]/controllers". - * @return string the directory that contains the controller classes. - */ - public function getControllerPath() - { - if ($this->_controllerPath !== null) { - return $this->_controllerPath; - } else { - return $this->_controllerPath = $this->getBasePath() . DIRECTORY_SEPARATOR . 'controllers'; - } - } - - /** - * Sets the directory that contains the controller classes. - * @param string $path the directory that contains the controller classes. - * This can be either a directory name or a path alias. - * @throws Exception if the directory is invalid - */ - public function setControllerPath($path) - { - $this->_controllerPath = FileHelper::ensureDirectory($path); - } - - /** - * Returns the directory that contains the view files for this module. - * @return string the root directory of view files. Defaults to "[[basePath]]/view". - */ - public function getViewPath() - { - if ($this->_viewPath !== null) { - return $this->_viewPath; - } else { - return $this->_viewPath = $this->getBasePath() . DIRECTORY_SEPARATOR . 'views'; - } - } - - /** - * Sets the directory that contains the view files. - * @param string $path the root directory of view files. - * @throws Exception if the directory is invalid - */ - public function setViewPath($path) - { - $this->_viewPath = FileHelper::ensureDirectory($path); - } - - /** - * Returns the directory that contains layout view files for this module. - * @return string the root directory of layout files. Defaults to "[[viewPath]]/layouts". - */ - public function getLayoutPath() - { - if ($this->_layoutPath !== null) { - return $this->_layoutPath; - } else { - return $this->_layoutPath = $this->getViewPath() . DIRECTORY_SEPARATOR . 'layouts'; - } - } - - /** - * Sets the directory that contains the layout files. - * @param string $path the root directory of layout files. - * @throws Exception if the directory is invalid - */ - public function setLayoutPath($path) - { - $this->_layoutPath = FileHelper::ensureDirectory($path); - } - - /** - * Imports the specified path aliases. - * This method is provided so that you can import a set of path aliases when configuring a module. - * The path aliases will be imported by calling [[Yii::import()]]. - * @param array $aliases list of path aliases to be imported - */ - public function setImport($aliases) - { - foreach ($aliases as $alias) { - Yii::import($alias); - } - } - - /** - * Defines path aliases. - * This method calls [[Yii::setAlias()]] to register the path aliases. - * This method is provided so that you can define path aliases when configuring a module. - * @param array $aliases list of path aliases to be defined. The array keys are alias names - * (must start with '@') and the array values are the corresponding paths or aliases. - * For example, - * - * ~~~ - * array( - * '@models' => '@application/models', // an existing alias - * '@backend' => __DIR__ . '/../backend', // a directory - * ) - * ~~~ - */ - public function setAliases($aliases) - { - foreach ($aliases as $name => $alias) { - Yii::setAlias($name, $alias); - } - } - - /** - * Checks whether the named module exists. - * @param string $id module ID - * @return boolean whether the named module exists. Both loaded and unloaded modules - * are considered. - */ - public function hasModule($id) - { - return isset($this->_modules[$id]); - } - - /** - * Retrieves the named module. - * @param string $id module ID (case-sensitive) - * @param boolean $load whether to load the module if it is not yet loaded. - * @return Module|null the module instance, null if the module - * does not exist. - * @see hasModule() - */ - public function getModule($id, $load = true) - { - if (isset($this->_modules[$id])) { - if ($this->_modules[$id] instanceof Module) { - return $this->_modules[$id]; - } elseif ($load) { - Yii::trace("Loading module: $id", __CLASS__); - return $this->_modules[$id] = Yii::createObject($this->_modules[$id], $id, $this); - } - } - return null; - } - - /** - * Adds a sub-module to this module. - * @param string $id module ID - * @param Module|array|null $module the sub-module to be added to this module. This can - * be one of the followings: - * - * - a [[Module]] object - * - a configuration array: when [[getModule()]] is called initially, the array - * will be used to instantiate the sub-module - * - null: the named sub-module will be removed from this module - */ - public function setModule($id, $module) - { - if ($module === null) { - unset($this->_modules[$id]); - } else { - $this->_modules[$id] = $module; - } - } - - /** - * Returns the sub-modules in this module. - * @param boolean $loadedOnly whether to return the loaded sub-modules only. If this is set false, - * then all sub-modules registered in this module will be returned, whether they are loaded or not. - * Loaded modules will be returned as objects, while unloaded modules as configuration arrays. - * @return array the modules (indexed by their IDs) - */ - public function getModules($loadedOnly = false) - { - if ($loadedOnly) { - $modules = array(); - foreach ($this->_modules as $module) { - if ($module instanceof Module) { - $modules[] = $module; - } - } - return $modules; - } else { - return $this->_modules; - } - } - - /** - * Registers sub-modules in the current module. - * - * Each sub-module should be specified as a name-value pair, where - * name refers to the ID of the module and value the module or a configuration - * array that can be used to create the module. In the latter case, [[Yii::createObject()]] - * will be used to create the module. - * - * If a new sub-module has the same ID as an existing one, the existing one will be overwritten silently. - * - * The following is an example for registering two sub-modules: - * - * ~~~ - * array( - * 'comment' => array( - * 'class' => 'app\modules\CommentModule', - * 'connectionID' => 'db', - * ), - * 'booking' => array( - * 'class' => 'app\modules\BookingModule', - * ), - * ) - * ~~~ - * - * @param array $modules modules (id => module configuration or instances) - */ - public function setModules($modules) - { - foreach ($modules as $id => $module) { - $this->_modules[$id] = $module; - } - } - - /** - * Checks whether the named component exists. - * @param string $id component ID - * @return boolean whether the named component exists. Both loaded and unloaded components - * are considered. - */ - public function hasComponent($id) - { - return isset($this->_components[$id]); - } - - /** - * Retrieves the named component. - * @param string $id component ID (case-sensitive) - * @param boolean $load whether to load the component if it is not yet loaded. - * @return Component|null the component instance, null if the component does not exist. - * @see hasComponent() - */ - public function getComponent($id, $load = true) - { - if (isset($this->_components[$id])) { - if ($this->_components[$id] instanceof Component) { - return $this->_components[$id]; - } elseif ($load) { - Yii::trace("Loading component: $id", __CLASS__); - return $this->_components[$id] = Yii::createObject($this->_components[$id]); - } - } - return null; - } - - /** - * Registers a component with this module. - * @param string $id component ID - * @param Component|array|null $component the component to be registered with the module. This can - * be one of the followings: - * - * - a [[Component]] object - * - a configuration array: when [[getComponent()]] is called initially for this component, the array - * will be used to instantiate the component via [[Yii::createObject()]]. - * - null: the named component will be removed from the module - */ - public function setComponent($id, $component) - { - if ($component === null) { - unset($this->_components[$id]); - } else { - $this->_components[$id] = $component; - } - } - - /** - * Returns the registered components. - * @param boolean $loadedOnly whether to return the loaded components only. If this is set false, - * then all components specified in the configuration will be returned, whether they are loaded or not. - * Loaded components will be returned as objects, while unloaded components as configuration arrays. - * @return array the components (indexed by their IDs) - */ - public function getComponents($loadedOnly = false) - { - if ($loadedOnly) { - $components = array(); - foreach ($this->_components as $component) { - if ($component instanceof Component) { - $components[] = $component; - } - } - return $components; - } else { - return $this->_components; - } - } - - /** - * Registers a set of components in this module. - * - * Each component should be specified as a name-value pair, where - * name refers to the ID of the component and value the component or a configuration - * array that can be used to create the component. In the latter case, [[Yii::createObject()]] - * will be used to create the component. - * - * If a new component has the same ID as an existing one, the existing one will be overwritten silently. - * - * The following is an example for setting two components: - * - * ~~~ - * array( - * 'db' => array( - * 'class' => 'yii\db\Connection', - * 'dsn' => 'sqlite:path/to/file.db', - * ), - * 'cache' => array( - * 'class' => 'yii\caching\DbCache', - * 'connectionID' => 'db', - * ), - * ) - * ~~~ - * - * @param array $components components (id => component configuration or instance) - */ - public function setComponents($components) - { - foreach ($components as $id => $component) { - $this->_components[$id] = $component; - } - } - - /** - * Loads components that are declared in [[preload]]. - */ - public function preloadComponents() - { - foreach ($this->preload as $id) { - $this->getComponent($id); - } - } - - /** - * Runs a controller action specified by a route. - * This method parses the specified route and creates the corresponding child module(s), controller and action - * instances. It then calls [[Controller::runAction()]] to run the action with the given parameters. - * If the route is empty, the method will use [[defaultRoute]]. - * @param string $route the route that specifies the action. - * @param array $params the parameters to be passed to the action - * @return integer the status code returned by the action execution. 0 means normal, and other values mean abnormal. - * @throws InvalidRouteException if the requested route cannot be resolved into an action successfully - */ - public function runAction($route, $params = array()) - { - $result = $this->createController($route); - if (is_array($result)) { - /** @var $controller Controller */ - list($controller, $actionID) = $result; - $oldController = Yii::$application->controller; - Yii::$application->controller = $controller; - $status = $controller->runAction($actionID, $params); - Yii::$application->controller = $oldController; - return $status; - } else { - throw new InvalidRouteException('Unable to resolve the request: ' . trim($this->getUniqueId() . '/' . $route, '/')); - } - } - - /** - * Creates a controller instance based on the controller ID. - * - * The controller is created within this module. The method first attempts to - * create the controller based on the [[controllerMap]] of the module. If not available, - * it will look for the controller class under the [[controllerPath]] and create an - * instance of it. - * - * @param string $route the route consisting of module, controller and action IDs. - * @return array|boolean if the controller is created successfully, it will be returned together - * with the remainder of the route which represents the action ID. Otherwise false will be returned. - */ - public function createController($route) - { - if ($route === '') { - $route = $this->defaultRoute; - } - if (($pos = strpos($route, '/')) !== false) { - $id = substr($route, 0, $pos); - $route = substr($route, $pos + 1); - } else { - $id = $route; - $route = ''; - } - - $module = $this->getModule($id); - if ($module !== null) { - return $module->createController($route); - } - - if (isset($this->controllerMap[$id])) { - $controller = Yii::createObject($this->controllerMap[$id], $id, $this); - } elseif (preg_match('/^[a-z0-9\\-_]+$/', $id)) { - $className = StringHelper::id2camel($id) . 'Controller'; - $classFile = $this->controllerPath . DIRECTORY_SEPARATOR . $className . '.php'; - if (is_file($classFile)) { - $className = $this->controllerNamespace . '\\' . $className; - if (!class_exists($className, false)) { - require($classFile); - } - if (class_exists($className, false) && is_subclass_of($className, '\yii\base\Controller')) { - $controller = new $className($id, $this); - } - } - } - - return isset($controller) ? array($controller, $route) : false; - } -} + configuration). + * @property array $components The components (indexed by their IDs) registered within this module. + * @property array $import List of aliases to be imported. This property is write-only. + * @property array $aliases List of aliases to be defined. This property is write-only. + * + * @author Qiang Xue + * @since 2.0 + */ +abstract class Module extends Component +{ + /** + * @var array custom module parameters (name => value). + */ + public $params = array(); + /** + * @var array the IDs of the components that should be preloaded when this module is created. + */ + public $preload = array(); + /** + * @var string an ID that uniquely identifies this module among other modules which have the same [[module|parent]]. + */ + public $id; + /** + * @var Module the parent module of this module. Null if this module does not have a parent. + */ + public $module; + /** + * @var string|boolean the layout that should be applied for views within this module. This refers to a view name + * relative to [[layoutPath]]. If this is not set, it means the layout value of the [[module|parent module]] + * will be taken. If this is false, layout will be disabled within this module. + */ + public $layout; + /** + * @var array mapping from controller ID to controller configurations. + * Each name-value pair specifies the configuration of a single controller. + * A controller configuration can be either a string or an array. + * If the former, the string should be the class name or path alias of the controller. + * If the latter, the array must contain a 'class' element which specifies + * the controller's class name or path alias, and the rest of the name-value pairs + * in the array are used to initialize the corresponding controller properties. For example, + * + * ~~~ + * array( + * 'account' => '@app/controllers/UserController', + * 'article' => array( + * 'class' => '@app/controllers/PostController', + * 'pageTitle' => 'something new', + * ), + * ) + * ~~~ + */ + public $controllerMap = array(); + /** + * @var string the namespace that controller classes are in. Default is to use global namespace. + */ + public $controllerNamespace; + /** + * @return string the default route of this module. Defaults to 'default'. + * The route may consist of child module ID, controller ID, and/or action ID. + * For example, `help`, `post/create`, `admin/post/create`. + * If action ID is not given, it will take the default value as specified in + * [[Controller::defaultAction]]. + */ + public $defaultRoute = 'default'; + /** + * @var string the root directory of the module. + */ + private $_basePath; + /** + * @var string the root directory that contains view files for this module + */ + private $_viewPath; + /** + * @var string the root directory that contains layout view files for this module. + */ + private $_layoutPath; + /** + * @var string the directory containing controller classes in the module. + */ + private $_controllerPath; + /** + * @var array child modules of this module + */ + private $_modules = array(); + /** + * @var array components registered under this module + */ + private $_components = array(); + + /** + * Constructor. + * @param string $id the ID of this module + * @param Module $parent the parent module (if any) + * @param array $config name-value pairs that will be used to initialize the object properties + */ + public function __construct($id, $parent = null, $config = array()) + { + $this->id = $id; + $this->module = $parent; + parent::__construct($config); + } + + /** + * Getter magic method. + * This method is overridden to support accessing components + * like reading module properties. + * @param string $name component or property name + * @return mixed the named property value + */ + public function __get($name) + { + if ($this->hasComponent($name)) { + return $this->getComponent($name); + } else { + return parent::__get($name); + } + } + + /** + * Checks if a property value is null. + * This method overrides the parent implementation by checking + * if the named component is loaded. + * @param string $name the property name or the event name + * @return boolean whether the property value is null + */ + public function __isset($name) + { + if ($this->hasComponent($name)) { + return $this->getComponent($name) !== null; + } else { + return parent::__isset($name); + } + } + + /** + * Initializes the module. + * This method is called after the module is created and initialized with property values + * given in configuration. The default implement will create a path alias using the module [[id]] + * and then call [[preloadComponents()]] to load components that are declared in [[preload]]. + */ + public function init() + { + Yii::setAlias('@' . $this->id, $this->getBasePath()); + $this->preloadComponents(); + } + + /** + * Returns an ID that uniquely identifies this module among all modules within the current application. + * Note that if the module is an application, an empty string will be returned. + * @return string the unique ID of the module. + */ + public function getUniqueId() + { + if ($this instanceof Application) { + return ''; + } elseif ($this->module) { + return $this->module->getUniqueId() . '/' . $this->id; + } else { + return $this->id; + } + } + + /** + * Returns the root directory of the module. + * It defaults to the directory containing the module class file. + * @return string the root directory of the module. + */ + public function getBasePath() + { + if ($this->_basePath === null) { + $class = new \ReflectionClass($this); + $this->_basePath = dirname($class->getFileName()); + } + return $this->_basePath; + } + + /** + * Sets the root directory of the module. + * This method can only be invoked at the beginning of the constructor. + * @param string $path the root directory of the module. This can be either a directory name or a path alias. + * @throws Exception if the directory does not exist. + */ + public function setBasePath($path) + { + $this->_basePath = FileHelper::ensureDirectory($path); + } + + /** + * Returns the directory that contains the controller classes. + * Defaults to "[[basePath]]/controllers". + * @return string the directory that contains the controller classes. + */ + public function getControllerPath() + { + if ($this->_controllerPath !== null) { + return $this->_controllerPath; + } else { + return $this->_controllerPath = $this->getBasePath() . DIRECTORY_SEPARATOR . 'controllers'; + } + } + + /** + * Sets the directory that contains the controller classes. + * @param string $path the directory that contains the controller classes. + * This can be either a directory name or a path alias. + * @throws Exception if the directory is invalid + */ + public function setControllerPath($path) + { + $this->_controllerPath = FileHelper::ensureDirectory($path); + } + + /** + * Returns the directory that contains the view files for this module. + * @return string the root directory of view files. Defaults to "[[basePath]]/view". + */ + public function getViewPath() + { + if ($this->_viewPath !== null) { + return $this->_viewPath; + } else { + return $this->_viewPath = $this->getBasePath() . DIRECTORY_SEPARATOR . 'views'; + } + } + + /** + * Sets the directory that contains the view files. + * @param string $path the root directory of view files. + * @throws Exception if the directory is invalid + */ + public function setViewPath($path) + { + $this->_viewPath = FileHelper::ensureDirectory($path); + } + + /** + * Returns the directory that contains layout view files for this module. + * @return string the root directory of layout files. Defaults to "[[viewPath]]/layouts". + */ + public function getLayoutPath() + { + if ($this->_layoutPath !== null) { + return $this->_layoutPath; + } else { + return $this->_layoutPath = $this->getViewPath() . DIRECTORY_SEPARATOR . 'layouts'; + } + } + + /** + * Sets the directory that contains the layout files. + * @param string $path the root directory of layout files. + * @throws Exception if the directory is invalid + */ + public function setLayoutPath($path) + { + $this->_layoutPath = FileHelper::ensureDirectory($path); + } + + /** + * Imports the specified path aliases. + * This method is provided so that you can import a set of path aliases when configuring a module. + * The path aliases will be imported by calling [[Yii::import()]]. + * @param array $aliases list of path aliases to be imported + */ + public function setImport($aliases) + { + foreach ($aliases as $alias) { + Yii::import($alias); + } + } + + /** + * Defines path aliases. + * This method calls [[Yii::setAlias()]] to register the path aliases. + * This method is provided so that you can define path aliases when configuring a module. + * @param array $aliases list of path aliases to be defined. The array keys are alias names + * (must start with '@') and the array values are the corresponding paths or aliases. + * For example, + * + * ~~~ + * array( + * '@models' => '@app/models', // an existing alias + * '@backend' => __DIR__ . '/../backend', // a directory + * ) + * ~~~ + */ + public function setAliases($aliases) + { + foreach ($aliases as $name => $alias) { + Yii::setAlias($name, $alias); + } + } + + /** + * Checks whether the named module exists. + * @param string $id module ID + * @return boolean whether the named module exists. Both loaded and unloaded modules + * are considered. + */ + public function hasModule($id) + { + return isset($this->_modules[$id]); + } + + /** + * Retrieves the named module. + * @param string $id module ID (case-sensitive) + * @param boolean $load whether to load the module if it is not yet loaded. + * @return Module|null the module instance, null if the module + * does not exist. + * @see hasModule() + */ + public function getModule($id, $load = true) + { + if (isset($this->_modules[$id])) { + if ($this->_modules[$id] instanceof Module) { + return $this->_modules[$id]; + } elseif ($load) { + Yii::trace("Loading module: $id", __CLASS__); + return $this->_modules[$id] = Yii::createObject($this->_modules[$id], $id, $this); + } + } + return null; + } + + /** + * Adds a sub-module to this module. + * @param string $id module ID + * @param Module|array|null $module the sub-module to be added to this module. This can + * be one of the followings: + * + * - a [[Module]] object + * - a configuration array: when [[getModule()]] is called initially, the array + * will be used to instantiate the sub-module + * - null: the named sub-module will be removed from this module + */ + public function setModule($id, $module) + { + if ($module === null) { + unset($this->_modules[$id]); + } else { + $this->_modules[$id] = $module; + } + } + + /** + * Returns the sub-modules in this module. + * @param boolean $loadedOnly whether to return the loaded sub-modules only. If this is set false, + * then all sub-modules registered in this module will be returned, whether they are loaded or not. + * Loaded modules will be returned as objects, while unloaded modules as configuration arrays. + * @return array the modules (indexed by their IDs) + */ + public function getModules($loadedOnly = false) + { + if ($loadedOnly) { + $modules = array(); + foreach ($this->_modules as $module) { + if ($module instanceof Module) { + $modules[] = $module; + } + } + return $modules; + } else { + return $this->_modules; + } + } + + /** + * Registers sub-modules in the current module. + * + * Each sub-module should be specified as a name-value pair, where + * name refers to the ID of the module and value the module or a configuration + * array that can be used to create the module. In the latter case, [[Yii::createObject()]] + * will be used to create the module. + * + * If a new sub-module has the same ID as an existing one, the existing one will be overwritten silently. + * + * The following is an example for registering two sub-modules: + * + * ~~~ + * array( + * 'comment' => array( + * 'class' => 'app\modules\CommentModule', + * 'db' => 'db', + * ), + * 'booking' => array( + * 'class' => 'app\modules\BookingModule', + * ), + * ) + * ~~~ + * + * @param array $modules modules (id => module configuration or instances) + */ + public function setModules($modules) + { + foreach ($modules as $id => $module) { + $this->_modules[$id] = $module; + } + } + + /** + * Checks whether the named component exists. + * @param string $id component ID + * @return boolean whether the named component exists. Both loaded and unloaded components + * are considered. + */ + public function hasComponent($id) + { + return isset($this->_components[$id]); + } + + /** + * Retrieves the named component. + * @param string $id component ID (case-sensitive) + * @param boolean $load whether to load the component if it is not yet loaded. + * @return Component|null the component instance, null if the component does not exist. + * @see hasComponent() + */ + public function getComponent($id, $load = true) + { + if (isset($this->_components[$id])) { + if ($this->_components[$id] instanceof Component) { + return $this->_components[$id]; + } elseif ($load) { + Yii::trace("Loading component: $id", __CLASS__); + return $this->_components[$id] = Yii::createObject($this->_components[$id]); + } + } + return null; + } + + /** + * Registers a component with this module. + * @param string $id component ID + * @param Component|array|null $component the component to be registered with the module. This can + * be one of the followings: + * + * - a [[Component]] object + * - a configuration array: when [[getComponent()]] is called initially for this component, the array + * will be used to instantiate the component via [[Yii::createObject()]]. + * - null: the named component will be removed from the module + */ + public function setComponent($id, $component) + { + if ($component === null) { + unset($this->_components[$id]); + } else { + $this->_components[$id] = $component; + } + } + + /** + * Returns the registered components. + * @param boolean $loadedOnly whether to return the loaded components only. If this is set false, + * then all components specified in the configuration will be returned, whether they are loaded or not. + * Loaded components will be returned as objects, while unloaded components as configuration arrays. + * @return array the components (indexed by their IDs) + */ + public function getComponents($loadedOnly = false) + { + if ($loadedOnly) { + $components = array(); + foreach ($this->_components as $component) { + if ($component instanceof Component) { + $components[] = $component; + } + } + return $components; + } else { + return $this->_components; + } + } + + /** + * Registers a set of components in this module. + * + * Each component should be specified as a name-value pair, where + * name refers to the ID of the component and value the component or a configuration + * array that can be used to create the component. In the latter case, [[Yii::createObject()]] + * will be used to create the component. + * + * If a new component has the same ID as an existing one, the existing one will be overwritten silently. + * + * The following is an example for setting two components: + * + * ~~~ + * array( + * 'db' => array( + * 'class' => 'yii\db\Connection', + * 'dsn' => 'sqlite:path/to/file.db', + * ), + * 'cache' => array( + * 'class' => 'yii\caching\DbCache', + * 'db' => 'db', + * ), + * ) + * ~~~ + * + * @param array $components components (id => component configuration or instance) + */ + public function setComponents($components) + { + foreach ($components as $id => $component) { + $this->_components[$id] = $component; + } + } + + /** + * Loads components that are declared in [[preload]]. + */ + public function preloadComponents() + { + foreach ($this->preload as $id) { + $this->getComponent($id); + } + } + + /** + * Runs a controller action specified by a route. + * This method parses the specified route and creates the corresponding child module(s), controller and action + * instances. It then calls [[Controller::runAction()]] to run the action with the given parameters. + * If the route is empty, the method will use [[defaultRoute]]. + * @param string $route the route that specifies the action. + * @param array $params the parameters to be passed to the action + * @return integer the status code returned by the action execution. 0 means normal, and other values mean abnormal. + * @throws InvalidRouteException if the requested route cannot be resolved into an action successfully + */ + public function runAction($route, $params = array()) + { + $result = $this->createController($route); + if (is_array($result)) { + /** @var $controller Controller */ + list($controller, $actionID) = $result; + $oldController = Yii::$app->controller; + Yii::$app->controller = $controller; + $status = $controller->runAction($actionID, $params); + Yii::$app->controller = $oldController; + return $status; + } else { + throw new InvalidRouteException('Unable to resolve the request "' . trim($this->getUniqueId() . '/' . $route, '/') . '".'); + } + } + + /** + * Creates a controller instance based on the controller ID. + * + * The controller is created within this module. The method first attempts to + * create the controller based on the [[controllerMap]] of the module. If not available, + * it will look for the controller class under the [[controllerPath]] and create an + * instance of it. + * + * @param string $route the route consisting of module, controller and action IDs. + * @return array|boolean if the controller is created successfully, it will be returned together + * with the remainder of the route which represents the action ID. Otherwise false will be returned. + */ + public function createController($route) + { + if ($route === '') { + $route = $this->defaultRoute; + } + if (($pos = strpos($route, '/')) !== false) { + $id = substr($route, 0, $pos); + $route = substr($route, $pos + 1); + } else { + $id = $route; + $route = ''; + } + + $module = $this->getModule($id); + if ($module !== null) { + return $module->createController($route); + } + + if (isset($this->controllerMap[$id])) { + $controller = Yii::createObject($this->controllerMap[$id], $id, $this); + } elseif (preg_match('/^[a-z0-9\\-_]+$/', $id)) { + $className = StringHelper::id2camel($id) . 'Controller'; + + $classFile = $this->controllerPath . DIRECTORY_SEPARATOR . $className . '.php'; + if (is_file($classFile)) { + $className = $this->controllerNamespace . '\\' . $className; + if (!class_exists($className, false)) { + require($classFile); + } + if (class_exists($className, false) && is_subclass_of($className, '\yii\base\Controller')) { + $controller = new $className($id, $this); + } + } + } + + return isset($controller) ? array($controller, $route) : false; + } +} diff --git a/framework/base/NotSupportedException.php b/framework/base/NotSupportedException.php index 56e7e36..2f08891 100644 --- a/framework/base/NotSupportedException.php +++ b/framework/base/NotSupportedException.php @@ -1,9 +1,7 @@ * @since 2.0 */ -class NotSupportedException extends \Exception +class NotSupportedException extends Exception { /** * @return string the user-friendly name of this exception */ public function getName() { - return \Yii::t('yii', 'Not Supported'); + return \Yii::t('yii|Not Supported'); } } diff --git a/framework/base/Object.php b/framework/base/Object.php index a3425dc..3bd8378 100644 --- a/framework/base/Object.php +++ b/framework/base/Object.php @@ -1,9 +1,7 @@ $getter(); } else { - throw new UnknownPropertyException('Getting unknown property: ' . get_class($this) . '.' . $name); + throw new UnknownPropertyException('Getting unknown property: ' . get_class($this) . '::' . $name); } } @@ -88,9 +86,9 @@ class Object if (method_exists($this, $setter)) { $this->$setter($value); } elseif (method_exists($this, 'get' . $name)) { - throw new InvalidCallException('Setting read-only property: ' . get_class($this) . '.' . $name); + throw new InvalidCallException('Setting read-only property: ' . get_class($this) . '::' . $name); } else { - throw new UnknownPropertyException('Setting unknown property: ' . get_class($this) . '.' . $name); + throw new UnknownPropertyException('Setting unknown property: ' . get_class($this) . '::' . $name); } } @@ -131,7 +129,7 @@ class Object if (method_exists($this, $setter)) { $this->$setter(null); } elseif (method_exists($this, 'get' . $name)) { - throw new InvalidCallException('Unsetting read-only property: ' . get_class($this) . '.' . $name); + throw new InvalidCallException('Unsetting read-only property: ' . get_class($this) . '::' . $name); } } diff --git a/framework/base/Request.php b/framework/base/Request.php index 0dbc568..45556ab 100644 --- a/framework/base/Request.php +++ b/framework/base/Request.php @@ -1,9 +1,7 @@ * @since 2.0 */ -class Request extends Component +abstract class Request extends Component { private $_scriptFile; private $_isConsoleRequest; /** + * Resolves the current request into a route and the associated parameters. + * @return array the first element is the route, and the second is the associated parameters. + */ + abstract public function resolve(); + + /** * Returns a value indicating whether the current request is made via command line * @return boolean the value indicating whether the current request is made via console */ @@ -39,24 +43,35 @@ class Request extends Component /** * Returns entry script file path. * @return string entry script file path (processed w/ realpath()) + * @throws InvalidConfigException if the entry script file path cannot be determined automatically. */ public function getScriptFile() { if ($this->_scriptFile === null) { - $this->_scriptFile = realpath($_SERVER['SCRIPT_FILENAME']); + if (isset($_SERVER['SCRIPT_FILENAME'])) { + $this->setScriptFile($_SERVER['SCRIPT_FILENAME']); + } else { + throw new InvalidConfigException('Unable to determine the entry script file path.'); + } } return $this->_scriptFile; } /** * Sets the entry script file path. - * This can be an absolute or relative file path, or a path alias. - * Note that you normally do not have to set the script file path - * as [[getScriptFile()]] can determine it based on `$_SERVER['SCRIPT_FILENAME']`. - * @param string $value the entry script file + * The entry script file path can normally be determined based on the `SCRIPT_FILENAME` SERVER variable. + * However, for some server configurations, this may not be correct or feasible. + * This setter is provided so that the entry script file path can be manually specified. + * @param string $value the entry script file path. This can be either a file path or a path alias. + * @throws InvalidConfigException if the provided entry script file path is invalid. */ public function setScriptFile($value) { - $this->_scriptFile = realpath(\Yii::getAlias($value)); + $scriptFile = realpath(\Yii::getAlias($value)); + if ($scriptFile !== false && is_file($scriptFile)) { + $this->_scriptFile = $scriptFile; + } else { + throw new InvalidConfigException('Unable to determine the entry script file path.'); + } } } diff --git a/framework/base/Response.php b/framework/base/Response.php index 3ced584..a53fd61 100644 --- a/framework/base/Response.php +++ b/framework/base/Response.php @@ -1,9 +1,7 @@ - * @since 2.0 - */ -class SecurityManager extends Component -{ - const STATE_VALIDATION_KEY = 'Yii.SecurityManager.validationkey'; - const STATE_ENCRYPTION_KEY = 'Yii.SecurityManager.encryptionkey'; - - /** - * @var string the name of the hashing algorithm to be used by {@link computeHMAC}. - * See {@link http://php.net/manual/en/function.hash-algos.php hash-algos} for the list of possible - * hash algorithms. Note that if you are using PHP 5.1.1 or below, you can only use 'sha1' or 'md5'. - * - * Defaults to 'sha1', meaning using SHA1 hash algorithm. - */ - public $hashAlgorithm = 'sha1'; - /** - * @var mixed the name of the crypt algorithm to be used by {@link encrypt} and {@link decrypt}. - * This will be passed as the first parameter to {@link http://php.net/manual/en/function.mcrypt-module-open.php mcrypt_module_open}. - * - * This property can also be configured as an array. In this case, the array elements will be passed in order - * as parameters to mcrypt_module_open. For example, array('rijndael-256', '', 'ofb', ''). - * - * Defaults to 'des', meaning using DES crypt algorithm. - */ - public $cryptAlgorithm = 'des'; - - private $_validationKey; - private $_encryptionKey; - - /** - * @return string a randomly generated private key - */ - protected function generateRandomKey() - { - return sprintf('%08x%08x%08x%08x', mt_rand(), mt_rand(), mt_rand(), mt_rand()); - } - - /** - * @return string the private key used to generate HMAC. - * If the key is not explicitly set, a random one is generated and returned. - */ - public function getValidationKey() - { - if ($this->_validationKey !== null) { - return $this->_validationKey; - } else { - if (($key = \Yii::$application->getGlobalState(self::STATE_VALIDATION_KEY)) !== null) { - $this->setValidationKey($key); - } else { - $key = $this->generateRandomKey(); - $this->setValidationKey($key); - \Yii::$application->setGlobalState(self::STATE_VALIDATION_KEY, $key); - } - return $this->_validationKey; - } - } - - /** - * @param string $value the key used to generate HMAC - * @throws CException if the key is empty - */ - public function setValidationKey($value) - { - if (!empty($value)) { - $this->_validationKey = $value; - } else { - throw new CException(Yii::t('yii', 'SecurityManager.validationKey cannot be empty.')); - } - } - - /** - * @return string the private key used to encrypt/decrypt data. - * If the key is not explicitly set, a random one is generated and returned. - */ - public function getEncryptionKey() - { - if ($this->_encryptionKey !== null) { - return $this->_encryptionKey; - } else { - if (($key = \Yii::$application->getGlobalState(self::STATE_ENCRYPTION_KEY)) !== null) { - $this->setEncryptionKey($key); - } else { - $key = $this->generateRandomKey(); - $this->setEncryptionKey($key); - \Yii::$application->setGlobalState(self::STATE_ENCRYPTION_KEY, $key); - } - return $this->_encryptionKey; - } - } - - /** - * @param string $value the key used to encrypt/decrypt data. - * @throws CException if the key is empty - */ - public function setEncryptionKey($value) - { - if (!empty($value)) { - $this->_encryptionKey = $value; - } else { - throw new CException(Yii::t('yii', 'SecurityManager.encryptionKey cannot be empty.')); - } - } - - /** - * This method has been deprecated since version 1.1.3. - * Please use {@link hashAlgorithm} instead. - * @return string - */ - public function getValidation() - { - return $this->hashAlgorithm; - } - - /** - * This method has been deprecated since version 1.1.3. - * Please use {@link hashAlgorithm} instead. - * @param string $value - - */ - public function setValidation($value) - { - $this->hashAlgorithm = $value; - } - - /** - * Encrypts data. - * @param string $data data to be encrypted. - * @param string $key the decryption key. This defaults to null, meaning using {@link getEncryptionKey EncryptionKey}. - * @return string the encrypted data - * @throws CException if PHP Mcrypt extension is not loaded - */ - public function encrypt($data, $key = null) - { - $module = $this->openCryptModule(); - $key = $this->substr($key === null ? md5($this->getEncryptionKey()) : $key, 0, mcrypt_enc_get_key_size($module)); - srand(); - $iv = mcrypt_create_iv(mcrypt_enc_get_iv_size($module), MCRYPT_RAND); - mcrypt_generic_init($module, $key, $iv); - $encrypted = $iv . mcrypt_generic($module, $data); - mcrypt_generic_deinit($module); - mcrypt_module_close($module); - return $encrypted; - } - - /** - * Decrypts data - * @param string $data data to be decrypted. - * @param string $key the decryption key. This defaults to null, meaning using {@link getEncryptionKey EncryptionKey}. - * @return string the decrypted data - * @throws CException if PHP Mcrypt extension is not loaded - */ - public function decrypt($data, $key = null) - { - $module = $this->openCryptModule(); - $key = $this->substr($key === null ? md5($this->getEncryptionKey()) : $key, 0, mcrypt_enc_get_key_size($module)); - $ivSize = mcrypt_enc_get_iv_size($module); - $iv = $this->substr($data, 0, $ivSize); - mcrypt_generic_init($module, $key, $iv); - $decrypted = mdecrypt_generic($module, $this->substr($data, $ivSize, $this->strlen($data))); - mcrypt_generic_deinit($module); - mcrypt_module_close($module); - return rtrim($decrypted, "\0"); - } - - /** - * Opens the mcrypt module with the configuration specified in {@link cryptAlgorithm}. - * @return resource the mycrypt module handle. - * @since 1.1.3 - */ - protected function openCryptModule() - { - if (extension_loaded('mcrypt')) { - if (is_array($this->cryptAlgorithm)) { - $module = @call_user_func_array('mcrypt_module_open', $this->cryptAlgorithm); - } else { - $module = @mcrypt_module_open($this->cryptAlgorithm, '', MCRYPT_MODE_CBC, ''); - } - - if ($module === false) { - throw new CException(Yii::t('yii', 'Failed to initialize the mcrypt module.')); - } - - return $module; - } else { - throw new CException(Yii::t('yii', 'SecurityManager requires PHP mcrypt extension to be loaded in order to use data encryption feature.')); - } - } - - /** - * Prefixes data with an HMAC. - * @param string $data data to be hashed. - * @param string $key the private key to be used for generating HMAC. Defaults to null, meaning using {@link validationKey}. - * @return string data prefixed with HMAC - */ - public function hashData($data, $key = null) - { - return $this->computeHMAC($data, $key) . $data; - } - - /** - * Validates if data is tampered. - * @param string $data data to be validated. The data must be previously - * generated using {@link hashData()}. - * @param string $key the private key to be used for generating HMAC. Defaults to null, meaning using {@link validationKey}. - * @return string the real data with HMAC stripped off. False if the data - * is tampered. - */ - public function validateData($data, $key = null) - { - $len = $this->strlen($this->computeHMAC('test')); - if ($this->strlen($data) >= $len) { - $hmac = $this->substr($data, 0, $len); - $data2 = $this->substr($data, $len, $this->strlen($data)); - return $hmac === $this->computeHMAC($data2, $key) ? $data2 : false; - } else { - return false; - } - } - - /** - * Computes the HMAC for the data with {@link getValidationKey ValidationKey}. - * @param string $data data to be generated HMAC - * @param string $key the private key to be used for generating HMAC. Defaults to null, meaning using {@link validationKey}. - * @return string the HMAC for the data - */ - protected function computeHMAC($data, $key = null) - { - if ($key === null) { - $key = $this->getValidationKey(); - } - - if (function_exists('hash_hmac')) { - return hash_hmac($this->hashAlgorithm, $data, $key); - } - - if (!strcasecmp($this->hashAlgorithm, 'sha1')) { - $pack = 'H40'; - $func = 'sha1'; - } else { - $pack = 'H32'; - $func = 'md5'; - } - if ($this->strlen($key) > 64) { - $key = pack($pack, $func($key)); - } - if ($this->strlen($key) < 64) { - $key = str_pad($key, 64, chr(0)); - } - $key = $this->substr($key, 0, 64); - return $func((str_repeat(chr(0x5C), 64) ^ $key) . pack($pack, $func((str_repeat(chr(0x36), 64) ^ $key) . $data))); - } - - /** - * Returns the length of the given string. - * If available uses the multibyte string function mb_strlen. - * @param string $string the string being measured for length - * @return int the length of the string - */ - private function strlen($string) - { - return function_exists('mb_strlen') ? mb_strlen($string, '8bit') : strlen($string); - } - - /** - * Returns the portion of string specified by the start and length parameters. - * If available uses the multibyte string function mb_substr - * @param string $string the input string. Must be one character or longer. - * @param int $start the starting position - * @param int $length the desired portion length - * @return string the extracted part of string, or FALSE on failure or an empty string. - */ - private function substr($string, $start, $length) - { - return function_exists('mb_substr') ? mb_substr($string, $start, $length, '8bit') : substr($string, $start, $length); - } -} diff --git a/framework/base/Theme.php b/framework/base/Theme.php index 03f8f55..88ecb0a 100644 --- a/framework/base/Theme.php +++ b/framework/base/Theme.php @@ -1,9 +1,7 @@ pathMap)) { if ($this->basePath !== null) { $this->basePath = FileHelper::ensureDirectory($this->basePath); - $this->pathMap = array(Yii::$application->getBasePath() => $this->basePath); + $this->pathMap = array(Yii::$app->getBasePath() => $this->basePath); } else { throw new InvalidConfigException("Theme::basePath must be set."); } } $paths = array(); foreach ($this->pathMap as $from => $to) { - $paths[FileHelper::normalizePath($from) . DIRECTORY_SEPARATOR] = FileHelper::normalizePath($to) . DIRECTORY_SEPARATOR; + $from = FileHelper::normalizePath(Yii::getAlias($from)); + $to = FileHelper::normalizePath(Yii::getAlias($to)); + $paths[$from . DIRECTORY_SEPARATOR] = $to . DIRECTORY_SEPARATOR; } $this->pathMap = $paths; } @@ -95,7 +96,7 @@ class Theme extends Component * @param string $path the file to be themed * @return string the themed file, or the original file if the themed version is not available. */ - public function apply($path) + public function applyTo($path) { $path = FileHelper::normalizePath($path); foreach ($this->pathMap as $from => $to) { diff --git a/framework/base/UnknownMethodException.php b/framework/base/UnknownMethodException.php index 459f791..29bedca 100644 --- a/framework/base/UnknownMethodException.php +++ b/framework/base/UnknownMethodException.php @@ -1,9 +1,7 @@ * @since 2.0 */ -class UnknownMethodException extends \Exception +class UnknownMethodException extends Exception { /** * @return string the user-friendly name of this exception */ public function getName() { - return \Yii::t('yii', 'Unknown Method'); + return \Yii::t('yii|Unknown Method'); } } diff --git a/framework/base/UnknownPropertyException.php b/framework/base/UnknownPropertyException.php index de8de1c..5ec3814 100644 --- a/framework/base/UnknownPropertyException.php +++ b/framework/base/UnknownPropertyException.php @@ -1,9 +1,7 @@ * @since 2.0 */ -class UnknownPropertyException extends \Exception +class UnknownPropertyException extends Exception { /** * @return string the user-friendly name of this exception */ public function getName() { - return \Yii::t('yii', 'Unknown Property'); + return \Yii::t('yii|Unknown Property'); } } diff --git a/framework/base/UrlManager.php b/framework/base/UrlManager.php deleted file mode 100644 index 3de8807..0000000 --- a/framework/base/UrlManager.php +++ /dev/null @@ -1,837 +0,0 @@ - - *
  • 'path' format: /path/to/EntryScript.php/name1/value1/name2/value2...
  • - *
  • 'get' format: /path/to/EntryScript.php?name1=value1&name2=value2...
  • - * - * - * When using 'path' format, UrlManager uses a set of {@link setRules rules} to: - * - * - * A rule consists of a route and a pattern. The latter is used by UrlManager to determine - * which rule is used for parsing/creating URLs. A pattern is meant to match the path info - * part of a URL. It may contain named parameters using the syntax '<ParamName:RegExp>'. - * - * When parsing a URL, a matching rule will extract the named parameters from the path info - * and put them into the $_GET variable; when creating a URL, a matching rule will extract - * the named parameters from $_GET and put them into the path info part of the created URL. - * - * If a pattern ends with '/*', it means additional GET parameters may be appended to the path - * info part of the URL; otherwise, the GET parameters can only appear in the query string part. - * - * To specify URL rules, set the {@link setRules rules} property as an array of rules (pattern=>route). - * For example, - *
    - * array(
    - *     'articles'=>'article/list',
    - *     'article//*'=>'article/read',
    - * )
    - * 
    - * Two rules are specified in the above: - * - * - * The route part may contain references to named parameters defined in the pattern part. - * This allows a rule to be applied to different routes based on matching criteria. - * For example, - *
    - * array(
    - *      '<_c:(post|comment)>//<_a:(create|update|delete)>'=>'<_c>/<_a>',
    - *      '<_c:(post|comment)>/'=>'<_c>/view',
    - *      '<_c:(post|comment)>s/*'=>'<_c>/list',
    - * )
    - * 
    - * In the above, we use two named parameters '<_c>' and '<_a>' in the route part. The '<_c>' - * parameter matches either 'post' or 'comment', while the '<_a>' parameter matches an action ID. - * - * Like normal rules, these rules can be used for both parsing and creating URLs. - * For example, using the rules above, the URL '/index.php/post/123/create' - * would be parsed as the route 'post/create' with GET parameter 'id' being 123. - * And given the route 'post/list' and GET parameter 'page' being 2, we should get a URL - * '/index.php/posts/page/2'. - * - * It is also possible to include hostname into the rules for parsing and creating URLs. - * One may extract part of the hostname to be a GET parameter. - * For example, the URL http://admin.example.com/en/profile may be parsed into GET parameters - * user=admin and lang=en. On the other hand, rules with hostname may also be used to - * create URLs with parameterized hostnames. - * - * In order to use parameterized hostnames, simply declare URL rules with host info, e.g.: - *
    - * array(
    - *     'http://.example.com//profile' => 'user/profile',
    - * )
    - * 
    - * - * If you want to customize URL generation and parsing you can write custom - * URL rule classes and use them for one or several URL rules. For example, - *
    - * array(
    - *   // a standard rule
    - *   '' => 'site/',
    - *   // a custom rule using data in DB
    - *   array(
    - *     'class' => '\application\components\MyUrlRule',
    - *     'connectionID' => 'db',
    - *   ),
    - * )
    - * 
    - * Please note that the custom URL rule class should extend from {@link BaseUrlRule} and - * implement the following two methods, - * - * - * UrlManager is a default application component that may be accessed via - * {@link \Yii::$application->urlManager}. - * - * @property string $baseUrl The base URL of the application (the part after host name and before query string). - * If {@link showScriptName} is true, it will include the script name part. - * Otherwise, it will not, and the ending slashes are stripped off. - * @property string $urlFormat The URL format. Defaults to 'path'. Valid values include 'path' and 'get'. - * Please refer to the guide for more details about the difference between these two formats. - * - * @author Qiang Xue - * @since 2.0 - */ -class UrlManager extends Component -{ - const CACHE_KEY='Yii.UrlManager.rules'; - const GET_FORMAT='get'; - const PATH_FORMAT='path'; - - /** - * @var array the URL rules (pattern=>route). - */ - public $rules=array(); - /** - * @var string the URL suffix used when in 'path' format. - * For example, ".html" can be used so that the URL looks like pointing to a static HTML page. Defaults to empty. - */ - public $urlSuffix=''; - /** - * @var boolean whether to show entry script name in the constructed URL. Defaults to true. - */ - public $showScriptName=true; - /** - * @var boolean whether to append GET parameters to the path info part. Defaults to true. - * This property is only effective when {@link urlFormat} is 'path' and is mainly used when - * creating URLs. When it is true, GET parameters will be appended to the path info and - * separate from each other using slashes. If this is false, GET parameters will be in query part. - */ - public $appendParams=true; - /** - * @var string the GET variable name for route. Defaults to 'r'. - */ - public $routeVar='r'; - /** - * @var boolean whether routes are case-sensitive. Defaults to true. By setting this to false, - * the route in the incoming request will be turned to lower case first before further processing. - * As a result, you should follow the convention that you use lower case when specifying - * controller mapping ({@link CWebApplication::controllerMap}) and action mapping - * ({@link CController::actions}). Also, the directory names for organizing controllers should - * be in lower case. - */ - public $caseSensitive=true; - /** - * @var boolean whether the GET parameter values should match the corresponding - * sub-patterns in a rule before using it to create a URL. Defaults to false, meaning - * a rule will be used for creating a URL only if its route and parameter names match the given ones. - * If this property is set true, then the given parameter values must also match the corresponding - * parameter sub-patterns. Note that setting this property to true will degrade performance. - * @since 1.1.0 - */ - public $matchValue=false; - /** - * @var string the ID of the cache application component that is used to cache the parsed URL rules. - * Defaults to 'cache' which refers to the primary cache application component. - * Set this property to false if you want to disable caching URL rules. - */ - public $cacheID='cache'; - /** - * @var boolean whether to enable strict URL parsing. - * This property is only effective when {@link urlFormat} is 'path'. - * If it is set true, then an incoming URL must match one of the {@link rules URL rules}. - * Otherwise, it will be treated as an invalid request and trigger a 404 HTTP exception. - * Defaults to false. - */ - public $useStrictParsing=false; - /** - * @var string the class name or path alias for the URL rule instances. Defaults to 'CUrlRule'. - * If you change this to something else, please make sure that the new class must extend from - * {@link CBaseUrlRule} and have the same constructor signature as {@link CUrlRule}. - * It must also be serializable and autoloadable. - */ - public $urlRuleClass='UrlRule'; - - private $_urlFormat=self::GET_FORMAT; - private $_rules=array(); - private $_baseUrl; - - - /** - * Initializes the application component. - */ - public function init() - { - parent::init(); - $this->processRules(); - } - - /** - * Processes the URL rules. - */ - protected function processRules() - { - if(empty($this->rules) || $this->getUrlFormat()===self::GET_FORMAT) - return; - if($this->cacheID!==false && ($cache=\Yii::$application->getComponent($this->cacheID))!==null) - { - $hash=md5(serialize($this->rules)); - if(($data=$cache->get(self::CACHE_KEY))!==false && isset($data[1]) && $data[1]===$hash) - { - $this->_rules=$data[0]; - return; - } - } - foreach($this->rules as $pattern=>$route) - $this->_rules[]=$this->createUrlRule($route,$pattern); - if(isset($cache)) - $cache->set(self::CACHE_KEY,array($this->_rules,$hash)); - } - - /** - * Adds new URL rules. - * In order to make the new rules effective, this method must be called BEFORE - * {@link CWebApplication::processRequest}. - * @param array $rules new URL rules (pattern=>route). - * @param boolean $append whether the new URL rules should be appended to the existing ones. If false, - * they will be inserted at the beginning. - */ - public function addRules($rules, $append=true) - { - if ($append) - { - foreach($rules as $pattern=>$route) - $this->_rules[]=$this->createUrlRule($route,$pattern); - } - else - { - foreach($rules as $pattern=>$route) - array_unshift($this->_rules, $this->createUrlRule($route,$pattern)); - } - } - - /** - * Creates a URL rule instance. - * The default implementation returns a CUrlRule object. - * @param mixed $route the route part of the rule. This could be a string or an array - * @param string $pattern the pattern part of the rule - * @return CUrlRule the URL rule instance - */ - protected function createUrlRule($route,$pattern) - { - if(is_array($route) && isset($route['class'])) - return $route; - else - return new $this->urlRuleClass($route,$pattern); - } - - /** - * Constructs a URL. - * @param string $route the controller and the action (e.g. article/read) - * @param array $params list of GET parameters (name=>value). Both the name and value will be URL-encoded. - * If the name is '#', the corresponding value will be treated as an anchor - * and will be appended at the end of the URL. - * @param string $ampersand the token separating name-value pairs in the URL. Defaults to '&'. - * @return string the constructed URL - */ - public function createUrl($route,$params=array(),$ampersand='&') - { - unset($params[$this->routeVar]); - foreach($params as $i=>$param) - if($param===null) - $params[$i]=''; - - if(isset($params['#'])) - { - $anchor='#'.$params['#']; - unset($params['#']); - } - else - $anchor=''; - $route=trim($route,'/'); - foreach($this->_rules as $i=>$rule) - { - if(is_array($rule)) - $this->_rules[$i]=$rule=Yii::createComponent($rule); - if(($url=$rule->createUrl($this,$route,$params,$ampersand))!==false) - { - if($rule->hasHostInfo) - return $url==='' ? '/'.$anchor : $url.$anchor; - else - return $this->getBaseUrl().'/'.$url.$anchor; - } - } - return $this->createUrlDefault($route,$params,$ampersand).$anchor; - } - - /** - * Creates a URL based on default settings. - * @param string $route the controller and the action (e.g. article/read) - * @param array $params list of GET parameters - * @param string $ampersand the token separating name-value pairs in the URL. - * @return string the constructed URL - */ - protected function createUrlDefault($route,$params,$ampersand) - { - if($this->getUrlFormat()===self::PATH_FORMAT) - { - $url=rtrim($this->getBaseUrl().'/'.$route,'/'); - if($this->appendParams) - { - $url=rtrim($url.'/'.$this->createPathInfo($params,'/','/'),'/'); - return $route==='' ? $url : $url.$this->urlSuffix; - } - else - { - if($route!=='') - $url.=$this->urlSuffix; - $query=$this->createPathInfo($params,'=',$ampersand); - return $query==='' ? $url : $url.'?'.$query; - } - } - else - { - $url=$this->getBaseUrl(); - if(!$this->showScriptName) - $url.='/'; - if($route!=='') - { - $url.='?'.$this->routeVar.'='.$route; - if(($query=$this->createPathInfo($params,'=',$ampersand))!=='') - $url.=$ampersand.$query; - } - else if(($query=$this->createPathInfo($params,'=',$ampersand))!=='') - $url.='?'.$query; - return $url; - } - } - - /** - * Parses the user request. - * @param HttpRequest $request the request application component - * @return string the route (controllerID/actionID) and perhaps GET parameters in path format. - */ - public function parseUrl($request) - { - if($this->getUrlFormat()===self::PATH_FORMAT) - { - $rawPathInfo=$request->getPathInfo(); - $pathInfo=$this->removeUrlSuffix($rawPathInfo,$this->urlSuffix); - foreach($this->_rules as $i=>$rule) - { - if(is_array($rule)) - $this->_rules[$i]=$rule=Yii::createComponent($rule); - if(($r=$rule->parseUrl($this,$request,$pathInfo,$rawPathInfo))!==false) - return isset($_GET[$this->routeVar]) ? $_GET[$this->routeVar] : $r; - } - if($this->useStrictParsing) - throw new HttpException(404,Yii::t('yii','Unable to resolve the request "{route}".', - array('{route}'=>$pathInfo))); - else - return $pathInfo; - } - else if(isset($_GET[$this->routeVar])) - return $_GET[$this->routeVar]; - else if(isset($_POST[$this->routeVar])) - return $_POST[$this->routeVar]; - else - return ''; - } - - /** - * Parses a path info into URL segments and saves them to $_GET and $_REQUEST. - * @param string $pathInfo path info - */ - public function parsePathInfo($pathInfo) - { - if($pathInfo==='') - return; - $segs=explode('/',$pathInfo.'/'); - $n=count($segs); - for($i=0;$i<$n-1;$i+=2) - { - $key=$segs[$i]; - if($key==='') continue; - $value=$segs[$i+1]; - if(($pos=strpos($key,'['))!==false && ($m=preg_match_all('/\[(.*?)\]/',$key,$matches))>0) - { - $name=substr($key,0,$pos); - for($j=$m-1;$j>=0;--$j) - { - if($matches[1][$j]==='') - $value=array($value); - else - $value=array($matches[1][$j]=>$value); - } - if(isset($_GET[$name]) && is_array($_GET[$name])) - $value=CMap::mergeArray($_GET[$name],$value); - $_REQUEST[$name]=$_GET[$name]=$value; - } - else - $_REQUEST[$key]=$_GET[$key]=$value; - } - } - - /** - * Creates a path info based on the given parameters. - * @param array $params list of GET parameters - * @param string $equal the separator between name and value - * @param string $ampersand the separator between name-value pairs - * @param string $key this is used internally. - * @return string the created path info - */ - public function createPathInfo($params,$equal,$ampersand, $key=null) - { - $pairs = array(); - foreach($params as $k => $v) - { - if ($key!==null) - $k = $key.'['.$k.']'; - - if (is_array($v)) - $pairs[]=$this->createPathInfo($v,$equal,$ampersand, $k); - else - $pairs[]=urlencode($k).$equal.urlencode($v); - } - return implode($ampersand,$pairs); - } - - /** - * Removes the URL suffix from path info. - * @param string $pathInfo path info part in the URL - * @param string $urlSuffix the URL suffix to be removed - * @return string path info with URL suffix removed. - */ - public function removeUrlSuffix($pathInfo,$urlSuffix) - { - if($urlSuffix!=='' && substr($pathInfo,-strlen($urlSuffix))===$urlSuffix) - return substr($pathInfo,0,-strlen($urlSuffix)); - else - return $pathInfo; - } - - /** - * Returns the base URL of the application. - * @return string the base URL of the application (the part after host name and before query string). - * If {@link showScriptName} is true, it will include the script name part. - * Otherwise, it will not, and the ending slashes are stripped off. - */ - public function getBaseUrl() - { - if($this->_baseUrl!==null) - return $this->_baseUrl; - else - { - if($this->showScriptName) - $this->_baseUrl=\Yii::$application->getRequest()->getScriptUrl(); - else - $this->_baseUrl=\Yii::$application->getRequest()->getBaseUrl(); - return $this->_baseUrl; - } - } - - /** - * Sets the base URL of the application (the part after host name and before query string). - * This method is provided in case the {@link baseUrl} cannot be determined automatically. - * The ending slashes should be stripped off. And you are also responsible to remove the script name - * if you set {@link showScriptName} to be false. - * @param string $value the base URL of the application - */ - public function setBaseUrl($value) - { - $this->_baseUrl=$value; - } - - /** - * Returns the URL format. - * @return string the URL format. Defaults to 'path'. Valid values include 'path' and 'get'. - * Please refer to the guide for more details about the difference between these two formats. - */ - public function getUrlFormat() - { - return $this->_urlFormat; - } - - /** - * Sets the URL format. - * @param string $value the URL format. It must be either 'path' or 'get'. - */ - public function setUrlFormat($value) - { - if($value===self::PATH_FORMAT || $value===self::GET_FORMAT) - $this->_urlFormat=$value; - else - throw new CException(Yii::t('yii','CUrlManager.UrlFormat must be either "path" or "get".')); - } -} - - -/** - * CBaseUrlRule is the base class for a URL rule class. - * - * Custom URL rule classes should extend from this class and implement two methods: - * {@link createUrl} and {@link parseUrl}. - * - * @author Qiang Xue - */ -abstract class CBaseUrlRule extends CComponent -{ - /** - * @var boolean whether this rule will also parse the host info part. Defaults to false. - */ - public $hasHostInfo=false; - /** - * Creates a URL based on this rule. - * @param CUrlManager $manager the manager - * @param string $route the route - * @param array $params list of parameters (name=>value) associated with the route - * @param string $ampersand the token separating name-value pairs in the URL. - * @return mixed the constructed URL. False if this rule does not apply. - */ - abstract public function createUrl($manager,$route,$params,$ampersand); - /** - * Parses a URL based on this rule. - * @param UrlManager $manager the URL manager - * @param HttpRequest $request the request object - * @param string $pathInfo path info part of the URL (URL suffix is already removed based on {@link CUrlManager::urlSuffix}) - * @param string $rawPathInfo path info that contains the potential URL suffix - * @return mixed the route that consists of the controller ID and action ID. False if this rule does not apply. - */ - abstract public function parseUrl($manager,$request,$pathInfo,$rawPathInfo); -} - -/** - * CUrlRule represents a URL formatting/parsing rule. - * - * It mainly consists of two parts: route and pattern. The former classifies - * the rule so that it only applies to specific controller-action route. - * The latter performs the actual formatting and parsing role. The pattern - * may have a set of named parameters. - * - * @author Qiang Xue - */ -class CUrlRule extends CBaseUrlRule -{ - /** - * @var string the URL suffix used for this rule. - * For example, ".html" can be used so that the URL looks like pointing to a static HTML page. - * Defaults to null, meaning using the value of {@link CUrlManager::urlSuffix}. - */ - public $urlSuffix; - /** - * @var boolean whether the rule is case sensitive. Defaults to null, meaning - * using the value of {@link CUrlManager::caseSensitive}. - */ - public $caseSensitive; - /** - * @var array the default GET parameters (name=>value) that this rule provides. - * When this rule is used to parse the incoming request, the values declared in this property - * will be injected into $_GET. - */ - public $defaultParams=array(); - /** - * @var boolean whether the GET parameter values should match the corresponding - * sub-patterns in the rule when creating a URL. Defaults to null, meaning using the value - * of {@link CUrlManager::matchValue}. When this property is false, it means - * a rule will be used for creating a URL if its route and parameter names match the given ones. - * If this property is set true, then the given parameter values must also match the corresponding - * parameter sub-patterns. Note that setting this property to true will degrade performance. - */ - public $matchValue; - /** - * @var string the HTTP verb (e.g. GET, POST, DELETE) that this rule should match. - * If this rule can match multiple verbs, please separate them with commas. - * If this property is not set, the rule can match any verb. - * Note that this property is only used when parsing a request. It is ignored for URL creation. - */ - public $verb; - /** - * @var boolean whether this rule is only used for request parsing. - * Defaults to false, meaning the rule is used for both URL parsing and creation. - */ - public $parsingOnly=false; - /** - * @var string the controller/action pair - */ - public $route; - /** - * @var array the mapping from route param name to token name (e.g. _r1=><1>) - */ - public $references=array(); - /** - * @var string the pattern used to match route - */ - public $routePattern; - /** - * @var string regular expression used to parse a URL - */ - public $pattern; - /** - * @var string template used to construct a URL - */ - public $template; - /** - * @var array list of parameters (name=>regular expression) - */ - public $params=array(); - /** - * @var boolean whether the URL allows additional parameters at the end of the path info. - */ - public $append; - /** - * @var boolean whether host info should be considered for this rule - */ - public $hasHostInfo; - - /** - * Constructor. - * @param string $route the route of the URL (controller/action) - * @param string $pattern the pattern for matching the URL - */ - public function __construct($route,$pattern) - { - if(is_array($route)) - { - foreach(array('urlSuffix', 'caseSensitive', 'defaultParams', 'matchValue', 'verb', 'parsingOnly') as $name) - { - if(isset($route[$name])) - $this->$name=$route[$name]; - } - if(isset($route['pattern'])) - $pattern=$route['pattern']; - $route=$route[0]; - } - $this->route=trim($route,'/'); - - $tr2['/']=$tr['/']='\\/'; - - if(strpos($route,'<')!==false && preg_match_all('/<(\w+)>/',$route,$matches2)) - { - foreach($matches2[1] as $name) - $this->references[$name]="<$name>"; - } - - $this->hasHostInfo=!strncasecmp($pattern,'http://',7) || !strncasecmp($pattern,'https://',8); - - if($this->verb!==null) - $this->verb=preg_split('/[\s,]+/',strtoupper($this->verb),-1,PREG_SPLIT_NO_EMPTY); - - if(preg_match_all('/<(\w+):?(.*?)?>/',$pattern,$matches)) - { - $tokens=array_combine($matches[1],$matches[2]); - foreach($tokens as $name=>$value) - { - if($value==='') - $value='[^\/]+'; - $tr["<$name>"]="(?P<$name>$value)"; - if(isset($this->references[$name])) - $tr2["<$name>"]=$tr["<$name>"]; - else - $this->params[$name]=$value; - } - } - $p=rtrim($pattern,'*'); - $this->append=$p!==$pattern; - $p=trim($p,'/'); - $this->template=preg_replace('/<(\w+):?.*?>/','<$1>',$p); - $this->pattern='/^'.strtr($this->template,$tr).'\/'; - if($this->append) - $this->pattern.='/u'; - else - $this->pattern.='$/u'; - - if($this->references!==array()) - $this->routePattern='/^'.strtr($this->route,$tr2).'$/u'; - - if(YII_DEBUG && @preg_match($this->pattern,'test')===false) - throw new CException(Yii::t('yii','The URL pattern "{pattern}" for route "{route}" is not a valid regular expression.', - array('{route}'=>$route,'{pattern}'=>$pattern))); - } - - /** - * Creates a URL based on this rule. - * @param CUrlManager $manager the manager - * @param string $route the route - * @param array $params list of parameters - * @param string $ampersand the token separating name-value pairs in the URL. - * @return mixed the constructed URL or false on error - */ - public function createUrl($manager,$route,$params,$ampersand) - { - if($this->parsingOnly) - return false; - - if($manager->caseSensitive && $this->caseSensitive===null || $this->caseSensitive) - $case=''; - else - $case='i'; - - $tr=array(); - if($route!==$this->route) - { - if($this->routePattern!==null && preg_match($this->routePattern.$case,$route,$matches)) - { - foreach($this->references as $key=>$name) - $tr[$name]=$matches[$key]; - } - else - return false; - } - - foreach($this->defaultParams as $key=>$value) - { - if(isset($params[$key])) - { - if($params[$key]==$value) - unset($params[$key]); - else - return false; - } - } - - foreach($this->params as $key=>$value) - if(!isset($params[$key])) - return false; - - if($manager->matchValue && $this->matchValue===null || $this->matchValue) - { - foreach($this->params as $key=>$value) - { - if(!preg_match('/\A'.$value.'\z/u'.$case,$params[$key])) - return false; - } - } - - foreach($this->params as $key=>$value) - { - $tr["<$key>"]=urlencode($params[$key]); - unset($params[$key]); - } - - $suffix=$this->urlSuffix===null ? $manager->urlSuffix : $this->urlSuffix; - - $url=strtr($this->template,$tr); - - if($this->hasHostInfo) - { - $hostInfo=\Yii::$application->getRequest()->getHostInfo(); - if(stripos($url,$hostInfo)===0) - $url=substr($url,strlen($hostInfo)); - } - - if(empty($params)) - return $url!=='' ? $url.$suffix : $url; - - if($this->append) - $url.='/'.$manager->createPathInfo($params,'/','/').$suffix; - else - { - if($url!=='') - $url.=$suffix; - $url.='?'.$manager->createPathInfo($params,'=',$ampersand); - } - - return $url; - } - - /** - * Parses a URL based on this rule. - * @param UrlManager $manager the URL manager - * @param HttpRequest $request the request object - * @param string $pathInfo path info part of the URL - * @param string $rawPathInfo path info that contains the potential URL suffix - * @return mixed the route that consists of the controller ID and action ID or false on error - */ - public function parseUrl($manager,$request,$pathInfo,$rawPathInfo) - { - if($this->verb!==null && !in_array($request->getRequestType(), $this->verb, true)) - return false; - - if($manager->caseSensitive && $this->caseSensitive===null || $this->caseSensitive) - $case=''; - else - $case='i'; - - if($this->urlSuffix!==null) - $pathInfo=$manager->removeUrlSuffix($rawPathInfo,$this->urlSuffix); - - // URL suffix required, but not found in the requested URL - if($manager->useStrictParsing && $pathInfo===$rawPathInfo) - { - $urlSuffix=$this->urlSuffix===null ? $manager->urlSuffix : $this->urlSuffix; - if($urlSuffix!='' && $urlSuffix!=='/') - return false; - } - - if($this->hasHostInfo) - $pathInfo=strtolower($request->getHostInfo()).rtrim('/'.$pathInfo,'/'); - - $pathInfo.='/'; - - if(preg_match($this->pattern.$case,$pathInfo,$matches)) - { - foreach($this->defaultParams as $name=>$value) - { - if(!isset($_GET[$name])) - $_REQUEST[$name]=$_GET[$name]=$value; - } - $tr=array(); - foreach($matches as $key=>$value) - { - if(isset($this->references[$key])) - $tr[$this->references[$key]]=$value; - else if(isset($this->params[$key])) - $_REQUEST[$key]=$_GET[$key]=$value; - } - if($pathInfo!==$matches[0]) // there're additional GET params - $manager->parsePathInfo(ltrim(substr($pathInfo,strlen($matches[0])),'/')); - if($this->routePattern!==null) - return strtr($this->route,$tr); - else - return $this->route; - } - else - return false; - } -} \ No newline at end of file diff --git a/framework/base/UserException.php b/framework/base/UserException.php new file mode 100644 index 0000000..01ca602 --- /dev/null +++ b/framework/base/UserException.php @@ -0,0 +1,19 @@ + + * @since 2.0 + */ +class UserException extends Exception +{ +} diff --git a/framework/base/Vector.php b/framework/base/Vector.php index c271ccc..7d43fdb 100644 --- a/framework/base/Vector.php +++ b/framework/base/Vector.php @@ -1,9 +1,7 @@ = 0 && $index < $this->_c) { // in case the value is null return $this->_d[$index]; } else { - throw new InvalidCallException('Index out of range: ' . $index); + throw new InvalidParamException('Index out of range: ' . $index); } } @@ -132,7 +130,7 @@ class Vector extends Object implements \IteratorAggregate, \ArrayAccess, \Counta * one step towards the end. * @param integer $index the specified position. * @param mixed $item new item to be inserted into the vector - * @throws InvalidCallException if the index specified is out of range, or the vector is read-only. + * @throws InvalidParamException if the index specified is out of range, or the vector is read-only. */ public function insertAt($index, $item) { @@ -142,7 +140,7 @@ class Vector extends Object implements \IteratorAggregate, \ArrayAccess, \Counta array_splice($this->_d, $index, 0, array($item)); $this->_c++; } else { - throw new InvalidCallException('Index out of range: ' . $index); + throw new InvalidParamException('Index out of range: ' . $index); } } @@ -169,7 +167,7 @@ class Vector extends Object implements \IteratorAggregate, \ArrayAccess, \Counta * Removes an item at the specified position. * @param integer $index the index of the item to be removed. * @return mixed the removed item. - * @throws InvalidCallException if the index is out of range, or the vector is read only. + * @throws InvalidParamException if the index is out of range, or the vector is read only. */ public function removeAt($index) { @@ -183,7 +181,7 @@ class Vector extends Object implements \IteratorAggregate, \ArrayAccess, \Counta return $item; } } else { - throw new InvalidCallException('Index out of range: ' . $index); + throw new InvalidParamException('Index out of range: ' . $index); } } @@ -193,7 +191,7 @@ class Vector extends Object implements \IteratorAggregate, \ArrayAccess, \Counta * Defaults to false, meaning all items in the vector will be cleared directly * without calling [[removeAt]]. */ - public function clear($safeClear = false) + public function removeAll($safeClear = false) { if ($safeClear) { for ($i = $this->_c - 1; $i >= 0; --$i) { @@ -211,7 +209,7 @@ class Vector extends Object implements \IteratorAggregate, \ArrayAccess, \Counta * @param mixed $item the item * @return boolean whether the vector contains the item */ - public function contains($item) + public function has($item) { return $this->indexOf($item) >= 0; } @@ -242,13 +240,13 @@ class Vector extends Object implements \IteratorAggregate, \ArrayAccess, \Counta * Copies iterable data into the vector. * Note, existing data in the vector will be cleared first. * @param mixed $data the data to be copied from, must be an array or an object implementing `Traversable` - * @throws InvalidCallException if data is neither an array nor an object implementing `Traversable`. + * @throws InvalidParamException if data is neither an array nor an object implementing `Traversable`. */ public function copyFrom($data) { if (is_array($data) || $data instanceof \Traversable) { if ($this->_c > 0) { - $this->clear(); + $this->removeAll(); } if ($data instanceof self) { $data = $data->_d; @@ -257,7 +255,7 @@ class Vector extends Object implements \IteratorAggregate, \ArrayAccess, \Counta $this->add($item); } } else { - throw new InvalidCallException('Data must be either an array or an object implementing Traversable.'); + throw new InvalidParamException('Data must be either an array or an object implementing Traversable.'); } } @@ -265,7 +263,7 @@ class Vector extends Object implements \IteratorAggregate, \ArrayAccess, \Counta * Merges iterable data into the vector. * New items will be appended to the end of the existing items. * @param array|\Traversable $data the data to be merged with. It must be an array or object implementing Traversable - * @throws InvalidCallException if data is neither an array nor an object implementing `Traversable`. + * @throws InvalidParamException if data is neither an array nor an object implementing `Traversable`. */ public function mergeWith($data) { @@ -277,7 +275,7 @@ class Vector extends Object implements \IteratorAggregate, \ArrayAccess, \Counta $this->add($item); } } else { - throw new InvalidCallException('The data to be merged with must be an array or an object implementing Traversable.'); + throw new InvalidParamException('The data to be merged with must be an array or an object implementing Traversable.'); } } diff --git a/framework/base/VectorIterator.php b/framework/base/VectorIterator.php index d1fefad..f83d42d 100644 --- a/framework/base/VectorIterator.php +++ b/framework/base/VectorIterator.php @@ -1,9 +1,7 @@ * @since 2.0 */ @@ -26,134 +24,124 @@ class View extends Component /** * @var object the object that owns this view. This can be a controller, a widget, or any other object. */ - public $owner; + public $context; /** - * @var string the layout to be applied when [[render()]] or [[renderContent()]] is called. - * If not set, it will use the [[Module::layout]] of the currently active module. + * @var mixed custom parameters that are shared among view templates. */ - public $layout; + public $params; /** - * @var string the language that the view should be rendered in. If not set, it will use - * the value of [[Application::language]]. + * @var ViewRenderer|array the view renderer object or the configuration array for + * creating the view renderer. If not set, view files will be treated as normal PHP files. */ - public $language; + public $renderer; /** - * @var string the language that the original view is in. If not set, it will use - * the value of [[Application::sourceLanguage]]. + * @var Theme|array the theme object or the configuration array for creating the theme. + * If not set, it means theming is not enabled. */ - public $sourceLanguage; + public $theme; /** - * @var boolean whether to localize the view when possible. Defaults to true. - * Note that when this is true, if a localized view cannot be found, the original view will be rendered. - * No error will be reported. + * @var array a list of named output clips. You can call [[beginClip()]] and [[endClip()]] + * to capture small fragments of a view. They can be later accessed at somewhere else + * through this property. */ - public $enableI18N = true; + public $clips; /** - * @var boolean whether to theme the view when possible. Defaults to true. - * Note that theming will be disabled if [[Application::theme]] is not set. + * @var Widget[] the widgets that are currently being rendered (not ended). This property + * is maintained by [[beginWidget()]] and [[endWidget()]] methods. Do not modify it directly. */ - public $enableTheme = true; + public $widgetStack = array(); /** - * @var mixed custom parameters that are available in the view template + * @var array a list of currently active fragment cache widgets. This property + * is used internally to implement the content caching feature. Do not modify it. */ - public $params; - + public $cacheStack = array(); /** - * @var Widget[] the widgets that are currently not ended + * @var array a list of placeholders for embedding dynamic contents. This property + * is used internally to implement the content caching feature. Do not modify it. */ - private $_widgetStack = array(); + public $dynamicPlaceholders = array(); - /** - * Constructor. - * @param object $owner the owner of this view. This usually is a controller or a widget. - * @param array $config name-value pairs that will be used to initialize the object properties - */ - public function __construct($owner, $config = array()) - { - $this->owner = $owner; - parent::__construct($config); - } - - /** - * Renders a view within a layout. - * This method is similar to [[renderPartial()]] except that if a layout is available, - * this method will embed the view result into the layout and then return it. - * @param string $view the view to be rendered. Please refer to [[findViewFile()]] on possible formats of the view name. - * @param array $params the parameters that should be made available in the view. The PHP function `extract()` - * will be called on this variable to extract the variables from this parameter. - * @return string the rendering result - * @throws InvalidConfigException if the view file or layout file cannot be found - * @see findViewFile() - * @see findLayoutFile() - */ - public function render($view, $params = array()) - { - $content = $this->renderPartial($view, $params); - return $this->renderContent($content); - } /** - * Renders a text content within a layout. - * The layout being used is resolved by [[findLayout()]]. - * If no layout is available, the content will be returned back. - * @param string $content the content to be rendered - * @return string the rendering result - * @throws InvalidConfigException if the layout file cannot be found - * @see findLayoutFile() + * Initializes the view component. */ - public function renderContent($content) + public function init() { - $layoutFile = $this->findLayoutFile(); - if ($layoutFile !== false) { - return $this->renderFile($layoutFile, array('content' => $content)); - } else { - return $content; + parent::init(); + if (is_array($this->renderer)) { + $this->renderer = Yii::createObject($this->renderer); + } + if (is_array($this->theme)) { + $this->theme = Yii::createObject($this->theme); } } /** * Renders a view. * - * The method first finds the actual view file corresponding to the specified view. - * It then calls [[renderFile()]] to render the view file. The rendering result is returned - * as a string. If the view file does not exist, an exception will be thrown. + * This method will call [[findViewFile()]] to convert the view name into the corresponding view + * file path, and it will then call [[renderFile()]] to render the view. * - * @param string $view the view to be rendered. Please refer to [[findViewFile()]] on possible formats of the view name. - * @param array $params the parameters that should be made available in the view. The PHP function `extract()` - * will be called on this variable to extract the variables from this parameter. + * @param string $view the view name. Please refer to [[findViewFile()]] on how to specify this parameter. + * @param array $params the parameters (name-value pairs) that will be extracted and made available in the view file. + * @param object $context the context that the view should use for rendering the view. If null, + * existing [[context]] will be used. * @return string the rendering result - * @throws InvalidCallException if the view file cannot be found - * @see findViewFile() + * @throws InvalidParamException if the view cannot be resolved or the view file does not exist. + * @see renderFile + * @see findViewFile */ - public function renderPartial($view, $params = array()) + public function render($view, $params = array(), $context = null) { - $file = $this->findViewFile($view); - if ($file !== false) { - return $this->renderFile($file, $params); - } else { - throw new InvalidCallException("Unable to find the view file for view '$view'."); - } + $viewFile = $this->findViewFile($context, $view); + return $this->renderFile($viewFile, $params, $context); } /** * Renders a view file. * - * If a [[ViewRenderer|view renderer]] is installed, this method will try to use the view renderer - * to render the view file. Otherwise, it will simply include the view file, capture its output - * and return it as a string. + * If [[theme]] is enabled (not null), it will try to render the themed version of the view file as long + * as it is available. * - * @param string $file the view file. + * The method will call [[FileHelper::localize()]] to localize the view file. + * + * If [[renderer]] is enabled (not null), the method will use it to render the view file. + * Otherwise, it will simply include the view file as a normal PHP file, capture its output and + * return it as a string. + * + * @param string $viewFile the view file. This can be either a file path or a path alias. * @param array $params the parameters (name-value pairs) that will be extracted and made available in the view file. + * @param object $context the context that the view should use for rendering the view. If null, + * existing [[context]] will be used. * @return string the rendering result + * @throws InvalidParamException if the view file does not exist */ - public function renderFile($file, $params = array()) + public function renderFile($viewFile, $params = array(), $context = null) { - $renderer = Yii::$application->getViewRenderer(); - if ($renderer !== null) { - return $renderer->render($this, $file, $params); + $viewFile = Yii::getAlias($viewFile); + if (is_file($viewFile)) { + if ($this->theme !== null) { + $viewFile = $this->theme->applyTo($viewFile); + } + $viewFile = FileHelper::localize($viewFile); } else { - return $this->renderPhpFile($file, $params); + throw new InvalidParamException("The view file does not exist: $viewFile"); } + + $oldContext = $this->context; + if ($context !== null) { + $this->context = $context; + } + + if ($this->renderer !== null) { + $output = $this->renderer->render($this, $viewFile, $params); + } else { + $output = $this->renderPhpFile($viewFile, $params); + } + + $this->context = $oldContext; + + return $output; } /** @@ -163,6 +151,8 @@ class View extends Component * It extracts the given parameters and makes them available in the view file. * The method captures the output of the included view file and returns it as a string. * + * This method should mainly be called by view renderer or [[renderFile()]]. + * * @param string $_file_ the view file. * @param array $_params_ the parameters (name-value pairs) that will be extracted and made available in the view file. * @return string the rendering result @@ -177,6 +167,95 @@ class View extends Component } /** + * Renders dynamic content returned by the given PHP statements. + * This method is mainly used together with content caching (fragment caching and page caching) + * when some portions of the content (called *dynamic content*) should not be cached. + * The dynamic content must be returned by some PHP statements. + * @param string $statements the PHP statements for generating the dynamic content. + * @return string the placeholder of the dynamic content, or the dynamic content if there is no + * active content cache currently. + */ + public function renderDynamic($statements) + { + if (!empty($this->cacheStack)) { + $n = count($this->dynamicPlaceholders); + $placeholder = ""; + $this->addDynamicPlaceholder($placeholder, $statements); + return $placeholder; + } else { + return $this->evaluateDynamicContent($statements); + } + } + + /** + * Adds a placeholder for dynamic content. + * This method is internally used. + * @param string $placeholder the placeholder name + * @param string $statements the PHP statements for generating the dynamic content + */ + public function addDynamicPlaceholder($placeholder, $statements) + { + foreach ($this->cacheStack as $cache) { + $cache->dynamicPlaceholders[$placeholder] = $statements; + } + $this->dynamicPlaceholders[$placeholder] = $statements; + } + + /** + * Evaluates the given PHP statements. + * This method is mainly used internally to implement dynamic content feature. + * @param string $statements the PHP statements to be evaluated. + * @return mixed the return value of the PHP statements. + */ + public function evaluateDynamicContent($statements) + { + return eval($statements); + } + + /** + * Finds the view file based on the given view name. + * + * A view name can be specified in one of the following formats: + * + * - path alias (e.g. "@app/views/site/index"); + * - absolute path within application (e.g. "//site/index"): the view name starts with double slashes. + * The actual view file will be looked for under the [[Application::viewPath|view path]] of the application. + * - absolute path within module (e.g. "/site/index"): the view name starts with a single slash. + * The actual view file will be looked for under the [[Module::viewPath|view path]] of the currently + * active module. + * - relative path (e.g. "index"): the actual view file will be looked for under [[Controller::viewPath|viewPath]] + * of the context object, assuming the context is either a [[Controller]] or a [[Widget]]. + * + * If the view name does not contain a file extension, it will use the default one `.php`. + * + * @param object $context the view context object + * @param string $view the view name or the path alias of the view file. + * @return string the view file path. Note that the file may not exist. + * @throws InvalidParamException if the view file is an invalid path alias or the context cannot be + * used to determine the actual view file corresponding to the specified view. + */ + protected function findViewFile($context, $view) + { + if (strncmp($view, '@', 1) === 0) { + // e.g. "@app/views/main" + $file = Yii::getAlias($view); + } elseif (strncmp($view, '//', 2) === 0) { + // e.g. "//layouts/main" + $file = Yii::$app->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); + } elseif (strncmp($view, '/', 1) === 0) { + // e.g. "/site/index" + $file = Yii::$app->controller->module->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); + } elseif ($context instanceof Controller || $context instanceof Widget) { + /** @var $context Controller|Widget */ + $file = $context->getViewPath() . DIRECTORY_SEPARATOR . $view; + } else { + throw new InvalidParamException("Unable to resolve the view file for '$view'."); + } + + return FileHelper::getExtension($file) === '' ? $file . '.php' : $file; + } + + /** * Creates a widget. * This method will use [[Yii::createObject()]] to create the widget. * @param string $class the widget class name or path alias @@ -186,7 +265,7 @@ class View extends Component public function createWidget($class, $properties = array()) { $properties['class'] = $class; - return Yii::createObject($properties, $this->owner); + return Yii::createObject($properties, $this->context); } /** @@ -225,7 +304,7 @@ class View extends Component public function beginWidget($class, $properties = array()) { $widget = $this->createWidget($class, $properties); - $this->_widgetStack[] = $widget; + $this->widgetStack[] = $widget; return $widget; } @@ -235,260 +314,108 @@ class View extends Component * If you want to capture the rendering result of a widget, you may use * [[createWidget()]] and [[Widget::run()]]. * @return Widget the widget instance - * @throws Exception if [[beginWidget()]] and [[endWidget()]] calls are not properly nested + * @throws InvalidCallException if [[beginWidget()]] and [[endWidget()]] calls are not properly nested */ public function endWidget() { - $widget = array_pop($this->_widgetStack); + $widget = array_pop($this->widgetStack); if ($widget instanceof Widget) { $widget->run(); return $widget; } else { - throw new Exception("Unmatched beginWidget() and endWidget() calls."); + throw new InvalidCallException("Unmatched beginWidget() and endWidget() calls."); } } -// -// /** -// * Begins recording a clip. -// * This method is a shortcut to beginning [[yii\widgets\Clip]] -// * @param string $id the clip ID. -// * @param array $properties initial property values for [[yii\widgets\Clip]] -// */ -// public function beginClip($id, $properties = array()) -// { -// $properties['id'] = $id; -// $this->beginWidget('yii\widgets\Clip', $properties); -// } -// -// /** -// * Ends recording a clip. -// */ -// public function endClip() -// { -// $this->endWidget(); -// } -// -// /** -// * Begins fragment caching. -// * This method will display cached content if it is available. -// * If not, it will start caching and would expect an [[endCache()]] -// * call to end the cache and save the content into cache. -// * A typical usage of fragment caching is as follows, -// * -// * ~~~ -// * if($this->beginCache($id)) { -// * // ...generate content here -// * $this->endCache(); -// * } -// * ~~~ -// * -// * @param string $id a unique ID identifying the fragment to be cached. -// * @param array $properties initial property values for [[yii\widgets\OutputCache]] -// * @return boolean whether we need to generate content for caching. False if cached version is available. -// * @see endCache -// */ -// public function beginCache($id, $properties = array()) -// { -// $properties['id'] = $id; -// $cache = $this->beginWidget('yii\widgets\OutputCache', $properties); -// if ($cache->getIsContentCached()) { -// $this->endCache(); -// return false; -// } else { -// return true; -// } -// } -// -// /** -// * Ends fragment caching. -// * This is an alias to [[endWidget()]] -// * @see beginCache -// */ -// public function endCache() -// { -// $this->endWidget(); -// } -// -// /** -// * Begins the rendering of content that is to be decorated by the specified view. -// * @param mixed $view the name of the view that will be used to decorate the content. The actual view script -// * is resolved via {@link getViewFile}. If this parameter is null (default), -// * the default layout will be used as the decorative view. -// * Note that if the current controller does not belong to -// * any module, the default layout refers to the application's {@link CWebApplication::layout default layout}; -// * If the controller belongs to a module, the default layout refers to the module's -// * {@link CWebModule::layout default layout}. -// * @param array $params the variables (name=>value) to be extracted and made available in the decorative view. -// * @see endContent -// * @see yii\widgets\ContentDecorator -// */ -// public function beginContent($view, $params = array()) -// { -// $this->beginWidget('yii\widgets\ContentDecorator', array( -// 'view' => $view, -// 'params' => $params, -// )); -// } -// -// /** -// * Ends the rendering of content. -// * @see beginContent -// */ -// public function endContent() -// { -// $this->endWidget(); -// } /** - * Finds the view file based on the given view name. - * - * A view name can be specified in one of the following formats: - * - * - path alias (e.g. "@application/views/site/index"); - * - absolute path within application (e.g. "//site/index"): the view name starts with double slashes. - * The actual view file will be looked for under the [[Application::viewPath|view path]] of the application. - * - absolute path within module (e.g. "/site/index"): the view name starts with a single slash. - * The actual view file will be looked for under the [[Module::viewPath|view path]] of the currently - * active module. - * - relative path (e.g. "index"): the actual view file will be looked for under the [[owner]]'s view path. - * If [[owner]] is a widget or a controller, its view path is given by their `viewPath` property. - * If [[owner]] is an object of any other type, its view path is the `view` sub-directory of the directory - * containing the owner class file. - * - * If the view name does not contain a file extension, it will default to `.php`. - * - * If [[enableTheme]] is true and there is an active application them, the method will also - * attempt to use a themed version of the view file, when available. - * - * And if [[enableI18N]] is true, the method will attempt to use a translated version of the view file, - * when available. - * - * @param string $view the view name or path alias. If the view name does not specify - * the view file extension name, it will use `.php` as the extension name. - * @return string the view file path if it exists. False if the view file cannot be found. - * @throws InvalidConfigException if the view file does not exist + * Begins recording a clip. + * This method is a shortcut to beginning [[yii\widgets\Clip]] + * @param string $id the clip ID. + * @param boolean $renderInPlace whether to render the clip content in place. + * Defaults to false, meaning the captured clip will not be displayed. + * @return \yii\widgets\Clip the Clip widget instance + * @see \yii\widgets\Clip */ - public function findViewFile($view) + public function beginClip($id, $renderInPlace = false) { - if (FileHelper::getExtension($view) === '') { - $view .= '.php'; - } - if (strncmp($view, '@', 1) === 0) { - // e.g. "@application/views/common" - if (($file = Yii::getAlias($view)) === false) { - throw new InvalidConfigException("Invalid path alias: $view"); - } - } elseif (strncmp($view, '/', 1) !== 0) { - // e.g. "index" - if ($this->owner instanceof Controller || $this->owner instanceof Widget) { - $file = $this->owner->getViewPath() . DIRECTORY_SEPARATOR . $view; - } elseif ($this->owner !== null) { - $class = new \ReflectionClass($this->owner); - $file = dirname($class->getFileName()) . DIRECTORY_SEPARATOR . 'views' . DIRECTORY_SEPARATOR . $view; - } else { - $file = Yii::$application->getViewPath() . DIRECTORY_SEPARATOR . $view; - } - } elseif (strncmp($view, '//', 2) !== 0 && Yii::$application->controller !== null) { - // e.g. "/site/index" - $file = Yii::$application->controller->module->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); - } else { - // e.g. "//layouts/main" - $file = Yii::$application->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); - } + return $this->beginWidget('yii\widgets\Clip', array( + 'id' => $id, + 'renderInPlace' => $renderInPlace, + 'view' => $this, + )); + } - if (is_file($file)) { - if ($this->enableTheme && ($theme = Yii::$application->getTheme()) !== null) { - $file = $theme->apply($file); - } - return $this->enableI18N ? FileHelper::localize($file, $this->language, $this->sourceLanguage) : $file; - } else { - throw new InvalidConfigException("View file for view '$view' does not exist: $file"); - } + /** + * Ends recording a clip. + */ + public function endClip() + { + $this->endWidget(); } /** - * Finds the layout file that can be applied to the view. - * - * The applicable layout is resolved according to the following rules: - * - * - If [[layout]] is specified as a string, use it as the layout name and search for the layout file - * under the layout path of the currently active module; - * - If [[layout]] is null and [[owner]] is a controller: - * * If the controller's [[Controller::layout|layout]] is a string, use it as the layout name - * and search for the layout file under the layout path of the parent module of the controller; - * * If the controller's [[Controller::layout|layout]] is null, look through its ancestor modules - * and find the first one whose [[Module::layout|layout]] is not null. Use the layout specified - * by that module; - * - Returns false for all other cases. - * - * Like view names, a layout name can take several formats: - * - * - path alias (e.g. "@application/views/layouts/main"); - * - absolute path (e.g. "/main"): the layout name starts with a slash. The actual layout file will be - * looked for under the [[Application::layoutPath|layout path]] of the application; - * - relative path (e.g. "main"): the actual layout layout file will be looked for under the - * [[Module::viewPath|view path]] of the context module determined by the above layout resolution process. - * - * If the layout name does not contain a file extension, it will default to `.php`. - * - * If [[enableTheme]] is true and there is an active application them, the method will also - * attempt to use a themed version of the layout file, when available. + * Begins the rendering of content that is to be decorated by the specified view. + * @param string $view the name of the view that will be used to decorate the content enclosed by this widget. + * Please refer to [[View::findViewFile()]] on how to set this property. + * @param array $params the variables (name=>value) to be extracted and made available in the decorative view. + * @return \yii\widgets\ContentDecorator the ContentDecorator widget instance + * @see \yii\widgets\ContentDecorator + */ + public function beginContent($view, $params = array()) + { + return $this->beginWidget('yii\widgets\ContentDecorator', array( + 'view' => $this, + 'viewName' => $view, + 'params' => $params, + )); + } + + /** + * Ends the rendering of content. + */ + public function endContent() + { + $this->endWidget(); + } + + /** + * Begins fragment caching. + * This method will display cached content if it is available. + * If not, it will start caching and would expect an [[endCache()]] + * call to end the cache and save the content into cache. + * A typical usage of fragment caching is as follows, * - * And if [[enableI18N]] is true, the method will attempt to use a translated version of the layout file, - * when available. + * ~~~ + * if($this->beginCache($id)) { + * // ...generate content here + * $this->endCache(); + * } + * ~~~ * - * @return string|boolean the layout file path, or false if layout is not needed. - * @throws InvalidConfigException if the layout file cannot be found + * @param string $id a unique ID identifying the fragment to be cached. + * @param array $properties initial property values for [[\yii\widgets\FragmentCache]] + * @return boolean whether you should generate the content for caching. + * False if the cached version is available. */ - public function findLayoutFile() + public function beginCache($id, $properties = array()) { - /** @var $module Module */ - if (is_string($this->layout)) { - if (Yii::$application->controller) { - $module = Yii::$application->controller->module; - } else { - $module = Yii::$application; - } - $view = $this->layout; - } elseif ($this->owner instanceof Controller) { - if (is_string($this->owner->layout)) { - $module = $this->owner->module; - $view = $this->owner->layout; - } elseif ($this->owner->layout === null) { - $module = $this->owner->module; - while ($module !== null && $module->layout === null) { - $module = $module->module; - } - if ($module !== null && is_string($module->layout)) { - $view = $module->layout; - } - } - } - - if (!isset($view)) { + $properties['id'] = $id; + $properties['view'] = $this; + /** @var $cache \yii\widgets\FragmentCache */ + $cache = $this->beginWidget('yii\widgets\FragmentCache', $properties); + if ($cache->getCachedContent() !== false) { + $this->endCache(); return false; - } - - if (FileHelper::getExtension($view) === '') { - $view .= '.php'; - } - if (strncmp($view, '@', 1) === 0) { - if (($file = Yii::getAlias($view)) === false) { - throw new InvalidConfigException("Invalid path alias: $view"); - } - } elseif (strncmp($view, '/', 1) === 0) { - $file = Yii::$application->getLayoutPath() . DIRECTORY_SEPARATOR . $view; } else { - $file = $module->getLayoutPath() . DIRECTORY_SEPARATOR . $view; + return true; } + } - if (is_file($file)) { - if ($this->enableTheme && ($theme = Yii::$application->getTheme()) !== null) { - $file = $theme->apply($file); - } - return $this->enableI18N ? FileHelper::localize($file, $this->language, $this->sourceLanguage) : $file; - } else { - throw new InvalidConfigException("Layout file for layout '$view' does not exist: $file"); - } + /** + * Ends fragment caching. + */ + public function endCache() + { + $this->endWidget(); } } \ No newline at end of file diff --git a/framework/base/ViewRenderer.php b/framework/base/ViewRenderer.php index ecb216d..576bbe8 100644 --- a/framework/base/ViewRenderer.php +++ b/framework/base/ViewRenderer.php @@ -1,9 +1,7 @@ createView()->renderPartial($view, $params); + return Yii::$app->getView()->render($view, $params, $this); } /** - * @return View + * Renders a view file. + * @param string $file the view file to be rendered. This can be either a file path or a path alias. + * @param array $params the parameters (name-value pairs) that should be made available in the view. + * @return string the rendering result. + * @throws InvalidParamException if the view file does not exist. */ - public function createView() + public function renderFile($file, $params = array()) { - return new View($this); + return Yii::$app->getView()->renderFile($file, $params, $this); } /** diff --git a/framework/caching/ApcCache.php b/framework/caching/ApcCache.php index b4df296..dd954cc 100644 --- a/framework/caching/ApcCache.php +++ b/framework/caching/ApcCache.php @@ -1,9 +1,7 @@ buildKey($className, $method, $id); * ~~~ * - * @param string $key the first parameter + * @param array|string $key the key to be normalized * @return string the generated cache key */ public function buildKey($key) { - if (func_num_args() === 1 && ctype_alnum($key) && strlen($key) <= 32) { - return (string)$key; + if (is_string($key)) { + return ctype_alnum($key) && StringHelper::strlen($key) <= 32 ? $key : md5($key); } else { - $params = func_get_args(); - return md5(serialize($params)); + return md5(json_encode($key)); } } diff --git a/framework/caching/ChainedDependency.php b/framework/caching/ChainedDependency.php index 570715d..9c4e547 100644 --- a/framework/caching/ChainedDependency.php +++ b/framework/caching/ChainedDependency.php @@ -1,9 +1,7 @@ array( + * 'class' => 'yii\caching\DbCache', + * // 'db' => 'mydb', + * // 'cacheTable' => 'my_cache', + * ) + * ~~~ * * @author Qiang Xue * @since 2.0 @@ -47,50 +36,56 @@ use yii\db\Query; class DbCache extends Cache { /** - * @var string the ID of the [[Connection|DB connection]] application component. Defaults to 'db'. + * @var Connection|string the DB connection object or the application component ID of the DB connection. + * After the DbCache object is created, if you want to change this property, you should only assign it + * with a DB connection object. */ - public $connectionID = 'db'; + public $db = 'db'; /** - * @var string name of the DB table to store cache content. Defaults to 'tbl_cache'. - * The table must be created before using this cache component. + * @var string name of the DB table to store cache content. + * The table should be pre-created as follows: + * + * ~~~ + * CREATE TABLE tbl_cache ( + * id char(128) NOT NULL PRIMARY KEY, + * expire int(11), + * data BLOB + * ); + * ~~~ + * + * where 'BLOB' refers to the BLOB-type of your preferred DBMS. Below are the BLOB type + * that can be used for some popular DBMS: + * + * - MySQL: LONGBLOB + * - PostgreSQL: BYTEA + * - MSSQL: BLOB + * + * When using DbCache in a production server, we recommend you create a DB index for the 'expire' + * column in the cache table to improve the performance. */ - public $cacheTableName = 'tbl_cache'; + public $cacheTable = 'tbl_cache'; /** * @var integer the probability (parts per million) that garbage collection (GC) should be performed - * when storing a piece of data in the cache. Defaults to 10, meaning 0.001% chance. + * when storing a piece of data in the cache. Defaults to 100, meaning 0.01% chance. * This number should be between 0 and 1000000. A value 0 meaning no GC will be performed at all. **/ public $gcProbability = 100; - /** - * @var Connection the DB connection instance - */ - private $_db; - /** - * Returns the DB connection instance used for caching purpose. - * @return Connection the DB connection instance - * @throws InvalidConfigException if [[connectionID]] does not point to a valid application component. - */ - public function getDb() - { - if ($this->_db === null) { - $db = \Yii::$application->getComponent($this->connectionID); - if ($db instanceof Connection) { - $this->_db = $db; - } else { - throw new InvalidConfigException("DbCache::connectionID must refer to the ID of a DB application component."); - } - } - return $this->_db; - } /** - * Sets the DB connection used by the cache component. - * @param Connection $value the DB connection instance + * Initializes the DbCache component. + * This method will initialize the [[db]] property to make sure it refers to a valid DB connection. + * @throws InvalidConfigException if [[db]] is invalid. */ - public function setDb($value) + public function init() { - $this->_db = $value; + parent::init(); + if (is_string($this->db)) { + $this->db = Yii::$app->getComponent($this->db); + } + if (!$this->db instanceof Connection) { + throw new InvalidConfigException("DbCache::db must be either a DB connection instance or the application component ID of a DB connection."); + } } /** @@ -103,17 +98,16 @@ class DbCache extends Cache { $query = new Query; $query->select(array('data')) - ->from($this->cacheTableName) - ->where('id = :id AND (expire = 0 OR expire > :time)', array(':id' => $key, ':time' => time())); - $db = $this->getDb(); - if ($db->enableQueryCache) { + ->from($this->cacheTable) + ->where('id = :id AND (expire = 0 OR expire >' . time() . ')', array(':id' => $key)); + if ($this->db->enableQueryCache) { // temporarily disable and re-enable query caching - $db->enableQueryCache = false; - $result = $query->createCommand($db)->queryScalar(); - $db->enableQueryCache = true; + $this->db->enableQueryCache = false; + $result = $query->createCommand($this->db)->queryScalar(); + $this->db->enableQueryCache = true; return $result; } else { - return $query->createCommand($db)->queryScalar(); + return $query->createCommand($this->db)->queryScalar(); } } @@ -129,17 +123,16 @@ class DbCache extends Cache } $query = new Query; $query->select(array('id', 'data')) - ->from($this->cacheTableName) + ->from($this->cacheTable) ->where(array('id' => $keys)) - ->andWhere("expire = 0 OR expire > " . time() . ")"); + ->andWhere('(expire = 0 OR expire > ' . time() . ')'); - $db = $this->getDb(); - if ($db->enableQueryCache) { - $db->enableQueryCache = false; - $rows = $query->createCommand($db)->queryAll(); - $db->enableQueryCache = true; + if ($this->db->enableQueryCache) { + $this->db->enableQueryCache = false; + $rows = $query->createCommand($this->db)->queryAll(); + $this->db->enableQueryCache = true; } else { - $rows = $query->createCommand($db)->queryAll(); + $rows = $query->createCommand($this->db)->queryAll(); } $results = array(); @@ -163,13 +156,13 @@ class DbCache extends Cache */ protected function setValue($key, $value, $expire) { - $command = $this->getDb()->createCommand(); - $command->update($this->cacheTableName, array( - 'expire' => $expire > 0 ? $expire + time() : 0, - 'data' => array($value, \PDO::PARAM_LOB), - ), array( - 'id' => $key, - ));; + $command = $this->db->createCommand() + ->update($this->cacheTable, array( + 'expire' => $expire > 0 ? $expire + time() : 0, + 'data' => array($value, \PDO::PARAM_LOB), + ), array( + 'id' => $key, + )); if ($command->execute()) { $this->gc(); @@ -198,14 +191,13 @@ class DbCache extends Cache $expire = 0; } - $command = $this->getDb()->createCommand(); - $command->insert($this->cacheTableName, array( - 'id' => $key, - 'expire' => $expire, - 'data' => array($value, \PDO::PARAM_LOB), - )); try { - $command->execute(); + $this->db->createCommand() + ->insert($this->cacheTable, array( + 'id' => $key, + 'expire' => $expire, + 'data' => array($value, \PDO::PARAM_LOB), + ))->execute(); return true; } catch (\Exception $e) { return false; @@ -220,8 +212,9 @@ class DbCache extends Cache */ protected function deleteValue($key) { - $command = $this->getDb()->createCommand(); - $command->delete($this->cacheTableName, array('id' => $key))->execute(); + $this->db->createCommand() + ->delete($this->cacheTable, array('id' => $key)) + ->execute(); return true; } @@ -233,8 +226,9 @@ class DbCache extends Cache public function gc($force = false) { if ($force || mt_rand(0, 1000000) < $this->gcProbability) { - $command = $this->getDb()->createCommand(); - $command->delete($this->cacheTableName, 'expire > 0 AND expire < ' . time())->execute(); + $this->db->createCommand() + ->delete($this->cacheTable, 'expire > 0 AND expire < ' . time()) + ->execute(); } } @@ -245,8 +239,9 @@ class DbCache extends Cache */ protected function flushValues() { - $command = $this->getDb()->createCommand(); - $command->delete($this->cacheTableName)->execute(); + $this->db->createCommand() + ->delete($this->cacheTable) + ->execute(); return true; } } diff --git a/framework/caching/DbDependency.php b/framework/caching/DbDependency.php index 7ffdb4e..cbe0ae1 100644 --- a/framework/caching/DbDependency.php +++ b/framework/caching/DbDependency.php @@ -1,23 +1,21 @@ * @since 2.0 @@ -25,88 +23,52 @@ use yii\db\Query; class DbDependency extends Dependency { /** - * @var string the ID of the [[Connection|DB connection]] application component. Defaults to 'db'. + * @var string the application component ID of the DB connection. */ - public $connectionID = 'db'; + public $db = 'db'; /** - * @var Query the SQL query whose result is used to determine if the dependency has been changed. + * @var string the SQL query whose result is used to determine if the dependency has been changed. * Only the first row of the query result will be used. */ - public $query; + public $sql; /** - * @var Connection the DB connection instance + * @var array the parameters (name=>value) to be bound to the SQL statement specified by [[sql]]. */ - private $_db; + public $params; /** * Constructor. - * @param Query $query the SQL query whose result is used to determine if the dependency has been changed. + * @param string $sql the SQL query whose result is used to determine if the dependency has been changed. + * @param array $params the parameters (name=>value) to be bound to the SQL statement specified by [[sql]]. * @param array $config name-value pairs that will be used to initialize the object properties */ - public function __construct($query = null, $config = array()) + public function __construct($sql, $params = array(), $config = array()) { - $this->query = $query; + $this->sql = $sql; + $this->params = $params; parent::__construct($config); } /** - * PHP sleep magic method. - * This method ensures that the database instance is set null because it contains resource handles. - * @return array - */ - public function __sleep() - { - $this->_db = null; - return array_keys((array)$this); - } - - /** * Generates the data needed to determine if dependency has been changed. * This method returns the value of the global state. * @return mixed the data needed to determine if dependency has been changed. */ protected function generateDependencyData() { - $db = $this->getDb(); - /** - * @var \yii\db\Command $command - */ - $command = $this->query->createCommand($db); + $db = Yii::$app->getComponent($this->db); + if (!$db instanceof Connection) { + throw new InvalidConfigException("DbDependency::db must be the application component ID of a DB connection."); + } + if ($db->enableQueryCache) { // temporarily disable and re-enable query caching $db->enableQueryCache = false; - $result = $command->queryRow(); + $result = $db->createCommand($this->sql, $this->params)->queryRow(); $db->enableQueryCache = true; } else { - $result = $command->queryRow(); + $result = $db->createCommand($this->sql, $this->params)->queryRow(); } return $result; } - - /** - * Returns the DB connection instance used for caching purpose. - * @return Connection the DB connection instance - * @throws InvalidConfigException if [[connectionID]] does not point to a valid application component. - */ - public function getDb() - { - if ($this->_db === null) { - $db = \Yii::$application->getComponent($this->connectionID); - if ($db instanceof Connection) { - $this->_db = $db; - } else { - throw new InvalidConfigException("DbCache::connectionID must refer to the ID of a DB application component."); - } - } - return $this->_db; - } - - /** - * Sets the DB connection used by the cache component. - * @param Connection $value the DB connection instance - */ - public function setDb($value) - { - $this->_db = $value; - } } diff --git a/framework/caching/Dependency.php b/framework/caching/Dependency.php index 2e66145..feb8c07 100644 --- a/framework/caching/Dependency.php +++ b/framework/caching/Dependency.php @@ -1,9 +1,7 @@ cache`. + * a 'cache' application component and save the check of existence of `\Yii::$app->cache`. * By replacing DummyCache with some other cache component, one can quickly switch from * non-caching mode to caching mode. * diff --git a/framework/caching/ExpressionDependency.php b/framework/caching/ExpressionDependency.php index 7ad7543..e13c962 100644 --- a/framework/caching/ExpressionDependency.php +++ b/framework/caching/ExpressionDependency.php @@ -1,9 +1,7 @@ cachePath = \Yii::getAlias($this->cachePath); - if ($this->cachePath === false) { - throw new InvalidConfigException('FileCache.cachePath must be a valid path alias.'); - } if (!is_dir($this->cachePath)) { mkdir($this->cachePath, 0777, true); } diff --git a/framework/caching/FileDependency.php b/framework/caching/FileDependency.php index 89b356c..3797dde 100644 --- a/framework/caching/FileDependency.php +++ b/framework/caching/FileDependency.php @@ -1,9 +1,7 @@ getRequest(); if ($request->getIsConsoleRequest()) { - return $this->runAction($request->route, $request->params); + list ($route, $params) = $request->resolve(); + return $this->runAction($route, $params); } else { - echo "Error: this script must be run from the command line."; - return 1; + throw new Exception(\Yii::t('yii|This script must be run from the command line.')); } } @@ -106,14 +106,14 @@ class Application extends \yii\base\Application * @param string $route the route that specifies the action. * @param array $params the parameters to be passed to the action * @return integer the status code returned by the action execution. 0 means normal, and other values mean abnormal. + * @throws Exception if the route is invalid */ public function runAction($route, $params = array()) { try { return parent::runAction($route, $params); } catch (InvalidRouteException $e) { - echo "Error: unknown command \"$route\".\n"; - return 1; + throw new Exception(\Yii::t('yii|Unknown command "{command}".', array('{command}' => $route))); } } @@ -127,8 +127,8 @@ class Application extends \yii\base\Application 'message' => 'yii\console\controllers\MessageController', 'help' => 'yii\console\controllers\HelpController', 'migrate' => 'yii\console\controllers\MigrateController', - 'shell' => 'yii\console\controllers\ShellController', - 'create' => 'yii\console\controllers\CreateController', + 'app' => 'yii\console\controllers\AppController', + 'cache' => 'yii\console\controllers\CacheController', ); } @@ -148,9 +148,4 @@ class Application extends \yii\base\Application ), )); } - - public function usageError($message) - { - - } } diff --git a/framework/console/Controller.php b/framework/console/Controller.php index 16968f2..9924822 100644 --- a/framework/console/Controller.php +++ b/framework/console/Controller.php @@ -1,9 +1,7 @@ globalOptions(); foreach ($params as $name => $value) { - if ($class->hasProperty($name)) { - $property = $class->getProperty($name); - if ($property->isPublic() && !$property->isStatic() && $property->getDeclaringClass()->getName() === get_class($this)) { - $this->$name = $value; - unset($params[$name]); - } + if (in_array($name, $options, true)) { + $this->$name = $value; + unset($params[$name]); } } } @@ -64,25 +59,60 @@ class Controller extends \yii\base\Controller } /** - * Validates the parameter being bound to actions. - * This method is invoked when parameters are being bound to the currently requested action. - * Child classes may override this method to throw exceptions when there are missing and/or unknown parameters. - * @param Action $action the currently requested action - * @param array $missingParams the names of the missing parameters - * @param array $unknownParams the unknown parameters (name=>value) - * @throws InvalidRequestException if there are missing or unknown parameters + * Binds the parameters to the action. + * This method is invoked by [[Action]] when it begins to run with the given parameters. + * This method will first bind the parameters with the [[globalOptions()|global options]] + * available to the action. It then validates the given arguments. + * @param Action $action the action to be bound with parameters + * @param array $params the parameters to be bound to the action + * @return array the valid parameters that the action can run with. + * @throws Exception if there are unknown options or missing arguments */ - public function validateActionParams($action, $missingParams, $unknownParams) + public function bindActionParams($action, $params) { - if (!empty($missingParams)) { - throw new InvalidRequestException(Yii::t('yii', 'Missing required options: {params}', array( - '{params}' => implode(', ', $missingParams), + if ($params !== array()) { + $options = $this->globalOptions(); + foreach ($params as $name => $value) { + if (in_array($name, $options, true)) { + $this->$name = $value; + unset($params[$name]); + } + } + } + + $args = isset($params[Request::ANONYMOUS_PARAMS]) ? $params[Request::ANONYMOUS_PARAMS] : array(); + unset($params[Request::ANONYMOUS_PARAMS]); + if ($params !== array()) { + throw new Exception(Yii::t('yii|Unknown options: {params}', array( + '{params}' => implode(', ', array_keys($params)), ))); - } elseif (!empty($unknownParams)) { - throw new InvalidRequestException(Yii::t('yii', 'Unknown options: {params}', array( - '{params}' => implode(', ', $unknownParams), + } + + if ($action instanceof InlineAction) { + $method = new \ReflectionMethod($this, $action->actionMethod); + } else { + $method = new \ReflectionMethod($action, 'run'); + } + + $missing = array(); + foreach ($method->getParameters() as $i => $param) { + $name = $param->getName(); + if (!isset($args[$i])) { + if ($param->isDefaultValueAvailable()) { + $args[$i] = $param->getDefaultValue(); + } else { + $missing[] = $name; + } + } + } + + if ($missing !== array()) { + throw new Exception(Yii::t('yii|Missing required arguments: {params}', array( + '{params}' => implode(', ', $missing), ))); } + + return $args; } /** @@ -103,12 +133,17 @@ class Controller extends \yii\base\Controller } } - public function usageError($message) - { - echo "\nError: $message\n"; - Yii::$application->end(1); - } - + /** + * Returns the names of the global options for this command. + * A global option requires the existence of a public member variable whose + * name is the option name. + * Child classes may override this method to specify possible global options. + * + * Note that the values setting via global options are not available + * until [[beforeAction()]] is being called. + * + * @return array the names of the global options for this command. + */ public function globalOptions() { return array(); diff --git a/framework/console/Exception.php b/framework/console/Exception.php new file mode 100644 index 0000000..cb10c19 --- /dev/null +++ b/framework/console/Exception.php @@ -0,0 +1,28 @@ + + * @since 2.0 + */ +class Exception extends UserException +{ + /** + * @return string the user-friendly name of this exception + */ + public function getName() + { + return \Yii::t('yii|Error'); + } +} + diff --git a/framework/console/Request.php b/framework/console/Request.php index dbf80ba..ed477e9 100644 --- a/framework/console/Request.php +++ b/framework/console/Request.php @@ -1,9 +1,7 @@ resolveRequest(); - } + const ANONYMOUS_PARAMS = '-args'; public function getRawParams() { return isset($_SERVER['argv']) ? $_SERVER['argv'] : array(); } - protected function resolveRequest() + /** + * Resolves the current request into a route and the associated parameters. + * @return array the first element is the route, and the second is the associated parameters. + */ + public function resolve() { $rawParams = $this->getRawParams(); array_shift($rawParams); // the 1st argument is the yiic script name if (isset($rawParams[0])) { - $this->route = $rawParams[0]; + $route = $rawParams[0]; array_shift($rawParams); } else { - $this->route = ''; + $route = ''; } - $this->params = array(); + $params = array(self::ANONYMOUS_PARAMS => array()); foreach ($rawParams as $param) { if (preg_match('/^--(\w+)(=(.*))?$/', $param, $matches)) { $name = $matches[1]; - $this->params[$name] = isset($matches[3]) ? $matches[3] : true; + $params[$name] = isset($matches[3]) ? $matches[3] : true; } else { - $this->params['args'][] = $param; + $params[self::ANONYMOUS_PARAMS][] = $param; } } + + return array($route, $params); } } diff --git a/framework/console/controllers/CreateController.php b/framework/console/controllers/AppController.php similarity index 91% rename from framework/console/controllers/CreateController.php rename to framework/console/controllers/AppController.php index 7bd7fd0..93ef5f5 100644 --- a/framework/console/controllers/CreateController.php +++ b/framework/console/controllers/AppController.php @@ -1,16 +1,14 @@ * @since 2.0 */ -class CreateController extends Controller +class AppController extends Controller { private $_rootPath; private $_config; /** * @var string custom template path. If specified, templates will be - * searched there additionally to `framework/console/create`. + * searched there additionally to `framework/console/webapp`. */ public $templatesPath; @@ -46,6 +44,16 @@ class CreateController extends Controller } } + public function globalOptions() + { + return array('templatesPath', 'type'); + } + + public function actionIndex() + { + $this->forward('help/index', array('-args' => array('app/create'))); + } + /** * Generates Yii application at the path specified via appPath parameter. * @@ -56,7 +64,7 @@ class CreateController extends Controller * @throws \yii\base\Exception if path specified is not valid * @return integer the exit status */ - public function actionIndex($path) + public function actionCreate($path) { $path = strtr($path, '/\\', DIRECTORY_SEPARATOR); if(strpos($path, DIRECTORY_SEPARATOR) === false) { @@ -127,7 +135,7 @@ class CreateController extends Controller */ protected function getDefaultTemplatesPath() { - return realpath(__DIR__.'/../create'); + return realpath(__DIR__.'/../webapp'); } /** diff --git a/framework/console/controllers/CacheController.php b/framework/console/controllers/CacheController.php new file mode 100644 index 0000000..6765f9b --- /dev/null +++ b/framework/console/controllers/CacheController.php @@ -0,0 +1,47 @@ + + * @since 2.0 + */ +class CacheController extends Controller +{ + public function actionIndex() + { + $this->forward('help/index', array('-args' => array('cache/flush'))); + } + + /** + * Flushes cache. + * @param string $component Name of the cache application component to use. + * + * @throws \yii\console\Exception + */ + public function actionFlush($component = 'cache') + { + /** @var $cache Cache */ + $cache = \Yii::$app->getComponent($component); + if(!$cache || !$cache instanceof Cache) { + throw new Exception('Application component "'.$component.'" is not defined or not a cache.'); + } + + if(!$cache->flush()) { + throw new Exception('Unable to flush cache.'); + } + + echo "\nDone.\n"; + } +} diff --git a/framework/console/controllers/HelpController.php b/framework/console/controllers/HelpController.php index 6e4b397..ea7e3d5 100644 --- a/framework/console/controllers/HelpController.php +++ b/framework/console/controllers/HelpController.php @@ -1,18 +1,19 @@ getHelp(); - } else { - $result = \Yii::$application->createController($args[0]); + if ($command !== null) { + $result = Yii::$app->createController($command); if ($result === false) { - echo "Error: no help for unknown command \"{$args[0]}\".\n"; - return 1; + throw new Exception(Yii::t('yii|No help for unknown command "{command}".', array( + '{command}' => $command, + ))); } list($controller, $actionID) = $result; - if ($actionID === '') { - $status = $this->getControllerHelp($controller); + $actions = $this->getActions($controller); + if ($actionID !== '' || count($actions) === 1 && $actions[0] === $controller->defaultAction) { + $this->getActionHelp($controller, $actionID); } else { - $status = $this->getActionHelp($controller, $actionID); + $this->getControllerHelp($controller); } + } else { + $this->getHelp(); } - return $status; } /** @@ -76,7 +79,7 @@ class HelpController extends Controller */ public function getCommands() { - $commands = $this->getModuleCommands(\Yii::$application); + $commands = $this->getModuleCommands(Yii::$app); sort($commands); return array_unique($commands); } @@ -91,7 +94,6 @@ class HelpController extends Controller $actions = array_keys($controller->actions()); $class = new \ReflectionClass($controller); foreach ($class->getMethods() as $method) { - /** @var $method \ReflectionMethod */ $name = $method->getName(); if ($method->isPublic() && !$method->isStatic() && strpos($name, 'action') === 0 && $name !== 'actions') { $actions[] = StringHelper::camel2id(substr($name, 6)); @@ -136,29 +138,25 @@ class HelpController extends Controller /** * Displays all available commands. - * @return integer the exit status */ protected function getHelp() { $commands = $this->getCommands(); if ($commands !== array()) { - echo "\nUsage: yiic [...options...]\n\n"; echo "The following commands are available:\n\n"; foreach ($commands as $command) { - echo " * $command\n"; + echo "* $command\n"; } echo "\nTo see the help of each command, enter:\n"; - echo "\n yiic help \n"; + echo "\n yiic help \n\n"; } else { echo "\nNo commands are found.\n"; } - return 0; } /** * Displays the overall information of the command. * @param Controller $controller the controller instance - * @return integer the exit status */ protected function getControllerHelp($controller) { @@ -169,181 +167,255 @@ class HelpController extends Controller } if ($comment !== '') { - echo "\n" . $comment . "\n"; - } - - $options = $this->getGlobalOptions($class, $controller); - if ($options !== array()) { - echo "\nGLOBAL OPTIONS"; - echo "\n--------------\n\n"; - foreach ($options as $name => $description) { - echo " --$name"; - if ($description != '') { - echo ": $description\n"; - } - echo "\n"; - } + echo "\nDESCRIPTION\n"; + echo "\n" . $comment . "\n\n"; } $actions = $this->getActions($controller); if ($actions !== array()) { - echo "\nSUB-COMMANDS"; - echo "\n------------\n\n"; + echo "\nSUB-COMMANDS\n\n"; $prefix = $controller->getUniqueId(); foreach ($actions as $action) { - if ($controller->defaultAction === $action) { - echo " * $prefix (default)\n"; + if ($action === $controller->defaultAction) { + echo "* $prefix/$action (default)"; } else { - echo " * $prefix/$action\n"; + echo "* $prefix/$action"; } + $summary = $this->getActionSummary($controller, $action); + if ($summary !== '') { + echo ': ' . $summary; + } + echo "\n"; } - echo "\n"; + echo "\n\nTo see the detailed information about individual sub-commands, enter:\n"; + echo "\n yiic help \n\n"; } + } - return 0; + /** + * Returns the short summary of the action. + * @param Controller $controller the controller instance + * @param string $actionID action ID + * @return string the summary about the action + */ + protected function getActionSummary($controller, $actionID) + { + $action = $controller->createAction($actionID); + if ($action === null) { + return ''; + } + if ($action instanceof InlineAction) { + $reflection = new \ReflectionMethod($controller, $action->actionMethod); + } else { + $reflection = new \ReflectionClass($action); + } + $tags = $this->parseComment($reflection->getDocComment()); + if ($tags['description'] !== '') { + $limit = 73 - strlen($action->getUniqueId()); + if ($actionID === $controller->defaultAction) { + $limit -= 10; + } + if ($limit < 0) { + $limit = 50; + } + $description = $tags['description']; + if (($pos = strpos($tags['description'], "\n")) !== false) { + $description = substr($description, 0, $pos); + } + $text = substr($description, 0, $limit); + return strlen($description) > $limit ? $text . '...' : $text; + } else { + return ''; + } } /** * Displays the detailed information of a command action. * @param Controller $controller the controller instance * @param string $actionID action ID - * @return integer the exit status + * @throws Exception if the action does not exist */ protected function getActionHelp($controller, $actionID) { $action = $controller->createAction($actionID); if ($action === null) { - echo 'Error: no help for unknown sub-command "' . $controller->getUniqueId() . "/$actionID\".\n"; - return 1; + throw new Exception(Yii::t('yii|No help for unknown sub-command "{command}".', array( + '{command}' => rtrim($controller->getUniqueId() . '/' . $actionID, '/'), + ))); } if ($action instanceof InlineAction) { - $method = new \ReflectionMethod($controller, 'action' . $action->id); + $method = new \ReflectionMethod($controller, $action->actionMethod); } else { $method = new \ReflectionMethod($action, 'run'); } - $comment = strtr(trim(preg_replace('/^\s*\**( |\t)?/m', '', trim($method->getDocComment(), '/'))), "\r", ''); - if (preg_match('/^\s*@\w+/m', $comment, $matches, PREG_OFFSET_CAPTURE)) { - $meta = substr($comment, $matches[0][1]); - $comment = trim(substr($comment, 0, $matches[0][1])); + + $tags = $this->parseComment($method->getDocComment()); + $options = $this->getOptionHelps($controller); + + if ($tags['description'] !== '') { + echo "\nDESCRIPTION"; + echo "\n\n" . $tags['description'] . "\n\n"; + } + + echo "\nUSAGE\n\n"; + if ($action->id === $controller->defaultAction) { + echo 'yiic ' . $controller->getUniqueId(); } else { - $meta = ''; + echo "yiic " . $action->getUniqueId(); + } + list ($required, $optional) = $this->getArgHelps($method, isset($tags['param']) ? $tags['param'] : array()); + if (!empty($required)) { + echo ' <' . implode('> <', array_keys($required)) . '>'; + } + if (!empty($optional)) { + echo ' [' . implode('] [', array_keys($optional)) . ']'; } + if (!empty($options)) { + echo ' [...options...]'; + } + echo "\n\n"; - if ($comment !== '') { - echo "\n" . $comment . "\n"; + if (!empty($required) || !empty($optional)) { + echo implode("\n\n", array_merge($required, $optional)) . "\n\n"; } - $options = $this->getOptions($method, $meta); + $options = $this->getOptionHelps($controller); if ($options !== array()) { - echo "\nOPTIONS"; - echo "\n-------\n\n"; - foreach ($options as $name => $description) { - echo " --$name"; - if ($description != '') { - echo ": $description\n"; - } - } - echo "\n"; + echo "\nOPTIONS\n\n"; + echo implode("\n\n", $options) . "\n\n"; } - - return 0; } /** + * Returns the help information about arguments. * @param \ReflectionMethod $method - * @param string $meta - * @return array + * @param string $tags the parsed comment block related with arguments + * @return array the required and optional argument help information */ - protected function getOptions($method, $meta) + protected function getArgHelps($method, $tags) { + if (is_string($tags)) { + $tags = array($tags); + } $params = $method->getParameters(); - $tags = preg_split('/^\s*@/m', $meta, -1, PREG_SPLIT_NO_EMPTY); - $options = array(); - $count = 0; - foreach ($tags as $tag) { - $parts = preg_split('/\s+/', trim($tag), 2); - if ($parts[0] === 'param' && isset($params[$count])) { - $param = $params[$count]; - $comment = isset($parts[1]) ? $parts[1] : ''; - if (preg_match('/^([^\s]+)\s+(\$\w+\s+)?(.*)/s', $comment, $matches)) { - $type = $matches[1]; - $doc = $matches[3]; - } else { - $type = $comment; - $doc = ''; - } - $comment = $type === '' ? '' : ($type . ', '); - if ($param->isDefaultValueAvailable()) { - $value = $param->getDefaultValue(); - if (!is_array($value)) { - $comment .= 'optional (defaults to ' . var_export($value, true) . ').'; - } else { - $comment .= 'optional.'; - } - } else { - $comment .= 'required.'; - } - if (trim($doc) !== '') { - $comment .= "\n" . preg_replace("/^/m", " ", $doc); - } - $options[$param->getName()] = $comment; - $count++; + $optional = $required = array(); + foreach ($params as $i => $param) { + $name = $param->getName(); + $tag = isset($tags[$i]) ? $tags[$i] : ''; + if (preg_match('/^([^\s]+)\s+(\$\w+\s+)?(.*)/s', $tag, $matches)) { + $type = $matches[1]; + $comment = $matches[3]; + } else { + $type = null; + $comment = $tag; } - } - if ($count < count($params)) { - for ($i = $count; $i < count($params); ++$i) { - $options[$params[$i]->getName()] = ''; + if ($param->isDefaultValueAvailable()) { + $optional[$name] = $this->formatOptionHelp('* ' . $name, false, $type, $param->getDefaultValue(), $comment); + } else { + $required[$name] = $this->formatOptionHelp('* ' . $name, true, $type, null, $comment); } } - ksort($options); - return $options; + return array($required, $optional); } /** - * @param \ReflectionClass $class - * @param Controller $controller - * @return array + * Returns the help information about the options available for a console controller. + * @param Controller $controller the console controller + * @return array the help information about the options */ - protected function getGlobalOptions($class, $controller) + protected function getOptionHelps($controller) { + $optionNames = $controller->globalOptions(); + if (empty($optionNames)) { + return array(); + } + + $class = new \ReflectionClass($controller); $options = array(); foreach ($class->getProperties() as $property) { - if (!$property->isPublic() || $property->isStatic() || $property->getDeclaringClass()->getName() !== get_class($controller)) { + $name = $property->getName(); + if (!in_array($name, $optionNames, true)) { continue; } - $name = $property->getName(); - $comment = strtr(trim(preg_replace('/^\s*\**( |\t)?/m', '', trim($property->getDocComment(), '/'))), "\r", ''); - if (preg_match('/^\s*@\w+/m', $comment, $matches, PREG_OFFSET_CAPTURE)) { - $meta = substr($comment, $matches[0][1]); + $defaultValue = $property->getValue($controller); + $tags = $this->parseComment($property->getDocComment()); + if (isset($tags['var']) || isset($tags['property'])) { + $doc = isset($tags['var']) ? $tags['var'] : $tags['property']; + if (is_array($doc)) { + $doc = reset($doc); + } + if (preg_match('/^([^\s]+)(.*)/s', $doc, $matches)) { + $type = $matches[1]; + $comment = $matches[2]; + } else { + $type = null; + $comment = $doc; + } + $options[$name] = $this->formatOptionHelp('--' . $name, false, $type, $defaultValue, $comment); } else { - $meta = ''; + $options[$name] = $this->formatOptionHelp('--' . $name, false, null, $defaultValue, ''); } - $tags = preg_split('/^\s*@/m', $meta, -1, PREG_SPLIT_NO_EMPTY); - foreach ($tags as $tag) { - $parts = preg_split('/\s+/', trim($tag), 2); - $comment = isset($parts[1]) ? $parts[1] : ''; - if ($parts[0] === 'var' || $parts[0] === 'property') { - if (preg_match('/^([^\s]+)(\s+.*)?/s', $comment, $matches)) { - $type = $matches[1]; - $doc = trim($matches[2]); - } else { - $type = $comment; - $doc = ''; - } - $comment = $type === '' ? '' : ($type); - if (trim($doc) !== '') { - $comment .= ', ' . preg_replace("/^/m", "", $doc); - } - $options[$name] = $comment; - break; + } + ksort($options); + return $options; + } + + /** + * Parses the comment block into tags. + * @param string $comment the comment block + * @return array the parsed tags + */ + protected function parseComment($comment) + { + $tags = array(); + $comment = "@description \n" . strtr(trim(preg_replace('/^\s*\**( |\t)?/m', '', trim($comment, '/'))), "\r", ''); + $parts = preg_split('/^\s*@/m', $comment, -1, PREG_SPLIT_NO_EMPTY); + foreach ($parts as $part) { + if (preg_match('/^(\w+)(.*)/ms', trim($part), $matches)) { + $name = $matches[1]; + if (!isset($tags[$name])) { + $tags[$name] = trim($matches[2]); + } elseif (is_array($tags[$name])) { + $tags[$name][] = trim($matches[2]); + } else { + $tags[$name] = array($tags[$name], trim($matches[2])); } } - if (!isset($options[$name])) { - $options[$name] = ''; + } + return $tags; + } + + /** + * Generates a well-formed string for an argument or option. + * @param string $name the name of the argument or option + * @param boolean $required whether the argument is required + * @param string $type the type of the option or argument + * @param mixed $defaultValue the default value of the option or argument + * @param string $comment comment about the option or argument + * @return string the formatted string for the argument or option + */ + protected function formatOptionHelp($name, $required, $type, $defaultValue, $comment) + { + $doc = ''; + $comment = trim($comment); + + if ($defaultValue !== null && !is_array($defaultValue)) { + if ($type === null) { + $type = gettype($defaultValue); } + $doc = "$type (defaults to " . var_export($defaultValue, true) . ")"; + } elseif (trim($type) !== '') { + $doc = $type; } - ksort($options); - return $options; + + if ($doc === '') { + $doc = $comment; + } elseif ($comment !== '') { + $doc .= "\n" . preg_replace("/^/m", " ", $comment); + } + + $name = $required ? "$name (required)" : $name; + return $doc === '' ? $name : "$name: $doc"; } } \ No newline at end of file diff --git a/framework/console/controllers/MessageController.php b/framework/console/controllers/MessageController.php index 2e8ec81..e010b55 100644 --- a/framework/console/controllers/MessageController.php +++ b/framework/console/controllers/MessageController.php @@ -1,10 +1,8 @@ * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ diff --git a/framework/console/controllers/MigrateController.php b/framework/console/controllers/MigrateController.php index e104856..3f816f1 100644 --- a/framework/console/controllers/MigrateController.php +++ b/framework/console/controllers/MigrateController.php @@ -1,539 +1,630 @@ - - * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -namespace yii\console\controllers; - -use Yii; -use yii\console\Controller; - -/** - * This command provides support for database migrations. - * - * The implementation of this command and other supporting classes referenced - * the yii-dbmigrations extension ((https://github.com/pieterclaerhout/yii-dbmigrations), - * authored by Pieter Claerhout. - * - * EXAMPLES - * - * - yiic migrate - * Applies ALL new migrations. This is equivalent to 'yiic migrate up'. - * - * - yiic migrate create create_user_table - * Creates a new migration named 'create_user_table'. - * - * - yiic migrate up 3 - * Applies the next 3 new migrations. - * - * - yiic migrate down - * Reverts the last applied migration. - * - * - yiic migrate down 3 - * Reverts the last 3 applied migrations. - * - * - yiic migrate to 101129_185401 - * Migrates up or down to version 101129_185401. - * - * - yiic migrate mark 101129_185401 - * Modifies the migration history up or down to version 101129_185401. - * No actual migration will be performed. - * - * - yiic migrate history - * Shows all previously applied migration information. - * - * - yiic migrate history 10 - * Shows the last 10 applied migrations. - * - * - yiic migrate new - * Shows all new migrations. - * - * - yiic migrate new 10 - * Shows the next 10 migrations that have not been applied. - * - * @author Qiang Xue - * @since 2.0 - */ -class MigrateController extends Controller -{ - const BASE_MIGRATION = 'm000000_000000_base'; - - /** - * @var string the directory that stores the migrations. This must be specified - * in terms of a path alias, and the corresponding directory must exist. - * Defaults to 'application.migrations' (meaning 'protected/migrations'). - */ - public $migrationPath = '@application/migrations'; - /** - * @var string the name of the table for keeping applied migration information. - * This table will be automatically created if not exists. Defaults to 'tbl_migration'. - * The table structure is: (version varchar(255) primary key, apply_time integer) - */ - public $migrationTable = 'tbl_migration'; - /** - * @var string the application component ID that specifies the database connection for - * storing migration information. Defaults to 'db'. - */ - public $connectionID = 'db'; - /** - * @var string the path of the template file for generating new migrations. This - * must be specified in terms of a path alias (e.g. application.migrations.template). - * If not set, an internal template will be used. - */ - public $templateFile; - /** - * @var string the default command action. It defaults to 'up'. - */ - public $defaultAction = 'up'; - /** - * @var boolean whether to execute the migration in an interactive mode. Defaults to true. - * Set this to false when performing migration in a cron job or background process. - */ - public $interactive = true; - - - public function beforeAction($action) - { - if (parent::beforeAction($action)) { - $path = Yii::getAlias($this->migrationPath); - if ($path === false || !is_dir($path)) { - echo 'Error: the migration directory does not exist "' . $this->migrationPath . "\"\n"; - return false; - } - $this->migrationPath = $path; - $version = Yii::getVersion(); - echo "\nYii Migration Tool v2.0 (based on Yii v{$version})\n\n"; - return true; - } else { - return false; - } - } - - /** - * @param array $args - */ - public function actionUp($args) - { - if (($migrations = $this->getNewMigrations()) === array()) { - echo "No new migration found. Your system is up-to-date.\n"; - Yii::$application->end(); - } - - $total = count($migrations); - $step = isset($args[0]) ? (int)$args[0] : 0; - if ($step > 0) { - $migrations = array_slice($migrations, 0, $step); - } - - $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) === false) { - echo "\nMigration failed. All later migrations are canceled.\n"; - return; - } - } - echo "\nMigrated up successfully.\n"; - } - } - - public function actionDown($args) - { - $step = isset($args[0]) ? (int)$args[0] : 1; - if ($step < 1) { - die("Error: The step parameter must be greater than 0.\n"); - } - - if (($migrations = $this->getMigrationHistory($step)) === array()) { - echo "No migration has been done before.\n"; - return; - } - $migrations = array_keys($migrations); - - $n = count($migrations); - echo "Total $n " . ($n === 1 ? 'migration' : 'migrations') . " to be reverted:\n"; - foreach ($migrations as $migration) { - echo " $migration\n"; - } - echo "\n"; - - if ($this->confirm('Revert the above ' . ($n === 1 ? 'migration' : 'migrations') . "?")) { - foreach ($migrations as $migration) { - if ($this->migrateDown($migration) === false) { - echo "\nMigration failed. All later migrations are canceled.\n"; - return; - } - } - echo "\nMigrated down successfully.\n"; - } - } - - public function actionRedo($args) - { - $step = isset($args[0]) ? (int)$args[0] : 1; - if ($step < 1) { - die("Error: The step parameter must be greater than 0.\n"); - } - - if (($migrations = $this->getMigrationHistory($step)) === array()) { - echo "No migration has been done before.\n"; - return; - } - $migrations = array_keys($migrations); - - $n = count($migrations); - echo "Total $n " . ($n === 1 ? 'migration' : 'migrations') . " to be redone:\n"; - foreach ($migrations as $migration) { - echo " $migration\n"; - } - echo "\n"; - - if ($this->confirm('Redo the above ' . ($n === 1 ? 'migration' : 'migrations') . "?")) { - foreach ($migrations as $migration) { - if ($this->migrateDown($migration) === false) { - echo "\nMigration failed. All later migrations are canceled.\n"; - return; - } - } - foreach (array_reverse($migrations) as $migration) { - if ($this->migrateUp($migration) === false) { - echo "\nMigration failed. All later migrations are canceled.\n"; - return; - } - } - echo "\nMigration redone successfully.\n"; - } - } - - public function actionTo($args) - { - if (isset($args[0])) { - $version = $args[0]; - } else { - $this->usageError('Please specify which version to migrate to.'); - } - - $originalVersion = $version; - if (preg_match('/^m?(\d{6}_\d{6})(_.*?)?$/', $version, $matches)) { - $version = 'm' . $matches[1]; - } else { - die("Error: The version option must be either a timestamp (e.g. 101129_185401)\nor the full name of a migration (e.g. m101129_185401_create_user_table).\n"); - } - - // try migrate up - $migrations = $this->getNewMigrations(); - foreach ($migrations as $i => $migration) { - if (strpos($migration, $version . '_') === 0) { - $this->actionUp(array($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(array($i)); - } - return; - } - } - - die("Error: Unable to find the version '$originalVersion'.\n"); - } - - public function actionMark($args) - { - if (isset($args[0])) { - $version = $args[0]; - } else { - $this->usageError('Please specify which version to mark to.'); - } - $originalVersion = $version; - if (preg_match('/^m?(\d{6}_\d{6})(_.*?)?$/', $version, $matches)) { - $version = 'm' . $matches[1]; - } else { - die("Error: The version option must be either a timestamp (e.g. 101129_185401)\nor the full name of a migration (e.g. m101129_185401_create_user_table).\n"); - } - - $db = $this->getDb(); - - // try mark up - $migrations = $this->getNewMigrations(); - foreach ($migrations as $i => $migration) { - if (strpos($migration, $version . '_') === 0) { - if ($this->confirm("Set migration history at $originalVersion?")) { - $command = $db->createCommand(); - for ($j = 0; $j <= $i; ++$j) { - $command->insert($this->migrationTable, array( - 'version' => $migrations[$j], - 'apply_time' => time(), - )); - } - 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 = $db->createCommand(); - for ($j = 0; $j < $i; ++$j) { - $command->delete($this->migrationTable, $db->quoteColumnName('version') . '=:version', array(':version' => $migrations[$j])); - } - echo "The migration history is set at $originalVersion.\nNo actual migration was performed.\n"; - } - } - return; - } - } - - die("Error: Unable to find the version '$originalVersion'.\n"); - } - - public function actionHistory($args) - { - $limit = isset($args[0]) ? (int)$args[0] : -1; - $migrations = $this->getMigrationHistory($limit); - if ($migrations === array()) { - echo "No migration has been done before.\n"; - } else { - $n = count($migrations); - if ($limit > 0) { - echo "Showing the last $n applied " . ($n === 1 ? 'migration' : 'migrations') . ":\n"; - } else { - echo "Total $n " . ($n === 1 ? 'migration has' : 'migrations have') . " been applied before:\n"; - } - foreach ($migrations as $version => $time) { - echo " (" . date('Y-m-d H:i:s', $time) . ') ' . $version . "\n"; - } - } - } - - public function actionNew($args) - { - $limit = isset($args[0]) ? (int)$args[0] : -1; - $migrations = $this->getNewMigrations(); - if ($migrations === array()) { - echo "No new migrations found. Your system is up-to-date.\n"; - } else { - $n = count($migrations); - if ($limit > 0 && $n > $limit) { - $migrations = array_slice($migrations, 0, $limit); - echo "Showing $limit out of $n new " . ($n === 1 ? 'migration' : 'migrations') . ":\n"; - } else { - echo "Found $n new " . ($n === 1 ? 'migration' : 'migrations') . ":\n"; - } - - foreach ($migrations as $migration) { - echo " " . $migration . "\n"; - } - } - } - - public function actionCreate($args) - { - if (isset($args[0])) { - $name = $args[0]; - } else { - $this->usageError('Please provide the name of the new migration.'); - } - - if (!preg_match('/^\w+$/', $name)) { - die("Error: The name of the migration must contain letters, digits and/or underscore characters only.\n"); - } - - $name = 'm' . gmdate('ymd_His') . '_' . $name; - $content = strtr($this->getTemplate(), array('{ClassName}' => $name)); - $file = $this->migrationPath . DIRECTORY_SEPARATOR . $name . '.php'; - - if ($this->confirm("Create new migration '$file'?")) { - file_put_contents($file, $content); - echo "New migration created successfully.\n"; - } - } - - protected function migrateUp($class) - { - if ($class === self::BASE_MIGRATION) { - return; - } - - echo "*** applying $class\n"; - $start = microtime(true); - $migration = $this->instantiateMigration($class); - if ($migration->up() !== false) { - $this->getDb()->createCommand()->insert($this->migrationTable, array( - 'version' => $class, - 'apply_time' => time(), - )); - $time = microtime(true) - $start; - echo "*** applied $class (time: " . sprintf("%.3f", $time) . "s)\n\n"; - } else { - $time = microtime(true) - $start; - echo "*** failed to apply $class (time: " . sprintf("%.3f", $time) . "s)\n\n"; - return false; - } - } - - protected function migrateDown($class) - { - if ($class === self::BASE_MIGRATION) { - return; - } - - echo "*** reverting $class\n"; - $start = microtime(true); - $migration = $this->instantiateMigration($class); - if ($migration->down() !== false) { - $db = $this->getDb(); - $db->createCommand()->delete($this->migrationTable, $db->quoteColumnName('version') . '=:version', array(':version' => $class)); - $time = microtime(true) - $start; - echo "*** reverted $class (time: " . sprintf("%.3f", $time) . "s)\n\n"; - } else { - $time = microtime(true) - $start; - echo "*** failed to revert $class (time: " . sprintf("%.3f", $time) . "s)\n\n"; - return false; - } - } - - protected function instantiateMigration($class) - { - $file = $this->migrationPath . DIRECTORY_SEPARATOR . $class . '.php'; - require_once($file); - $migration = new $class; - $migration->setDb($this->getDb()); - return $migration; - } - - /** - * @var CDbConnection - */ - private $_db; - - protected function getDb() - { - if ($this->_db !== null) { - return $this->_db; - } else { - if (($this->_db = Yii::$application->getComponent($this->connectionID)) instanceof CDbConnection) { - return $this->_db; - } else { - die("Error: CMigrationCommand.connectionID '{$this->connectionID}' is invalid. Please make sure it refers to the ID of a CDbConnection application component.\n"); - } - } - } - - protected function getMigrationHistory($limit) - { - $db = $this->getDb(); - if ($db->schema->getTable($this->migrationTable) === null) { - $this->createMigrationHistoryTable(); - } - return CHtml::listData($db->createCommand() - ->select('version, apply_time') - ->from($this->migrationTable) - ->order('version DESC') - ->limit($limit) - ->queryAll(), 'version', 'apply_time'); - } - - protected function createMigrationHistoryTable() - { - $db = $this->getDb(); - echo 'Creating migration history table "' . $this->migrationTable . '"...'; - $db->createCommand()->createTable($this->migrationTable, array( - 'version' => 'string NOT NULL PRIMARY KEY', - 'apply_time' => 'integer', - )); - $db->createCommand()->insert($this->migrationTable, array( - 'version' => self::BASE_MIGRATION, - 'apply_time' => time(), - )); - echo "done.\n"; - } - - 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; - } - - protected function getTemplate() - { - if ($this->templateFile !== null) { - return file_get_contents(Yii::getPathOfAlias($this->templateFile) . '.php'); - } else { - return << + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\console\controllers; + +use Yii; +use yii\console\Exception; +use yii\console\Controller; +use yii\db\Connection; +use yii\db\Query; +use yii\helpers\ArrayHelper; + +/** + * This command manages application migrations. + * + * A migration means a set of persistent changes to the application environment + * that is shared among different developers. For example, in an application + * backed by a database, a migration may refer to a set of changes to + * the database, such as creating a new table, adding a new table column. + * + * This command provides support for tracking the migration history, upgrading + * or downloading with migrations, and creating new migration skeletons. + * + * The migration history is stored in a database table named + * as [[migrationTable]]. The table will be automatically created the first time + * this command is executed, if it does not exist. You may also manually + * create it as follows: + * + * ~~~ + * CREATE TABLE tbl_migration ( + * version varchar(255) PRIMARY KEY, + * apply_time integer + * ) + * ~~~ + * + * Below are some common usages of this command: + * + * ~~~ + * # creates a new migration named 'create_user_table' + * yiic migrate/create create_user_table + * + * # applies ALL new migrations + * yiic migrate + * + * # reverts the last applied migration + * yiic migrate/down + * ~~~ + * + * @author Qiang Xue + * @since 2.0 + */ +class MigrateController extends Controller +{ + /** + * The name of the dummy migration that marks the beginning of the whole migration history. + */ + const BASE_MIGRATION = 'm000000_000000_base'; + + /** + * @var string the default command action. + */ + public $defaultAction = 'up'; + /** + * @var string the directory storing the migration classes. This can be either + * a path alias or a directory. + */ + public $migrationPath = '@app/migrations'; + /** + * @var string the name of the table for keeping applied migration information. + */ + public $migrationTable = 'tbl_migration'; + /** + * @var string the template file for generating new migrations. + * This can be either a path alias (e.g. "@app/migrations/template.php") + * or a file path. + */ + public $templateFile = '@yii/views/migration.php'; + /** + * @var boolean whether to execute the migration in an interactive mode. + */ + public $interactive = true; + /** + * @var Connection|string the DB connection object or the application + * component ID of the DB connection. + */ + public $db = 'db'; + + /** + * Returns the names of the global options for this command. + * @return array the names of the global options for this command. + */ + public function globalOptions() + { + return array('migrationPath', 'migrationTable', 'db', 'templateFile', 'interactive'); + } + + /** + * This method is invoked right before an action is to be executed (after all possible filters.) + * It checks the existence of the [[migrationPath]]. + * @param \yii\base\Action $action the action to be executed. + * @return boolean whether the action should continue to be executed. + * @throws Exception if the migration directory does not exist. + */ + public function beforeAction($action) + { + if (parent::beforeAction($action)) { + $path = Yii::getAlias($this->migrationPath); + if (!is_dir($path)) { + throw new Exception("The migration directory \"{$this->migrationPath}\" does not exist."); + } + $this->migrationPath = $path; + + if (is_string($this->db)) { + $this->db = Yii::$app->getComponent($this->db); + } + if (!$this->db instanceof Connection) { + throw new Exception("The 'db' option must refer to the application component ID of a DB connection."); + } + + $version = Yii::getVersion(); + echo "Yii Migration Tool (based on Yii v{$version})\n\n"; + return true; + } else { + return false; + } + } + + /** + * Upgrades the application by applying new migrations. + * For example, + * + * ~~~ + * yiic migrate # apply all new migrations + * yiic migrate 3 # apply the first 3 new migrations + * ~~~ + * + * @param integer $limit the number of new migrations to be applied. If 0, it means + * applying all available new migrations. + */ + public function actionUp($limit = 0) + { + if (($migrations = $this->getNewMigrations()) === array()) { + echo "No new migration found. Your system is up-to-date.\n"; + Yii::$app->end(); + } + + $total = count($migrations); + $limit = (int)$limit; + if ($limit > 0) { + $migrations = array_slice($migrations, 0, $limit); + } + + $n = count($migrations); + if ($n === $total) { + echo "Total $n new " . ($n === 1 ? 'migration' : 'migrations') . " to be applied:\n"; + } else { + echo "Total $n out of $total new " . ($total === 1 ? 'migration' : 'migrations') . " to be applied:\n"; + } + + foreach ($migrations as $migration) { + echo " $migration\n"; + } + echo "\n"; + + if ($this->confirm('Apply the above ' . ($n === 1 ? 'migration' : 'migrations') . "?")) { + foreach ($migrations as $migration) { + if (!$this->migrateUp($migration)) { + echo "\nMigration failed. The rest of the migrations are canceled.\n"; + return; + } + } + echo "\nMigrated up successfully.\n"; + } + } + + /** + * Downgrades the application by reverting old migrations. + * For example, + * + * ~~~ + * yiic migrate/down # revert the last migration + * yiic migrate/down 3 # revert the last 3 migrations + * ~~~ + * + * @param integer $limit the number of migrations to be reverted. Defaults to 1, + * meaning the last applied migration will be reverted. + * @throws Exception if the number of the steps specified is less than 1. + */ + public function actionDown($limit = 1) + { + $limit = (int)$limit; + if ($limit < 1) { + throw new Exception("The step argument must be greater than 0."); + } + + if (($migrations = $this->getMigrationHistory($limit)) === array()) { + echo "No migration has been done before.\n"; + return; + } + $migrations = array_keys($migrations); + + $n = count($migrations); + echo "Total $n " . ($n === 1 ? 'migration' : 'migrations') . " to be reverted:\n"; + foreach ($migrations as $migration) { + echo " $migration\n"; + } + echo "\n"; + + if ($this->confirm('Revert the above ' . ($n === 1 ? 'migration' : 'migrations') . "?")) { + foreach ($migrations as $migration) { + if (!$this->migrateDown($migration)) { + echo "\nMigration failed. The rest of the migrations are canceled.\n"; + return; + } + } + echo "\nMigrated down successfully.\n"; + } + } + + /** + * Redoes the last few migrations. + * + * This command will first revert the specified migrations, and then apply + * them again. For example, + * + * ~~~ + * yiic migrate/redo # redo the last applied migration + * yiic migrate/redo 3 # redo the last 3 applied migrations + * ~~~ + * + * @param integer $limit the number of migrations to be redone. Defaults to 1, + * meaning the last applied migration will be redone. + * @throws Exception if the number of the steps specified is less than 1. + */ + public function actionRedo($limit = 1) + { + $limit = (int)$limit; + if ($limit < 1) { + throw new Exception("The step argument must be greater than 0."); + } + + if (($migrations = $this->getMigrationHistory($limit)) === array()) { + echo "No migration has been done before.\n"; + return; + } + $migrations = array_keys($migrations); + + $n = count($migrations); + echo "Total $n " . ($n === 1 ? 'migration' : 'migrations') . " to be redone:\n"; + foreach ($migrations as $migration) { + echo " $migration\n"; + } + echo "\n"; + + if ($this->confirm('Redo the above ' . ($n === 1 ? 'migration' : 'migrations') . "?")) { + foreach ($migrations as $migration) { + if (!$this->migrateDown($migration)) { + echo "\nMigration failed. The rest of the migrations are canceled.\n"; + return; + } + } + foreach (array_reverse($migrations) as $migration) { + if (!$this->migrateUp($migration)) { + echo "\nMigration failed. The rest of the migrations migrations are canceled.\n"; + return; + } + } + echo "\nMigration redone successfully.\n"; + } + } + + /** + * Upgrades or downgrades till the specified version. + * + * This command will first revert the specified migrations, and then apply + * them again. For example, + * + * ~~~ + * yiic migrate/to 101129_185401 # using timestamp + * yiic migrate/to m101129_185401_create_user_table # using full name + * ~~~ + * + * @param string $version the version name that the application should be migrated to. + * This can be either the timestamp or the full name of the migration. + * @throws Exception if the version argument is invalid + */ + public function actionTo($version) + { + $originalVersion = $version; + if (preg_match('/^m?(\d{6}_\d{6})(_.*?)?$/', $version, $matches)) { + $version = 'm' . $matches[1]; + } else { + throw new Exception("The version argument must be either a timestamp (e.g. 101129_185401)\nor the full name of a migration (e.g. m101129_185401_create_user_table)."); + } + + // try migrate up + $migrations = $this->getNewMigrations(); + foreach ($migrations as $i => $migration) { + if (strpos($migration, $version . '_') === 0) { + $this->actionUp($i + 1); + return; + } + } + + // try migrate down + $migrations = array_keys($this->getMigrationHistory(-1)); + foreach ($migrations as $i => $migration) { + if (strpos($migration, $version . '_') === 0) { + if ($i === 0) { + echo "Already at '$originalVersion'. Nothing needs to be done.\n"; + } else { + $this->actionDown($i); + } + return; + } + } + + throw new Exception("Unable to find the version '$originalVersion'."); + } + + /** + * Modifies the migration history to the specified version. + * + * No actual migration will be performed. + * + * ~~~ + * yiic migrate/mark 101129_185401 # using timestamp + * yiic migrate/mark m101129_185401_create_user_table # using full name + * ~~~ + * + * @param string $version the version at which the migration history should be marked. + * This can be either the timestamp or the full name of the migration. + * @throws Exception if the version argument is invalid or the version cannot be found. + */ + public function actionMark($version) + { + $originalVersion = $version; + if (preg_match('/^m?(\d{6}_\d{6})(_.*?)?$/', $version, $matches)) { + $version = 'm' . $matches[1]; + } else { + throw new Exception("The version argument must be either a timestamp (e.g. 101129_185401)\nor the full name of a migration (e.g. m101129_185401_create_user_table)."); + } + + // try mark up + $migrations = $this->getNewMigrations(); + foreach ($migrations as $i => $migration) { + if (strpos($migration, $version . '_') === 0) { + if ($this->confirm("Set migration history at $originalVersion?")) { + $command = $this->db->createCommand(); + for ($j = 0; $j <= $i; ++$j) { + $command->insert($this->migrationTable, array( + 'version' => $migrations[$j], + 'apply_time' => time(), + ))->execute(); + } + echo "The migration history is set at $originalVersion.\nNo actual migration was performed.\n"; + } + return; + } + } + + // try mark down + $migrations = array_keys($this->getMigrationHistory(-1)); + foreach ($migrations as $i => $migration) { + if (strpos($migration, $version . '_') === 0) { + if ($i === 0) { + echo "Already at '$originalVersion'. Nothing needs to be done.\n"; + } else { + if ($this->confirm("Set migration history at $originalVersion?")) { + $command = $this->db->createCommand(); + for ($j = 0; $j < $i; ++$j) { + $command->delete($this->migrationTable, array( + 'version' => $migrations[$j], + ))->execute(); + } + echo "The migration history is set at $originalVersion.\nNo actual migration was performed.\n"; + } + } + return; + } + } + + throw new Exception("Unable to find the version '$originalVersion'."); + } + + /** + * Displays the migration history. + * + * This command will show the list of migrations that have been applied + * so far. For example, + * + * ~~~ + * yiic migrate/history # showing the last 10 migrations + * yiic migrate/history 5 # showing the last 5 migrations + * yiic migrate/history 0 # showing the whole history + * ~~~ + * + * @param integer $limit the maximum number of migrations to be displayed. + * If it is 0, the whole migration history will be displayed. + */ + public function actionHistory($limit = 10) + { + $limit = (int)$limit; + $migrations = $this->getMigrationHistory($limit); + if ($migrations === array()) { + echo "No migration has been done before.\n"; + } else { + $n = count($migrations); + if ($limit > 0) { + echo "Showing the last $n applied " . ($n === 1 ? 'migration' : 'migrations') . ":\n"; + } else { + echo "Total $n " . ($n === 1 ? 'migration has' : 'migrations have') . " been applied before:\n"; + } + foreach ($migrations as $version => $time) { + echo " (" . date('Y-m-d H:i:s', $time) . ') ' . $version . "\n"; + } + } + } + + /** + * Displays the un-applied new migrations. + * + * This command will show the new migrations that have not been applied. + * For example, + * + * ~~~ + * yiic migrate/new # showing the first 10 new migrations + * yiic migrate/new 5 # showing the first 5 new migrations + * yiic migrate/new 0 # showing all new migrations + * ~~~ + * + * @param integer $limit the maximum number of new migrations to be displayed. + * If it is 0, all available new migrations will be displayed. + */ + public function actionNew($limit = 10) + { + $limit = (int)$limit; + $migrations = $this->getNewMigrations(); + if ($migrations === array()) { + echo "No new migrations found. Your system is up-to-date.\n"; + } else { + $n = count($migrations); + if ($limit > 0 && $n > $limit) { + $migrations = array_slice($migrations, 0, $limit); + echo "Showing $limit out of $n new " . ($n === 1 ? 'migration' : 'migrations') . ":\n"; + } else { + echo "Found $n new " . ($n === 1 ? 'migration' : 'migrations') . ":\n"; + } + + foreach ($migrations as $migration) { + echo " " . $migration . "\n"; + } + } + } + + /** + * Creates a new migration. + * + * This command creates a new migration using the available migration template. + * After using this command, developers should modify the created migration + * skeleton by filling up the actual migration logic. + * + * ~~~ + * yiic migrate/create create_user_table + * ~~~ + * + * @param string $name the name of the new migration. This should only contain + * letters, digits and/or underscores. + * @throws Exception if the name argument is invalid. + */ + public function actionCreate($name) + { + if (!preg_match('/^\w+$/', $name)) { + throw new Exception("The migration name should contain letters, digits and/or underscore characters only."); + } + + $name = 'm' . gmdate('ymd_His') . '_' . $name; + $file = $this->migrationPath . DIRECTORY_SEPARATOR . $name . '.php'; + + if ($this->confirm("Create new migration '$file'?")) { + $content = $this->renderFile(Yii::getAlias($this->templateFile), array( + 'className' => $name, + )); + file_put_contents($file, $content); + echo "New migration created successfully.\n"; + } + } + + /** + * Upgrades with the specified migration class. + * @param string $class the migration class name + * @return boolean whether the migration is successful + */ + protected function migrateUp($class) + { + if ($class === self::BASE_MIGRATION) { + return true; + } + + echo "*** applying $class\n"; + $start = microtime(true); + $migration = $this->createMigration($class); + if ($migration->up() !== false) { + $this->db->createCommand()->insert($this->migrationTable, array( + 'version' => $class, + 'apply_time' => time(), + ))->execute(); + $time = microtime(true) - $start; + echo "*** applied $class (time: " . sprintf("%.3f", $time) . "s)\n\n"; + return true; + } else { + $time = microtime(true) - $start; + echo "*** failed to apply $class (time: " . sprintf("%.3f", $time) . "s)\n\n"; + return false; + } + } + + /** + * Downgrades with the specified migration class. + * @param string $class the migration class name + * @return boolean whether the migration is successful + */ + protected function migrateDown($class) + { + if ($class === self::BASE_MIGRATION) { + return true; + } + + echo "*** reverting $class\n"; + $start = microtime(true); + $migration = $this->createMigration($class); + if ($migration->down() !== false) { + $this->db->createCommand()->delete($this->migrationTable, array( + 'version' => $class, + ))->execute(); + $time = microtime(true) - $start; + echo "*** reverted $class (time: " . sprintf("%.3f", $time) . "s)\n\n"; + return true; + } else { + $time = microtime(true) - $start; + echo "*** failed to revert $class (time: " . sprintf("%.3f", $time) . "s)\n\n"; + return false; + } + } + + /** + * Creates a new migration instance. + * @param string $class the migration class name + * @return \yii\db\Migration the migration instance + */ + protected function createMigration($class) + { + $file = $this->migrationPath . DIRECTORY_SEPARATOR . $class . '.php'; + require_once($file); + return new $class(array( + 'db' => $this->db, + )); + } + + /** + * Returns the migration history. + * @param integer $limit the maximum number of records in the history to be returned + * @return array the migration history + */ + protected function getMigrationHistory($limit) + { + if ($this->db->schema->getTableSchema($this->migrationTable) === null) { + $this->createMigrationHistoryTable(); + } + $query = new Query; + $rows = $query->select(array('version', 'apply_time')) + ->from($this->migrationTable) + ->orderBy('version DESC') + ->limit($limit) + ->createCommand() + ->queryAll(); + $history = ArrayHelper::map($rows, 'version', 'apply_time'); + unset($history[self::BASE_MIGRATION]); + return $history; + } + + /** + * Creates the migration history table. + */ + protected function createMigrationHistoryTable() + { + echo 'Creating migration history table "' . $this->migrationTable . '"...'; + $this->db->createCommand()->createTable($this->migrationTable, array( + 'version' => 'varchar(255) NOT NULL PRIMARY KEY', + 'apply_time' => 'integer', + ))->execute(); + $this->db->createCommand()->insert($this->migrationTable, array( + 'version' => self::BASE_MIGRATION, + 'apply_time' => time(), + ))->execute(); + echo "done.\n"; + } + + /** + * Returns the migrations that are not applied. + * @return array list of new migrations + */ + protected function getNewMigrations() + { + $applied = array(); + foreach ($this->getMigrationHistory(-1) as $version => $time) { + $applied[substr($version, 1, 13)] = true; + } + + $migrations = array(); + $handle = opendir($this->migrationPath); + while (($file = readdir($handle)) !== false) { + if ($file === '.' || $file === '..') { + continue; + } + $path = $this->migrationPath . DIRECTORY_SEPARATOR . $file; + if (preg_match('/^(m(\d{6}_\d{6})_.*?)\.php$/', $file, $matches) && is_file($path) && !isset($applied[$matches[2]])) { + $migrations[] = $matches[1]; + } + } + closedir($handle); + sort($migrations); + return $migrations; + } +} diff --git a/framework/console/create/config.php b/framework/console/webapp/config.php similarity index 83% rename from framework/console/create/config.php rename to framework/console/webapp/config.php index 29f0b0b..112fb18 100644 --- a/framework/console/create/config.php +++ b/framework/console/webapp/config.php @@ -1,5 +1,5 @@ * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ diff --git a/framework/db/ActiveRecord.php b/framework/db/ActiveRecord.php index c6d3d81..d8f2f65 100644 --- a/framework/db/ActiveRecord.php +++ b/framework/db/ActiveRecord.php @@ -1,24 +1,22 @@ * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ namespace yii\db; use yii\base\Model; -use yii\base\Event; +use yii\base\InvalidParamException; use yii\base\ModelEvent; use yii\base\UnknownMethodException; use yii\base\InvalidCallException; use yii\db\Connection; use yii\db\TableSchema; use yii\db\Expression; -use yii\util\StringHelper; +use yii\helpers\StringHelper; /** * ActiveRecord is the base class for classes representing relational data in terms of objects. @@ -96,7 +94,7 @@ class ActiveRecord extends Model */ public static function getDb() { - return \Yii::$application->getDb(); + return \Yii::$app->getDb(); } /** @@ -849,7 +847,7 @@ class ActiveRecord extends Model */ public function beforeSave($insert) { - $event = new ModelEvent($this); + $event = new ModelEvent; $this->trigger($insert ? self::EVENT_BEFORE_INSERT : self::EVENT_BEFORE_UPDATE, $event); return $event->isValid; } @@ -889,7 +887,7 @@ class ActiveRecord extends Model */ public function beforeDelete() { - $event = new ModelEvent($this); + $event = new ModelEvent; $this->trigger(self::EVENT_BEFORE_DELETE, $event); return $event->isValid; } @@ -1045,7 +1043,7 @@ class ActiveRecord extends Model * It can be declared in either the Active Record class itself or one of its behaviors. * @param string $name the relation name * @return ActiveRelation the relation object - * @throws InvalidCallException if the named relation does not exist. + * @throws InvalidParamException if the named relation does not exist. */ public function getRelation($name) { @@ -1057,7 +1055,7 @@ class ActiveRecord extends Model } } catch (UnknownMethodException $e) { } - throw new InvalidCallException(get_class($this) . ' has no relation named "' . $name . '".'); + throw new InvalidParamException(get_class($this) . ' has no relation named "' . $name . '".'); } /** diff --git a/framework/db/ActiveRelation.php b/framework/db/ActiveRelation.php index 4d87fb3..c547f1a 100644 --- a/framework/db/ActiveRelation.php +++ b/framework/db/ActiveRelation.php @@ -1,10 +1,8 @@ * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ @@ -57,16 +55,16 @@ class ActiveRelation extends ActiveQuery /** * Specifies the relation associated with the pivot table. * @param string $relationName the relation name. This refers to a relation declared in [[primaryModel]]. - * @param callback $callback a PHP callback for customizing the relation associated with the pivot table. + * @param callable $callable a PHP callback for customizing the relation associated with the pivot table. * Its signature should be `function($query)`, where `$query` is the query to be customized. * @return ActiveRelation the relation object itself. */ - public function via($relationName, $callback = null) + public function via($relationName, $callable = null) { $relation = $this->primaryModel->getRelation($relationName); $this->via = array($relationName, $relation); - if ($callback !== null) { - call_user_func($callback, $relation); + if ($callable !== null) { + call_user_func($callable, $relation); } return $this; } @@ -77,11 +75,11 @@ class ActiveRelation extends ActiveQuery * @param array $link the link between the pivot table and the table associated with [[primaryModel]]. * The keys of the array represent the columns in the pivot table, and the values represent the columns * in the [[primaryModel]] table. - * @param callback $callback a PHP callback for customizing the relation associated with the pivot table. + * @param callable $callable a PHP callback for customizing the relation associated with the pivot table. * Its signature should be `function($query)`, where `$query` is the query to be customized. * @return ActiveRelation */ - public function viaTable($tableName, $link, $callback = null) + public function viaTable($tableName, $link, $callable = null) { $relation = new ActiveRelation(array( 'modelClass' => get_class($this->primaryModel), @@ -91,8 +89,8 @@ class ActiveRelation extends ActiveQuery 'asArray' => true, )); $this->via = $relation; - if ($callback !== null) { - call_user_func($callback, $relation); + if ($callable !== null) { + call_user_func($callable, $relation); } return $this; } diff --git a/framework/db/ColumnSchema.php b/framework/db/ColumnSchema.php index 44e6cb0..ffdafd4 100644 --- a/framework/db/ColumnSchema.php +++ b/framework/db/ColumnSchema.php @@ -1,9 +1,7 @@ pdoStatement = $this->db->pdo->prepare($sql); } catch (\Exception $e) { - \Yii::error($e->getMessage() . "\nFailed to prepare SQL: $sql", __CLASS__); + Yii::error($e->getMessage() . "\nFailed to prepare SQL: $sql", __CLASS__); $errorInfo = $e instanceof \PDOException ? $e->errorInfo : null; - throw new Exception($e->getMessage(), (int)$e->getCode(), $errorInfo); + throw new Exception($e->getMessage(), $errorInfo, (int)$e->getCode()); } } } @@ -253,24 +253,20 @@ class Command extends \yii\base\Component * Executes the SQL statement. * This method should only be used for executing non-query SQL statement, such as `INSERT`, `DELETE`, `UPDATE` SQLs. * No result set will be returned. - * @param array $params input parameters (name=>value) for the SQL execution. This is an alternative - * to [[bindValues()]]. Note that if you pass parameters in this way, any previous call to [[bindParam()]] - * or [[bindValue()]] will be ignored. * @return integer number of rows affected by the execution. * @throws Exception execution failed */ - public function execute($params = array()) + public function execute() { $sql = $this->getSql(); - $this->_params = array_merge($this->_params, $params); if ($this->_params === array()) { $paramLog = ''; } else { $paramLog = "\nParameters: " . var_export($this->_params, true); } - \Yii::trace("Executing SQL: {$sql}{$paramLog}", __CLASS__); + Yii::trace("Executing SQL: {$sql}{$paramLog}", __CLASS__); if ($sql == '') { return 0; @@ -278,94 +274,78 @@ class Command extends \yii\base\Component try { if ($this->db->enableProfiling) { - \Yii::beginProfile(__METHOD__ . "($sql)", __CLASS__); + Yii::beginProfile(__METHOD__ . "($sql)", __CLASS__); } $this->prepare(); - if ($params === array()) { - $this->pdoStatement->execute(); - } else { - $this->pdoStatement->execute($params); - } + $this->pdoStatement->execute(); $n = $this->pdoStatement->rowCount(); if ($this->db->enableProfiling) { - \Yii::endProfile(__METHOD__ . "($sql)", __CLASS__); + Yii::endProfile(__METHOD__ . "($sql)", __CLASS__); } return $n; } catch (\Exception $e) { if ($this->db->enableProfiling) { - \Yii::endProfile(__METHOD__ . "($sql)", __CLASS__); + Yii::endProfile(__METHOD__ . "($sql)", __CLASS__); } $message = $e->getMessage(); - \Yii::error("$message\nFailed to execute SQL: {$sql}{$paramLog}", __CLASS__); + Yii::error("$message\nFailed to execute SQL: {$sql}{$paramLog}", __CLASS__); $errorInfo = $e instanceof \PDOException ? $e->errorInfo : null; - throw new Exception($message, (int)$e->getCode(), $errorInfo); + throw new Exception($message, $errorInfo, (int)$e->getCode()); } } /** * Executes the SQL statement and returns query result. * This method is for executing a SQL query that returns result set, such as `SELECT`. - * @param array $params input parameters (name=>value) for the SQL execution. This is an alternative - * to [[bindValues()]]. Note that if you pass parameters in this way, any previous call to [[bindParam()]] - * or [[bindValue()]] will be ignored. * @return DataReader the reader object for fetching the query result * @throws Exception execution failed */ - public function query($params = array()) + public function query() { - return $this->queryInternal('', $params); + return $this->queryInternal(''); } /** * Executes the SQL statement and returns ALL rows at once. - * @param array $params input parameters (name=>value) for the SQL execution. This is an alternative - * to [[bindValues()]]. Note that if you pass parameters in this way, any previous call to [[bindParam()]] - * or [[bindValue()]] will be ignored. * @param mixed $fetchMode the result fetch mode. Please refer to [PHP manual](http://www.php.net/manual/en/function.PDOStatement-setFetchMode.php) * for valid fetch modes. If this parameter is null, the value set in [[fetchMode]] will be used. * @return array all rows of the query result. Each array element is an array representing a row of data. * An empty array is returned if the query results in nothing. * @throws Exception execution failed */ - public function queryAll($params = array(), $fetchMode = null) + public function queryAll($fetchMode = null) { - return $this->queryInternal('fetchAll', $params, $fetchMode); + return $this->queryInternal('fetchAll', $fetchMode); } /** * Executes the SQL statement and returns the first row of the result. * This method is best used when only the first row of result is needed for a query. - * @param array $params input parameters (name=>value) for the SQL execution. This is an alternative - * to [[bindValues()]]. Note that if you pass parameters in this way, any previous call to [[bindParam()]] - * or [[bindValue()]] will be ignored. * @param mixed $fetchMode the result fetch mode. Please refer to [PHP manual](http://www.php.net/manual/en/function.PDOStatement-setFetchMode.php) * for valid fetch modes. If this parameter is null, the value set in [[fetchMode]] will be used. * @return array|boolean the first row (in terms of an array) of the query result. False is returned if the query * results in nothing. * @throws Exception execution failed */ - public function queryRow($params = array(), $fetchMode = null) + public function queryRow($fetchMode = null) { - return $this->queryInternal('fetch', $params, $fetchMode); + return $this->queryInternal('fetch', $fetchMode); } /** * Executes the SQL statement and returns the value of the first column in the first row of data. * This method is best used when only a single value is needed for a query. - * @param array $params input parameters (name=>value) for the SQL execution. This is an alternative - * to [[bindValues()]]. Note that if you pass parameters in this way, any previous call to [[bindParam()]] - * or [[bindValue()]] will be ignored. * @return string|boolean the value of the first column in the first row of the query result. * False is returned if there is no value. * @throws Exception execution failed */ - public function queryScalar($params = array()) + public function queryScalar() { - $result = $this->queryInternal('fetchColumn', $params, 0); + $result = $this->queryInternal('fetchColumn', 0); if (is_resource($result) && get_resource_type($result) === 'stream') { return stream_get_contents($result); } else { @@ -377,65 +357,60 @@ class Command extends \yii\base\Component * Executes the SQL statement and returns the first column of the result. * This method is best used when only the first column of result (i.e. the first element in each row) * is needed for a query. - * @param array $params input parameters (name=>value) for the SQL execution. This is an alternative - * to [[bindValues()]]. Note that if you pass parameters in this way, any previous call to [[bindParam()]] - * or [[bindValue()]] will be ignored. * @return array the first column of the query result. Empty array is returned if the query results in nothing. * @throws Exception execution failed */ - public function queryColumn($params = array()) + public function queryColumn() { - return $this->queryInternal('fetchAll', $params, \PDO::FETCH_COLUMN); + return $this->queryInternal('fetchAll', \PDO::FETCH_COLUMN); } /** * Performs the actual DB query of a SQL statement. * @param string $method method of PDOStatement to be called - * @param array $params input parameters (name=>value) for the SQL execution. This is an alternative - * to [[bindValues()]]. Note that if you pass parameters in this way, any previous call to [[bindParam()]] - * or [[bindValue()]] will be ignored. * @param mixed $fetchMode the result fetch mode. Please refer to [PHP manual](http://www.php.net/manual/en/function.PDOStatement-setFetchMode.php) * for valid fetch modes. If this parameter is null, the value set in [[fetchMode]] will be used. * @return mixed the method execution result * @throws Exception if the query causes any problem */ - private function queryInternal($method, $params, $fetchMode = null) + private function queryInternal($method, $fetchMode = null) { $db = $this->db; $sql = $this->getSql(); - $this->_params = array_merge($this->_params, $params); if ($this->_params === array()) { $paramLog = ''; } else { $paramLog = "\nParameters: " . var_export($this->_params, true); } - \Yii::trace("Querying SQL: {$sql}{$paramLog}", __CLASS__); + Yii::trace("Querying SQL: {$sql}{$paramLog}", __CLASS__); /** @var $cache \yii\caching\Cache */ if ($db->enableQueryCache && $method !== '') { - $cache = \Yii::$application->getComponent($db->queryCacheID); + $cache = is_string($db->queryCache) ? Yii::$app->getComponent($db->queryCache) : $db->queryCache; } - if (isset($cache)) { - $cacheKey = $cache->buildKey(__CLASS__, $db->dsn, $db->username, $sql, $paramLog); + if (isset($cache) && $cache instanceof Cache) { + $cacheKey = $cache->buildKey(array( + __CLASS__, + $db->dsn, + $db->username, + $sql, + $paramLog, + )); if (($result = $cache->get($cacheKey)) !== false) { - \Yii::trace('Query result found in cache', __CLASS__); + Yii::trace('Query result served from cache', __CLASS__); return $result; } } try { if ($db->enableProfiling) { - \Yii::beginProfile(__METHOD__ . "($sql)", __CLASS__); + Yii::beginProfile(__METHOD__ . "($sql)", __CLASS__); } $this->prepare(); - if ($params === array()) { - $this->pdoStatement->execute(); - } else { - $this->pdoStatement->execute($params); - } + $this->pdoStatement->execute(); if ($method === '') { $result = new DataReader($this); @@ -448,23 +423,23 @@ class Command extends \yii\base\Component } if ($db->enableProfiling) { - \Yii::endProfile(__METHOD__ . "($sql)", __CLASS__); + Yii::endProfile(__METHOD__ . "($sql)", __CLASS__); } - if (isset($cache, $cacheKey)) { + if (isset($cache, $cacheKey) && $cache instanceof Cache) { $cache->set($cacheKey, $result, $db->queryCacheDuration, $db->queryCacheDependency); - \Yii::trace('Saved query result in cache', __CLASS__); + Yii::trace('Saved query result in cache', __CLASS__); } return $result; } catch (\Exception $e) { if ($db->enableProfiling) { - \Yii::endProfile(__METHOD__ . "($sql)", __CLASS__); + Yii::endProfile(__METHOD__ . "($sql)", __CLASS__); } $message = $e->getMessage(); - \Yii::error("$message\nCommand::$method() failed: {$sql}{$paramLog}", __CLASS__); + Yii::error("$message\nCommand::$method() failed: {$sql}{$paramLog}", __CLASS__); $errorInfo = $e instanceof \PDOException ? $e->errorInfo : null; - throw new Exception($message, (int)$e->getCode(), $errorInfo); + throw new Exception($message, $errorInfo, (int)$e->getCode()); } } diff --git a/framework/db/Connection.php b/framework/db/Connection.php index 3564361..59e8422 100644 --- a/framework/db/Connection.php +++ b/framework/db/Connection.php @@ -1,9 +1,7 @@ pdo === null) { if (empty($this->dsn)) { - throw new InvalidConfigException('Connection.dsn cannot be empty.'); + throw new InvalidConfigException('Connection::dsn cannot be empty.'); } try { \Yii::trace('Opening DB connection: ' . $this->dsn, __CLASS__); @@ -332,7 +331,7 @@ class Connection extends Component catch (\PDOException $e) { \Yii::error("Failed to open DB connection ({$this->dsn}): " . $e->getMessage(), __CLASS__); $message = YII_DEBUG ? 'Failed to open DB connection: ' . $e->getMessage() : 'Failed to open DB connection.'; - throw new Exception($message, (int)$e->getCode(), $e->errorInfo); + throw new Exception($message, $e->errorInfo, (int)$e->getCode()); } } } diff --git a/framework/db/DataReader.php b/framework/db/DataReader.php index 8e5291e..20444e7 100644 --- a/framework/db/DataReader.php +++ b/framework/db/DataReader.php @@ -1,9 +1,7 @@ errorInfo = $errorInfo; - parent::__construct($message, $code); + parent::__construct($message, $code, $previous); } /** @@ -40,6 +39,6 @@ class Exception extends \yii\base\Exception */ public function getName() { - return \Yii::t('yii', 'Database Exception'); + return \Yii::t('yii|Database Exception'); } } \ No newline at end of file diff --git a/framework/db/Expression.php b/framework/db/Expression.php index 23fb13e..4ebcd5f 100644 --- a/framework/db/Expression.php +++ b/framework/db/Expression.php @@ -1,9 +1,7 @@ db === null) { - $this->db = \Yii::$application->getComponent('db'); + $this->db = \Yii::$app->getComponent('db'); } } diff --git a/framework/db/Query.php b/framework/db/Query.php index 10bba08..2239f5d 100644 --- a/framework/db/Query.php +++ b/framework/db/Query.php @@ -1,9 +1,7 @@ db; + $db = \Yii::$app->db; } $sql = $db->getQueryBuilder()->build($this); return $db->createCommand($sql, $this->params); @@ -136,6 +156,9 @@ class Query extends \yii\base\Component */ public function select($columns, $option = null) { + if (!is_array($columns)) { + $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY); + } $this->select = $columns; $this->selectOption = $option; return $this; @@ -163,6 +186,9 @@ class Query extends \yii\base\Component */ public function from($tables) { + if (!is_array($tables)) { + $tables = preg_split('/\s*,\s*/', trim($tables), -1, PREG_SPLIT_NO_EMPTY); + } $this->from = $tables; return $this; } @@ -362,10 +388,13 @@ class Query extends \yii\base\Component * The method will automatically quote the column names unless a column contains some parenthesis * (which means the column contains a DB expression). * @return Query the query object itself - * @see addGroup() + * @see addGroupBy() */ public function groupBy($columns) { + if (!is_array($columns)) { + $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY); + } $this->groupBy = $columns; return $this; } @@ -377,19 +406,16 @@ class Query extends \yii\base\Component * The method will automatically quote the column names unless a column contains some parenthesis * (which means the column contains a DB expression). * @return Query the query object itself - * @see group() + * @see groupBy() */ - public function addGroup($columns) + public function addGroupBy($columns) { - if (empty($this->groupBy)) { + if (!is_array($columns)) { + $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY); + } + if ($this->groupBy === null) { $this->groupBy = $columns; } else { - if (!is_array($this->groupBy)) { - $this->groupBy = preg_split('/\s*,\s*/', trim($this->groupBy), -1, PREG_SPLIT_NO_EMPTY); - } - if (!is_array($columns)) { - $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY); - } $this->groupBy = array_merge($this->groupBy, $columns); } return $this; @@ -456,43 +482,58 @@ class Query extends \yii\base\Component /** * Sets the ORDER BY part of the query. * @param string|array $columns the columns (and the directions) to be ordered by. - * Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array (e.g. array('id ASC', 'name DESC')). + * Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array + * (e.g. `array('id' => Query::SORT_ASC ASC, 'name' => Query::SORT_DESC)`). * The method will automatically quote the column names unless a column contains some parenthesis * (which means the column contains a DB expression). * @return Query the query object itself - * @see addOrder() + * @see addOrderBy() */ public function orderBy($columns) { - $this->orderBy = $columns; + $this->orderBy = $this->normalizeOrderBy($columns); return $this; } /** * Adds additional ORDER BY columns to the query. * @param string|array $columns the columns (and the directions) to be ordered by. - * Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array (e.g. array('id ASC', 'name DESC')). + * Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array + * (e.g. `array('id' => Query::SORT_ASC ASC, 'name' => Query::SORT_DESC)`). * The method will automatically quote the column names unless a column contains some parenthesis * (which means the column contains a DB expression). * @return Query the query object itself - * @see order() + * @see orderBy() */ public function addOrderBy($columns) { - if (empty($this->orderBy)) { + $columns = $this->normalizeOrderBy($columns); + if ($this->orderBy === null) { $this->orderBy = $columns; } else { - if (!is_array($this->orderBy)) { - $this->orderBy = preg_split('/\s*,\s*/', trim($this->orderBy), -1, PREG_SPLIT_NO_EMPTY); - } - if (!is_array($columns)) { - $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY); - } $this->orderBy = array_merge($this->orderBy, $columns); } return $this; } + protected function normalizeOrderBy($columns) + { + if (is_array($columns)) { + return $columns; + } else { + $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY); + $result = array(); + foreach ($columns as $column) { + if (preg_match('/^(.*?)\s+(asc|desc)$/i', $column, $matches)) { + $result[$matches[1]] = strcasecmp($matches[2], 'desc') ? self::SORT_ASC : self::SORT_DESC; + } else { + $result[$column] = self::SORT_ASC; + } + } + return $result; + } + } + /** * Sets the LIMIT part of the query. * @param integer $limit the limit diff --git a/framework/db/QueryBuilder.php b/framework/db/QueryBuilder.php index 35bfcb3..75375cc 100644 --- a/framework/db/QueryBuilder.php +++ b/framework/db/QueryBuilder.php @@ -1,9 +1,7 @@ buildFrom($query->from), $this->buildJoin($query->join), $this->buildWhere($query->where), - $this->buildGroup($query->groupBy), + $this->buildGroupBy($query->groupBy), $this->buildHaving($query->having), $this->buildUnion($query->union), - $this->buildOrder($query->orderBy), + $this->buildOrderBy($query->orderBy), $this->buildLimit($query->limit, $query->offset), ); return implode($this->separator, array_filter($clauses)); @@ -131,11 +129,10 @@ class QueryBuilder extends \yii\base\Object * @param string $table the table that new rows will be inserted into. * @param array $columns the column names * @param array $rows the rows to be batch inserted into the table - * @param array $params the parameters to be bound to the command * @return string the batch INSERT SQL statement * @throws NotSupportedException if this is not supported by the underlying DBMS */ - public function batchInsert($table, $columns, $rows, $params = array()) + public function batchInsert($table, $columns, $rows) { throw new NotSupportedException($this->db->getDriverName() . ' does not support batch insert.'); @@ -593,21 +590,19 @@ class QueryBuilder extends \yii\base\Object return $operator === 'IN' ? '0=1' : ''; } - if (is_array($column)) { - if (count($column) > 1) { - return $this->buildCompositeInCondition($operator, $column, $values); + if (count($column) > 1) { + return $this->buildCompositeInCondition($operator, $column, $values); + } elseif (is_array($column)) { + $column = reset($column); + } + foreach ($values as $i => $value) { + if (is_array($value)) { + $value = isset($value[$column]) ? $value[$column] : null; + } + if ($value === null) { + $values[$i] = 'NULL'; } else { - $column = reset($column); - foreach ($values as $i => $value) { - if (is_array($value)) { - $value = isset($value[$column]) ? $value[$column] : null; - } - if ($value === null) { - $values[$i] = 'NULL'; - } else { - $values[$i] = is_string($value) ? $this->db->quoteValue($value) : (string)$value; - } - } + $values[$i] = is_string($value) ? $this->db->quoteValue($value) : (string)$value; } } if (strpos($column, '(') === false) { @@ -678,7 +673,7 @@ class QueryBuilder extends \yii\base\Object } /** - * @param string|array $columns + * @param array $columns * @param boolean $distinct * @param string $selectOption * @return string the SELECT clause built from [[query]]. @@ -694,13 +689,6 @@ class QueryBuilder extends \yii\base\Object return $select . ' *'; } - if (!is_array($columns)) { - if (strpos($columns, '(') !== false) { - return $select . ' ' . $columns; - } else { - $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY); - } - } foreach ($columns as $i => $column) { if (is_object($column)) { $columns[$i] = (string)$column; @@ -721,7 +709,7 @@ class QueryBuilder extends \yii\base\Object } /** - * @param string|array $tables + * @param array $tables * @return string the FROM clause built from [[query]]. */ public function buildFrom($tables) @@ -730,13 +718,6 @@ class QueryBuilder extends \yii\base\Object return ''; } - if (!is_array($tables)) { - if (strpos($tables, '(') !== false) { - return 'FROM ' . $tables; - } else { - $tables = preg_split('/\s*,\s*/', trim($tables), -1, PREG_SPLIT_NO_EMPTY); - } - } foreach ($tables as $i => $table) { if (strpos($table, '(') === false) { if (preg_match('/^(.*?)(?i:\s+as\s+|\s+)(.*)$/i', $table, $matches)) { // with alias @@ -757,37 +738,36 @@ class QueryBuilder extends \yii\base\Object /** * @param string|array $joins * @return string the JOIN clause built from [[query]]. + * @throws Exception if the $joins parameter is not in proper format */ public function buildJoin($joins) { if (empty($joins)) { return ''; } - if (is_string($joins)) { - return $joins; - } foreach ($joins as $i => $join) { - if (is_array($join)) { // 0:join type, 1:table name, 2:on-condition - if (isset($join[0], $join[1])) { - $table = $join[1]; - if (strpos($table, '(') === false) { - if (preg_match('/^(.*?)(?i:\s+as\s+|\s+)(.*)$/', $table, $matches)) { // with alias - $table = $this->db->quoteTableName($matches[1]) . ' ' . $this->db->quoteTableName($matches[2]); - } else { - $table = $this->db->quoteTableName($table); - } + if (is_object($join)) { + $joins[$i] = (string)$join; + } elseif (is_array($join) && isset($join[0], $join[1])) { + // 0:join type, 1:table name, 2:on-condition + $table = $join[1]; + if (strpos($table, '(') === false) { + if (preg_match('/^(.*?)(?i:\s+as\s+|\s+)(.*)$/', $table, $matches)) { // with alias + $table = $this->db->quoteTableName($matches[1]) . ' ' . $this->db->quoteTableName($matches[2]); + } else { + $table = $this->db->quoteTableName($table); } - $joins[$i] = $join[0] . ' ' . $table; - if (isset($join[2])) { - $condition = $this->buildCondition($join[2]); - if ($condition !== '') { - $joins[$i] .= ' ON ' . $this->buildCondition($join[2]); - } + } + $joins[$i] = $join[0] . ' ' . $table; + if (isset($join[2])) { + $condition = $this->buildCondition($join[2]); + if ($condition !== '') { + $joins[$i] .= ' ON ' . $this->buildCondition($join[2]); } - } else { - throw new Exception('A join clause must be specified as an array of at least two elements.'); } + } else { + throw new Exception('A join clause must be specified as an array of join type, join table, and optionally join condition.'); } } @@ -805,16 +785,12 @@ class QueryBuilder extends \yii\base\Object } /** - * @param string|array $columns + * @param array $columns * @return string the GROUP BY clause */ - public function buildGroup($columns) + public function buildGroupBy($columns) { - if (empty($columns)) { - return ''; - } else { - return 'GROUP BY ' . $this->buildColumns($columns); - } + return empty($columns) ? '' : 'GROUP BY ' . $this->buildColumns($columns); } /** @@ -828,36 +804,24 @@ class QueryBuilder extends \yii\base\Object } /** - * @param string|array $columns + * @param array $columns * @return string the ORDER BY clause built from [[query]]. */ - public function buildOrder($columns) + public function buildOrderBy($columns) { if (empty($columns)) { return ''; } - if (!is_array($columns)) { - if (strpos($columns, '(') !== false) { - return 'ORDER BY ' . $columns; + $orders = array(); + foreach ($columns as $name => $direction) { + if (is_object($direction)) { + $orders[] = (string)$direction; } else { - $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY); - } - } - foreach ($columns as $i => $column) { - if (is_object($column)) { - $columns[$i] = (string)$column; - } elseif (strpos($column, '(') === false) { - if (preg_match('/^(.*?)\s+(asc|desc)$/i', $column, $matches)) { - $columns[$i] = $this->db->quoteColumnName($matches[1]) . ' ' . $matches[2]; - } else { - $columns[$i] = $this->db->quoteColumnName($column); - } + $orders[] = $this->db->quoteColumnName($name) . ($direction === Query::SORT_DESC ? ' DESC' : ''); } } - if (is_array($columns)) { - $columns = implode(', ', $columns); - } - return 'ORDER BY ' . $columns; + + return 'ORDER BY ' . implode(', ', $orders); } /** @@ -878,7 +842,7 @@ class QueryBuilder extends \yii\base\Object } /** - * @param string|array $unions + * @param array $unions * @return string the UNION clause built from [[query]]. */ public function buildUnion($unions) @@ -886,9 +850,6 @@ class QueryBuilder extends \yii\base\Object if (empty($unions)) { return ''; } - if (!is_array($unions)) { - $unions = array($unions); - } foreach ($unions as $i => $union) { if ($union instanceof Query) { $unions[$i] = $this->build($union); diff --git a/framework/db/Schema.php b/framework/db/Schema.php index 7415bee..71bc9a2 100644 --- a/framework/db/Schema.php +++ b/framework/db/Schema.php @@ -1,14 +1,13 @@ db; $realName = $this->getRealTableName($name); - /** @var $cache Cache */ - if ($db->enableSchemaCache && ($cache = \Yii::$application->getComponent($db->schemaCacheID)) !== null && !in_array($name, $db->schemaCacheExclude, true)) { - $key = $this->getCacheKey($cache, $name); - if ($refresh || ($table = $cache->get($key)) === false) { - $table = $this->loadTableSchema($realName); - if ($table !== null) { - $cache->set($key, $table, $db->schemaCacheDuration); + if ($db->enableSchemaCache && !in_array($name, $db->schemaCacheExclude, true)) { + /** @var $cache Cache */ + $cache = is_string($db->schemaCache) ? Yii::$app->getComponent($db->schemaCache) : $db->schemaCache; + if ($cache instanceof Cache) { + $key = $this->getCacheKey($cache, $name); + if ($refresh || ($table = $cache->get($key)) === false) { + $table = $this->loadTableSchema($realName); + if ($table !== null) { + $cache->set($key, $table, $db->schemaCacheDuration); + } } + return $this->_tables[$name] = $table; } - $this->_tables[$name] = $table; - } else { - $this->_tables[$name] = $table = $this->loadTableSchema($realName); } - - return $table; + return $this->_tables[$name] = $table = $this->loadTableSchema($realName); } /** @@ -111,7 +110,12 @@ abstract class Schema extends \yii\base\Object */ public function getCacheKey($cache, $name) { - return $cache->buildKey(__CLASS__, $this->db->dsn, $this->db->username, $name); + return $cache->buildKey(array( + __CLASS__, + $this->db->dsn, + $this->db->username, + $name, + )); } /** @@ -170,8 +174,9 @@ abstract class Schema extends \yii\base\Object */ public function refresh() { - /** @var $cache \yii\caching\Cache */ - if ($this->db->enableSchemaCache && ($cache = \Yii::$application->getComponent($this->db->schemaCacheID)) !== null) { + /** @var $cache Cache */ + $cache = is_string($this->db->schemaCache) ? Yii::$app->getComponent($this->db->schemaCache) : $this->db->schemaCache; + if ($this->db->enableSchemaCache && $cache instanceof Cache) { foreach ($this->_tables as $name => $table) { $cache->delete($this->getCacheKey($cache, $name)); } diff --git a/framework/db/TableSchema.php b/framework/db/TableSchema.php index 987d221..1065b51 100644 --- a/framework/db/TableSchema.php +++ b/framework/db/TableSchema.php @@ -1,7 +1,5 @@ columns[$key])) { $this->columns[$key]->isPrimaryKey = true; } else { - throw new InvalidCallException("Primary key '$key' cannot be found in table '{$this->name}'."); + throw new InvalidParamException("Primary key '$key' cannot be found in table '{$this->name}'."); } } } diff --git a/framework/db/Transaction.php b/framework/db/Transaction.php index 3e53c0c..177d2cb 100644 --- a/framework/db/Transaction.php +++ b/framework/db/Transaction.php @@ -1,9 +1,7 @@ db->quoteTableName($table); $row = $this->db->createCommand('SHOW CREATE TABLE ' . $quotedTable)->queryRow(); if ($row === false) { - throw new Exception("Unable to find '$oldName' in table '$table'."); + throw new Exception("Unable to find column '$oldName' in table '$table'."); } if (isset($row['Create Table'])) { $sql = $row['Create Table']; @@ -98,7 +96,7 @@ class QueryBuilder extends \yii\db\QueryBuilder * @param mixed $value the value for the primary key of the next new row inserted. If this is not set, * the next new row's primary key will have a value 1. * @return string the SQL statement for resetting sequence - * @throws InvalidCallException if the table does not exist or there is no sequence associated with the table. + * @throws InvalidParamException if the table does not exist or there is no sequence associated with the table. */ public function resetSequence($tableName, $value = null) { @@ -113,9 +111,9 @@ class QueryBuilder extends \yii\db\QueryBuilder } return "ALTER TABLE $tableName AUTO_INCREMENT=$value"; } elseif ($table === null) { - throw new InvalidCallException("Table not found: $tableName"); + throw new InvalidParamException("Table not found: $tableName"); } else { - throw new InvalidCallException("There is not sequence associated with table '$tableName'.'"); + throw new InvalidParamException("There is not sequence associated with table '$tableName'.'"); } } diff --git a/framework/db/mysql/Schema.php b/framework/db/mysql/Schema.php index 32df0b3..501149a 100644 --- a/framework/db/mysql/Schema.php +++ b/framework/db/mysql/Schema.php @@ -1,9 +1,7 @@ firstName . ' ' . $user->lastName; * }); * ~~~ @@ -242,7 +241,7 @@ class ArrayHelper * value is for sorting strings in case-insensitive manner. Please refer to * See [PHP manual](http://php.net/manual/en/function.sort.php) for more details. * When sorting by multiple keys with different sort flags, use an array of sort flags. - * @throws InvalidCallException if the $ascending or $sortFlag parameters do not have + * @throws InvalidParamException if the $ascending or $sortFlag parameters do not have * correct number of elements as that of $key. */ public static function multisort(&$array, $key, $ascending = true, $sortFlag = SORT_REGULAR) @@ -255,12 +254,12 @@ class ArrayHelper if (is_scalar($ascending)) { $ascending = array_fill(0, $n, $ascending); } elseif (count($ascending) !== $n) { - throw new InvalidCallException('The length of $ascending parameter must be the same as that of $keys.'); + throw new InvalidParamException('The length of $ascending parameter must be the same as that of $keys.'); } if (is_scalar($sortFlag)) { $sortFlag = array_fill(0, $n, $sortFlag); } elseif (count($sortFlag) !== $n) { - throw new InvalidCallException('The length of $ascending parameter must be the same as that of $keys.'); + throw new InvalidParamException('The length of $ascending parameter must be the same as that of $keys.'); } $args = array(); foreach ($keys as $i => $key) { @@ -281,4 +280,61 @@ class ArrayHelper $args[] = &$array; call_user_func_array('array_multisort', $args); } + + /** + * Encodes special characters in an array of strings into HTML entities. + * Both the array keys and values will be encoded. + * If a value is an array, this method will also encode it recursively. + * @param array $data data to be encoded + * @param boolean $valuesOnly whether to encode array values only. If false, + * both the array keys and array values will be encoded. + * @param string $charset the charset that the data is using. If not set, + * [[\yii\base\Application::charset]] will be used. + * @return array the encoded data + * @see http://www.php.net/manual/en/function.htmlspecialchars.php + */ + public static function htmlEncode($data, $valuesOnly = true, $charset = null) + { + if ($charset === null) { + $charset = Yii::$app->charset; + } + $d = array(); + foreach ($data as $key => $value) { + if (!$valuesOnly && is_string($key)) { + $key = htmlspecialchars($key, ENT_QUOTES, $charset); + } + if (is_string($value)) { + $d[$key] = htmlspecialchars($value, ENT_QUOTES, $charset); + } elseif (is_array($value)) { + $d[$key] = static::htmlEncode($value, $charset); + } + } + return $d; + } + + /** + * Decodes HTML entities into the corresponding characters in an array of strings. + * Both the array keys and values will be decoded. + * If a value is an array, this method will also decode it recursively. + * @param array $data data to be decoded + * @param boolean $valuesOnly whether to decode array values only. If false, + * both the array keys and array values will be decoded. + * @return array the decoded data + * @see http://www.php.net/manual/en/function.htmlspecialchars-decode.php + */ + public static function htmlDecode($data, $valuesOnly = true) + { + $d = array(); + foreach ($data as $key => $value) { + if (!$valuesOnly && is_string($key)) { + $key = htmlspecialchars_decode($key, ENT_QUOTES); + } + if (is_string($value)) { + $d[$key] = htmlspecialchars_decode($value, ENT_QUOTES); + } elseif (is_array($value)) { + $d[$key] = static::htmlDecode($value); + } + } + return $d; + } } \ No newline at end of file diff --git a/framework/util/ConsoleColor.php b/framework/helpers/ConsoleColor.php similarity index 97% rename from framework/util/ConsoleColor.php rename to framework/helpers/ConsoleColor.php index 1fadc40..429aeb1 100644 --- a/framework/util/ConsoleColor.php +++ b/framework/helpers/ConsoleColor.php @@ -1,23 +1,13 @@ $content) { - if ($name = 'text-decoration') { + if ($name === 'text-decoration') { $content = implode(' ', $content); } $styleString[] = $name.':'.$content; diff --git a/framework/util/FileHelper.php b/framework/helpers/FileHelper.php similarity index 97% rename from framework/util/FileHelper.php rename to framework/helpers/FileHelper.php index c65e4f0..f850b98 100644 --- a/framework/util/FileHelper.php +++ b/framework/helpers/FileHelper.php @@ -3,11 +3,11 @@ * Filesystem helper class file. * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ -namespace yii\util; +namespace yii\helpers; use yii\base\Exception; use yii\base\InvalidConfigException; @@ -43,7 +43,7 @@ class FileHelper public static function ensureDirectory($path) { $p = \Yii::getAlias($path); - if ($p !== false && ($p = realpath($p)) !== false && is_dir($p)) { + if (($p = realpath($p)) !== false && is_dir($p)) { return $p; } else { throw new InvalidConfigException('Directory does not exist: ' . $path); @@ -91,10 +91,10 @@ class FileHelper public static function localize($file, $language = null, $sourceLanguage = null) { if ($language === null) { - $language = \Yii::$application->getLanguage(); + $language = \Yii::$app->language; } if ($sourceLanguage === null) { - $sourceLanguage = \Yii::$application->sourceLanguage; + $sourceLanguage = \Yii::$app->sourceLanguage; } if ($language === $sourceLanguage) { return $file; diff --git a/framework/helpers/Html.php b/framework/helpers/Html.php new file mode 100644 index 0000000..b2ca576 --- /dev/null +++ b/framework/helpers/Html.php @@ -0,0 +1,981 @@ + + * @since 2.0 + */ +class Html +{ + /** + * @var boolean whether to close void (empty) elements. Defaults to true. + * @see voidElements + */ + public static $closeVoidElements = true; + /** + * @var array list of void elements (element name => 1) + * @see closeVoidElements + * @see http://www.w3.org/TR/html-markup/syntax.html#void-element + */ + public static $voidElements = array( + 'area' => 1, + 'base' => 1, + 'br' => 1, + 'col' => 1, + 'command' => 1, + 'embed' => 1, + 'hr' => 1, + 'img' => 1, + 'input' => 1, + 'keygen' => 1, + 'link' => 1, + 'meta' => 1, + 'param' => 1, + 'source' => 1, + 'track' => 1, + 'wbr' => 1, + ); + /** + * @var boolean whether to show the values of boolean attributes in element tags. + * If false, only the attribute names will be generated. + * @see booleanAttributes + */ + public static $showBooleanAttributeValues = true; + /** + * @var array list of boolean attributes. The presence of a boolean attribute on + * an element represents the true value, and the absence of the attribute represents the false value. + * @see showBooleanAttributeValues + * @see http://www.w3.org/TR/html5/infrastructure.html#boolean-attributes + */ + public static $booleanAttributes = array( + 'async' => 1, + 'autofocus' => 1, + 'autoplay' => 1, + 'checked' => 1, + 'controls' => 1, + 'declare' => 1, + 'default' => 1, + 'defer' => 1, + 'disabled' => 1, + 'formnovalidate' => 1, + 'hidden' => 1, + 'ismap' => 1, + 'loop' => 1, + 'multiple' => 1, + 'muted' => 1, + 'nohref' => 1, + 'noresize' => 1, + 'novalidate' => 1, + 'open' => 1, + 'readonly' => 1, + 'required' => 1, + 'reversed' => 1, + 'scoped' => 1, + 'seamless' => 1, + 'selected' => 1, + 'typemustmatch' => 1, + ); + /** + * @var array the preferred order of attributes in a tag. This mainly affects the order of the attributes + * that are rendered by [[renderAttributes()]]. + */ + public static $attributeOrder = array( + 'type', + 'id', + 'class', + 'name', + 'value', + + 'href', + 'src', + 'action', + 'method', + + 'selected', + 'checked', + 'readonly', + 'disabled', + 'multiple', + + 'size', + 'maxlength', + 'width', + 'height', + 'rows', + 'cols', + + 'alt', + 'title', + 'rel', + 'media', + ); + + /** + * Encodes special characters into HTML entities. + * The [[yii\base\Application::charset|application charset]] will be used for encoding. + * @param string $content the content to be encoded + * @return string the encoded content + * @see decode + * @see http://www.php.net/manual/en/function.htmlspecialchars.php + */ + public static function encode($content) + { + return htmlspecialchars($content, ENT_QUOTES, Yii::$app->charset); + } + + /** + * Decodes special HTML entities back to the corresponding characters. + * This is the opposite of [[encode()]]. + * @param string $content the content to be decoded + * @return string the decoded content + * @see encode + * @see http://www.php.net/manual/en/function.htmlspecialchars-decode.php + */ + public static function decode($content) + { + return htmlspecialchars_decode($content, ENT_QUOTES); + } + + /** + * Generates a complete HTML tag. + * @param string $name the tag name + * @param string $content the content to be enclosed between the start and end tags. It will not be HTML-encoded. + * If this is coming from end users, you should consider [[encode()]] it to prevent XSS attacks. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated HTML tag + * @see beginTag + * @see endTag + */ + public static function tag($name, $content = '', $options = array()) + { + $html = '<' . $name . static::renderTagAttributes($options); + if (isset(static::$voidElements[strtolower($name)])) { + return $html . (static::$closeVoidElements ? ' />' : '>'); + } else { + return $html . ">$content"; + } + } + + /** + * Generates a start tag. + * @param string $name the tag name + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated start tag + * @see endTag + * @see tag + */ + public static function beginTag($name, $options = array()) + { + return '<' . $name . static::renderTagAttributes($options) . '>'; + } + + /** + * Generates an end tag. + * @param string $name the tag name + * @return string the generated end tag + * @see beginTag + * @see tag + */ + public static function endTag($name) + { + return ""; + } + + /** + * Encloses the given content within a CDATA tag. + * @param string $content the content to be enclosed within the CDATA tag + * @return string the CDATA tag with the enclosed content. + */ + public static function cdata($content) + { + return ''; + } + + /** + * Generates a style tag. + * @param string $content the style content + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * If the options does not contain "type", a "type" attribute with value "text/css" will be used. + * @return string the generated style tag + */ + public static function style($content, $options = array()) + { + if (!isset($options['type'])) { + $options['type'] = 'text/css'; + } + return static::tag('style', "/**/", $options); + } + + /** + * Generates a script tag. + * @param string $content the script content + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * If the options does not contain "type", a "type" attribute with value "text/javascript" will be rendered. + * @return string the generated script tag + */ + public static function script($content, $options = array()) + { + if (!isset($options['type'])) { + $options['type'] = 'text/javascript'; + } + return static::tag('script', "/**/", $options); + } + + /** + * Generates a link tag that refers to an external CSS file. + * @param array|string $url the URL of the external CSS file. This parameter will be processed by [[url()]]. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated link tag + * @see url + */ + public static function cssFile($url, $options = array()) + { + $options['rel'] = 'stylesheet'; + $options['type'] = 'text/css'; + $options['href'] = static::url($url); + return static::tag('link', '', $options); + } + + /** + * Generates a script tag that refers to an external JavaScript file. + * @param string $url the URL of the external JavaScript file. This parameter will be processed by [[url()]]. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated script tag + * @see url + */ + public static function jsFile($url, $options = array()) + { + $options['type'] = 'text/javascript'; + $options['src'] = static::url($url); + return static::tag('script', '', $options); + } + + /** + * Generates a form start tag. + * @param array|string $action the form action URL. This parameter will be processed by [[url()]]. + * @param string $method the form submission method, either "post" or "get" (case-insensitive) + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated form start tag. + * @see endForm + */ + public static function beginForm($action = '', $method = 'post', $options = array()) + { + $action = static::url($action); + + // query parameters in the action are ignored for GET method + // we use hidden fields to add them back + $hiddens = array(); + if (!strcasecmp($method, 'get') && ($pos = strpos($action, '?')) !== false) { + foreach (explode('&', substr($action, $pos + 1)) as $pair) { + if (($pos1 = strpos($pair, '=')) !== false) { + $hiddens[] = static::hiddenInput(urldecode(substr($pair, 0, $pos1)), urldecode(substr($pair, $pos1 + 1))); + } else { + $hiddens[] = static::hiddenInput(urldecode($pair), ''); + } + } + $action = substr($action, 0, $pos); + } + + $options['action'] = $action; + $options['method'] = $method; + $form = static::beginTag('form', $options); + if ($hiddens !== array()) { + $form .= "\n" . implode("\n", $hiddens); + } + + return $form; + } + + /** + * Generates a form end tag. + * @return string the generated tag + * @see beginForm + */ + public static function endForm() + { + return ''; + } + + /** + * Generates a hyperlink tag. + * @param string $text link body. It will NOT be HTML-encoded. Therefore you can pass in HTML code + * such as an image tag. If this is is coming from end users, you should consider [[encode()]] + * it to prevent XSS attacks. + * @param array|string|null $url the URL for the hyperlink tag. This parameter will be processed by [[url()]] + * and will be used for the "href" attribute of the tag. If this parameter is null, the "href" attribute + * will not be generated. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated hyperlink + * @see url + */ + public static function a($text, $url = null, $options = array()) + { + if ($url !== null) { + $options['href'] = static::url($url); + } + return static::tag('a', $text, $options); + } + + /** + * Generates a mailto hyperlink. + * @param string $text link body. It will NOT be HTML-encoded. Therefore you can pass in HTML code + * such as an image tag. If this is is coming from end users, you should consider [[encode()]] + * it to prevent XSS attacks. + * @param string $email email address. If this is null, the first parameter (link body) will be treated + * as the email address and used. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated mailto link + */ + public static function mailto($text, $email = null, $options = array()) + { + return static::a($text, 'mailto:' . ($email === null ? $text : $email), $options); + } + + /** + * Generates an image tag. + * @param string $src the image URL. This parameter will be processed by [[url()]]. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated image tag + */ + public static function img($src, $options = array()) + { + $options['src'] = static::url($src); + if (!isset($options['alt'])) { + $options['alt'] = ''; + } + return static::tag('img', null, $options); + } + + /** + * Generates a label tag. + * @param string $content label text. It will NOT be HTML-encoded. Therefore you can pass in HTML code + * such as an image tag. If this is is coming from end users, you should consider [[encode()]] + * it to prevent XSS attacks. + * @param string $for the ID of the HTML element that this label is associated with. + * If this is null, the "for" attribute will not be generated. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated label tag + */ + public static function label($content, $for = null, $options = array()) + { + $options['for'] = $for; + return static::tag('label', $content, $options); + } + + /** + * Generates a button tag. + * @param string $name the name attribute. If it is null, the name attribute will not be generated. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param string $content the content enclosed within the button tag. It will NOT be HTML-encoded. + * Therefore you can pass in HTML code such as an image tag. If this is is coming from end users, + * you should consider [[encode()]] it to prevent XSS attacks. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * If the options does not contain "type", a "type" attribute with value "button" will be rendered. + * @return string the generated button tag + */ + public static function button($name = null, $value = null, $content = 'Button', $options = array()) + { + $options['name'] = $name; + $options['value'] = $value; + if (!isset($options['type'])) { + $options['type'] = 'button'; + } + return static::tag('button', $content, $options); + } + + /** + * Generates a submit button tag. + * @param string $name the name attribute. If it is null, the name attribute will not be generated. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param string $content the content enclosed within the button tag. It will NOT be HTML-encoded. + * Therefore you can pass in HTML code such as an image tag. If this is is coming from end users, + * you should consider [[encode()]] it to prevent XSS attacks. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated submit button tag + */ + public static function submitButton($name = null, $value = null, $content = 'Submit', $options = array()) + { + $options['type'] = 'submit'; + return static::button($name, $value, $content, $options); + } + + /** + * Generates a reset button tag. + * @param string $name the name attribute. If it is null, the name attribute will not be generated. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param string $content the content enclosed within the button tag. It will NOT be HTML-encoded. + * Therefore you can pass in HTML code such as an image tag. If this is is coming from end users, + * you should consider [[encode()]] it to prevent XSS attacks. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated reset button tag + */ + public static function resetButton($name = null, $value = null, $content = 'Reset', $options = array()) + { + $options['type'] = 'reset'; + return static::button($name, $value, $content, $options); + } + + /** + * Generates an input type of the given type. + * @param string $type the type attribute. + * @param string $name the name attribute. If it is null, the name attribute will not be generated. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated input tag + */ + public static function input($type, $name = null, $value = null, $options = array()) + { + $options['type'] = $type; + $options['name'] = $name; + $options['value'] = $value; + return static::tag('input', null, $options); + } + + /** + * Generates an input button. + * @param string $name the name attribute. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated button tag + */ + public static function buttonInput($name, $value = 'Button', $options = array()) + { + return static::input('button', $name, $value, $options); + } + + /** + * Generates a submit input button. + * @param string $name the name attribute. If it is null, the name attribute will not be generated. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated button tag + */ + public static function submitInput($name = null, $value = 'Submit', $options = array()) + { + return static::input('submit', $name, $value, $options); + } + + /** + * Generates a reset input button. + * @param string $name the name attribute. If it is null, the name attribute will not be generated. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param array $options the attributes of the button tag. The values will be HTML-encoded using [[encode()]]. + * Attributes whose value is null will be ignored and not put in the tag returned. + * @return string the generated button tag + */ + public static function resetInput($name = null, $value = 'Reset', $options = array()) + { + return static::input('reset', $name, $value, $options); + } + + /** + * Generates a text input field. + * @param string $name the name attribute. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated button tag + */ + public static function textInput($name, $value = null, $options = array()) + { + return static::input('text', $name, $value, $options); + } + + /** + * Generates a hidden input field. + * @param string $name the name attribute. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated button tag + */ + public static function hiddenInput($name, $value = null, $options = array()) + { + return static::input('hidden', $name, $value, $options); + } + + /** + * Generates a password input field. + * @param string $name the name attribute. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated button tag + */ + public static function passwordInput($name, $value = null, $options = array()) + { + return static::input('password', $name, $value, $options); + } + + /** + * Generates a file input field. + * To use a file input field, you should set the enclosing form's "enctype" attribute to + * be "multipart/form-data". After the form is submitted, the uploaded file information + * can be obtained via $_FILES[$name] (see PHP documentation). + * @param string $name the name attribute. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated button tag + */ + public static function fileInput($name, $value = null, $options = array()) + { + return static::input('file', $name, $value, $options); + } + + /** + * Generates a text area input. + * @param string $name the input name + * @param string $value the input value. Note that it will be encoded using [[encode()]]. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated text area tag + */ + public static function textarea($name, $value = '', $options = array()) + { + $options['name'] = $name; + return static::tag('textarea', static::encode($value), $options); + } + + /** + * Generates a radio button input. + * @param string $name the name attribute. + * @param boolean $checked whether the radio button should be checked. + * @param string $value the value attribute. If it is null, the value attribute will not be rendered. + * @param array $options the tag options in terms of name-value pairs. The following options are supported: + * + * - uncheck: string, the value associated with the uncheck state of the radio button. When this attribute + * is present, a hidden input will be generated so that if the radio button is not checked and is submitted, + * the value of this attribute will still be submitted to the server via the hidden input. + * + * The rest of the options will be rendered as the attributes of the resulting tag. The values will + * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. + * + * @return string the generated radio button tag + */ + public static function radio($name, $checked = false, $value = '1', $options = array()) + { + $options['checked'] = $checked; + $options['value'] = $value; + if (isset($options['uncheck'])) { + // add a hidden field so that if the radio button is not selected, it still submits a value + $hidden = static::hiddenInput($name, $options['uncheck']); + unset($options['uncheck']); + } else { + $hidden = ''; + } + return $hidden . static::input('radio', $name, $value, $options); + } + + /** + * Generates a checkbox input. + * @param string $name the name attribute. + * @param boolean $checked whether the checkbox should be checked. + * @param string $value the value attribute. If it is null, the value attribute will not be rendered. + * @param array $options the tag options in terms of name-value pairs. The following options are supported: + * + * - uncheck: string, the value associated with the uncheck state of the checkbox. When this attribute + * is present, a hidden input will be generated so that if the checkbox is not checked and is submitted, + * the value of this attribute will still be submitted to the server via the hidden input. + * + * The rest of the options will be rendered as the attributes of the resulting tag. The values will + * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. + * + * @return string the generated checkbox tag + */ + public static function checkbox($name, $checked = false, $value = '1', $options = array()) + { + $options['checked'] = $checked; + $options['value'] = $value; + if (isset($options['uncheck'])) { + // add a hidden field so that if the checkbox is not selected, it still submits a value + $hidden = static::hiddenInput($name, $options['uncheck']); + unset($options['uncheck']); + } else { + $hidden = ''; + } + return $hidden . static::input('checkbox', $name, $value, $options); + } + + /** + * Generates a drop-down list. + * @param string $name the input name + * @param string $selection the selected value + * @param array $items the option data items. The array keys are option values, and the array values + * are the corresponding option labels. The array can also be nested (i.e. some array values are arrays too). + * For each sub-array, an option group will be generated whose label is the key associated with the sub-array. + * If you have a list of data models, you may convert them into the format described above using + * [[\yii\helpers\ArrayHelper::map()]]. + * + * Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in + * the labels will also be HTML-encoded. + * @param array $options the tag options in terms of name-value pairs. The following options are supported: + * + * - prompt: string, a prompt text to be displayed as the first option; + * - options: array, the attributes for the select option tags. The array keys must be valid option values, + * and the array values are the extra attributes for the corresponding option tags. For example, + * + * ~~~ + * array( + * 'value1' => array('disabled' => true), + * 'value2' => array('label' => 'value 2'), + * ); + * ~~~ + * + * - groups: array, the attributes for the optgroup tags. The structure of this is similar to that of 'options', + * except that the array keys represent the optgroup labels specified in $items. + * + * The rest of the options will be rendered as the attributes of the resulting tag. The values will + * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. + * + * @return string the generated drop-down list tag + */ + public static function dropDownList($name, $selection = null, $items = array(), $options = array()) + { + $options['name'] = $name; + $selectOptions = static::renderSelectOptions($selection, $items, $options); + return static::tag('select', "\n" . $selectOptions . "\n", $options); + } + + /** + * Generates a list box. + * @param string $name the input name + * @param string|array $selection the selected value(s) + * @param array $items the option data items. The array keys are option values, and the array values + * are the corresponding option labels. The array can also be nested (i.e. some array values are arrays too). + * For each sub-array, an option group will be generated whose label is the key associated with the sub-array. + * If you have a list of data models, you may convert them into the format described above using + * [[\yii\helpers\ArrayHelper::map()]]. + * + * Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in + * the labels will also be HTML-encoded. + * @param array $options the tag options in terms of name-value pairs. The following options are supported: + * + * - prompt: string, a prompt text to be displayed as the first option; + * - options: array, the attributes for the select option tags. The array keys must be valid option values, + * and the array values are the extra attributes for the corresponding option tags. For example, + * + * ~~~ + * array( + * 'value1' => array('disabled' => true), + * 'value2' => array('label' => 'value 2'), + * ); + * ~~~ + * + * - groups: array, the attributes for the optgroup tags. The structure of this is similar to that of 'options', + * except that the array keys represent the optgroup labels specified in $items. + * - unselect: string, the value that will be submitted when no option is selected. + * When this attribute is set, a hidden field will be generated so that if no option is selected in multiple + * mode, we can still obtain the posted unselect value. + * + * The rest of the options will be rendered as the attributes of the resulting tag. The values will + * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. + * + * @return string the generated list box tag + */ + public static function listBox($name, $selection = null, $items = array(), $options = array()) + { + if (!isset($options['size'])) { + $options['size'] = 4; + } + if (isset($options['multiple']) && $options['multiple'] && substr($name, -2) !== '[]') { + $name .= '[]'; + } + $options['name'] = $name; + if (isset($options['unselect'])) { + // add a hidden field so that if the list box has no option being selected, it still submits a value + if (substr($name, -2) === '[]') { + $name = substr($name, 0, -2); + } + $hidden = static::hiddenInput($name, $options['unselect']); + unset($options['unselect']); + } else { + $hidden = ''; + } + $selectOptions = static::renderSelectOptions($selection, $items, $options); + return $hidden . static::tag('select', "\n" . $selectOptions . "\n", $options); + } + + /** + * Generates a list of checkboxes. + * A checkbox list allows multiple selection, like [[listBox()]]. + * As a result, the corresponding submitted value is an array. + * @param string $name the name attribute of each checkbox. + * @param string|array $selection the selected value(s). + * @param array $items the data item used to generate the checkboxes. + * The array keys are the labels, while the array values are the corresponding checkbox values. + * Note that the labels will NOT be HTML-encoded, while the values will. + * @param array $options options (name => config) for the checkbox list. The following options are supported: + * + * - unselect: string, the value that should be submitted when none of the checkboxes is selected. + * By setting this option, a hidden input will be generated. + * - separator: string, the HTML code that separates items. + * - item: callable, a callback that can be used to customize the generation of the HTML code + * corresponding to a single item in $items. The signature of this callback must be: + * + * ~~~ + * function ($index, $label, $name, $checked, $value) + * ~~~ + * + * where $index is the zero-based index of the checkbox in the whole list; $label + * is the label for the checkbox; and $name, $value and $checked represent the name, + * value and the checked status of the checkbox input. + * @return string the generated checkbox list + */ + public static function checkboxList($name, $selection = null, $items = array(), $options = array()) + { + if (substr($name, -2) !== '[]') { + $name .= '[]'; + } + + $formatter = isset($options['item']) ? $options['item'] : null; + $lines = array(); + $index = 0; + foreach ($items as $value => $label) { + $checked = $selection !== null && + (!is_array($selection) && !strcmp($value, $selection) + || is_array($selection) && in_array($value, $selection)); + if ($formatter !== null) { + $lines[] = call_user_func($formatter, $index, $label, $name, $checked, $value); + } else { + $lines[] = static::label(static::checkbox($name, $checked, $value) . ' ' . $label); + } + $index++; + } + + if (isset($options['unselect'])) { + // add a hidden field so that if the list box has no option being selected, it still submits a value + $name2 = substr($name, -2) === '[]' ? substr($name, 0, -2) : $name; + $hidden = static::hiddenInput($name2, $options['unselect']); + } else { + $hidden = ''; + } + $separator = isset($options['separator']) ? $options['separator'] : "\n"; + + return $hidden . implode($separator, $lines); + } + + /** + * Generates a list of radio buttons. + * A radio button list is like a checkbox list, except that it only allows single selection. + * @param string $name the name attribute of each radio button. + * @param string|array $selection the selected value(s). + * @param array $items the data item used to generate the radio buttons. + * The array keys are the labels, while the array values are the corresponding radio button values. + * Note that the labels will NOT be HTML-encoded, while the values will. + * @param array $options options (name => config) for the radio button list. The following options are supported: + * + * - unselect: string, the value that should be submitted when none of the radio buttons is selected. + * By setting this option, a hidden input will be generated. + * - separator: string, the HTML code that separates items. + * - item: callable, a callback that can be used to customize the generation of the HTML code + * corresponding to a single item in $items. The signature of this callback must be: + * + * ~~~ + * function ($index, $label, $name, $checked, $value) + * ~~~ + * + * where $index is the zero-based index of the radio button in the whole list; $label + * is the label for the radio button; and $name, $value and $checked represent the name, + * value and the checked status of the radio button input. + * @return string the generated radio button list + */ + public static function radioList($name, $selection = null, $items = array(), $options = array()) + { + $formatter = isset($options['item']) ? $options['item'] : null; + $lines = array(); + $index = 0; + foreach ($items as $value => $label) { + $checked = $selection !== null && + (!is_array($selection) && !strcmp($value, $selection) + || is_array($selection) && in_array($value, $selection)); + if ($formatter !== null) { + $lines[] = call_user_func($formatter, $index, $label, $name, $checked, $value); + } else { + $lines[] = static::label(static::radio($name, $checked, $value) . ' ' . $label); + } + $index++; + } + + $separator = isset($options['separator']) ? $options['separator'] : "\n"; + if (isset($options['unselect'])) { + // add a hidden field so that if the list box has no option being selected, it still submits a value + $hidden = static::hiddenInput($name, $options['unselect']); + } else { + $hidden = ''; + } + + return $hidden . implode($separator, $lines); + } + + /** + * Renders the option tags that can be used by [[dropDownList()]] and [[listBox()]]. + * @param string|array $selection the selected value(s). This can be either a string for single selection + * or an array for multiple selections. + * @param array $items the option data items. The array keys are option values, and the array values + * are the corresponding option labels. The array can also be nested (i.e. some array values are arrays too). + * For each sub-array, an option group will be generated whose label is the key associated with the sub-array. + * If you have a list of data models, you may convert them into the format described above using + * [[\yii\helpers\ArrayHelper::map()]]. + * + * Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in + * the labels will also be HTML-encoded. + * @param array $tagOptions the $options parameter that is passed to the [[dropDownList()]] or [[listBox()]] call. + * This method will take out these elements, if any: "prompt", "options" and "groups". See more details + * in [[dropDownList()]] for the explanation of these elements. + * + * @return string the generated list options + */ + public static function renderSelectOptions($selection, $items, &$tagOptions = array()) + { + $lines = array(); + if (isset($tagOptions['prompt'])) { + $prompt = str_replace(' ', ' ', static::encode($tagOptions['prompt'])); + $lines[] = static::tag('option', $prompt, array('value' => '')); + } + + $options = isset($tagOptions['options']) ? $tagOptions['options'] : array(); + $groups = isset($tagOptions['groups']) ? $tagOptions['groups'] : array(); + unset($tagOptions['prompt'], $tagOptions['options'], $tagOptions['groups']); + + foreach ($items as $key => $value) { + if (is_array($value)) { + $groupAttrs = isset($groups[$key]) ? $groups[$key] : array(); + $groupAttrs['label'] = $key; + $attrs = array('options' => $options, 'groups' => $groups); + $content = static::renderSelectOptions($selection, $value, $attrs); + $lines[] = static::tag('optgroup', "\n" . $content . "\n", $groupAttrs); + } else { + $attrs = isset($options[$key]) ? $options[$key] : array(); + $attrs['value'] = $key; + $attrs['selected'] = $selection !== null && + (!is_array($selection) && !strcmp($key, $selection) + || is_array($selection) && in_array($key, $selection)); + $lines[] = static::tag('option', str_replace(' ', ' ', static::encode($value)), $attrs); + } + } + + return implode("\n", $lines); + } + + /** + * Renders the HTML tag attributes. + * Boolean attributes such as s 'checked', 'disabled', 'readonly', will be handled specially + * according to [[booleanAttributes]] and [[showBooleanAttributeValues]]. + * @param array $attributes attributes to be rendered. The attribute values will be HTML-encoded using [[encode()]]. + * Attributes whose value is null will be ignored and not put in the rendering result. + * @return string the rendering result. If the attributes are not empty, they will be rendered + * into a string with a leading white space (such that it can be directly appended to the tag name + * in a tag. If there is no attribute, an empty string will be returned. + */ + public static function renderTagAttributes($attributes) + { + if (count($attributes) > 1) { + $sorted = array(); + foreach (static::$attributeOrder as $name) { + if (isset($attributes[$name])) { + $sorted[$name] = $attributes[$name]; + } + } + $attributes = array_merge($sorted, $attributes); + } + + $html = ''; + foreach ($attributes as $name => $value) { + if (isset(static::$booleanAttributes[strtolower($name)])) { + if ($value || strcasecmp($name, $value) === 0) { + $html .= static::$showBooleanAttributeValues ? " $name=\"$name\"" : " $name"; + } + } elseif ($value !== null) { + $html .= " $name=\"" . static::encode($value) . '"'; + } + } + return $html; + } + + /** + * Normalizes the input parameter to be a valid URL. + * + * If the input parameter + * + * - is an empty string: the currently requested URL will be returned; + * - is a non-empty string: it will be processed by [[Yii::getAlias()]] and returned; + * - is an array: the first array element is considered a route, while the rest of the name-value + * pairs are treated as the parameters to be used for URL creation using [[\yii\web\Controller::createUrl()]]. + * For example: `array('post/index', 'page' => 2)`, `array('index')`. + * + * @param array|string $url the parameter to be used to generate a valid URL + * @return string the normalized URL + * @throws InvalidParamException if the parameter is invalid. + */ + public static function url($url) + { + if (is_array($url)) { + if (isset($url[0])) { + $route = $url[0]; + $params = array_splice($url, 1); + if (Yii::$app->controller !== null) { + return Yii::$app->controller->createUrl($route, $params); + } else { + return Yii::$app->getUrlManager()->createUrl($route, $params); + } + } else { + throw new InvalidParamException('The array specifying a URL must contain at least one element.'); + } + } elseif ($url === '') { + return Yii::$app->getRequest()->getUrl(); + } else { + return Yii::getAlias($url); + } + } +} diff --git a/framework/helpers/SecurityHelper.php b/framework/helpers/SecurityHelper.php new file mode 100644 index 0000000..5029dd6 --- /dev/null +++ b/framework/helpers/SecurityHelper.php @@ -0,0 +1,272 @@ + + * @author Tom Worster + * @since 2.0 + */ +class SecurityHelper +{ + /** + * Encrypts data. + * @param string $data data to be encrypted. + * @param string $key the encryption secret key + * @return string the encrypted data + * @throws Exception if PHP Mcrypt extension is not loaded or failed to be initialized + * @see decrypt() + */ + public static function encrypt($data, $key) + { + $module = static::openCryptModule(); + $key = StringHelper::substr($key, 0, mcrypt_enc_get_key_size($module)); + srand(); + $iv = mcrypt_create_iv(mcrypt_enc_get_iv_size($module), MCRYPT_RAND); + mcrypt_generic_init($module, $key, $iv); + $encrypted = $iv . mcrypt_generic($module, $data); + mcrypt_generic_deinit($module); + mcrypt_module_close($module); + return $encrypted; + } + + /** + * Decrypts data + * @param string $data data to be decrypted. + * @param string $key the decryption secret key + * @return string the decrypted data + * @throws Exception if PHP Mcrypt extension is not loaded or failed to be initialized + * @see encrypt() + */ + public static function decrypt($data, $key) + { + $module = static::openCryptModule(); + $key = StringHelper::substr($key, 0, mcrypt_enc_get_key_size($module)); + $ivSize = mcrypt_enc_get_iv_size($module); + $iv = StringHelper::substr($data, 0, $ivSize); + mcrypt_generic_init($module, $key, $iv); + $decrypted = mdecrypt_generic($module, StringHelper::substr($data, $ivSize, StringHelper::strlen($data))); + mcrypt_generic_deinit($module); + mcrypt_module_close($module); + return rtrim($decrypted, "\0"); + } + + /** + * Prefixes data with a keyed hash value so that it can later be detected if it is tampered. + * @param string $data the data to be protected + * @param string $key the secret key to be used for generating hash + * @param string $algorithm the hashing algorithm (e.g. "md5", "sha1", "sha256", etc.). Call PHP "hash_algos()" + * function to see the supported hashing algorithms on your system. + * @return string the data prefixed with the keyed hash + * @see validateData() + * @see getSecretKey() + */ + public static function hashData($data, $key, $algorithm = 'sha256') + { + return hash_hmac($algorithm, $data, $key) . $data; + } + + /** + * Validates if the given data is tampered. + * @param string $data the data to be validated. The data must be previously + * generated by [[hashData()]]. + * @param string $key the secret key that was previously used to generate the hash for the data in [[hashData()]]. + * @param string $algorithm the hashing algorithm (e.g. "md5", "sha1", "sha256", etc.). Call PHP "hash_algos()" + * function to see the supported hashing algorithms on your system. This must be the same + * as the value passed to [[hashData()]] when generating the hash for the data. + * @return string the real data with the hash stripped off. False if the data is tampered. + * @see hashData() + */ + public static function validateData($data, $key, $algorithm = 'sha256') + { + $hashSize = StringHelper::strlen(hash_hmac($algorithm, 'test', $key)); + $n = StringHelper::strlen($data); + if ($n >= $hashSize) { + $hash = StringHelper::substr($data, 0, $hashSize); + $data2 = StringHelper::substr($data, $hashSize, $n - $hashSize); + return $hash === hash_hmac($algorithm, $data2, $key) ? $data2 : false; + } else { + return false; + } + } + + /** + * Returns a secret key associated with the specified name. + * If the secret key does not exist, a random key will be generated + * and saved in the file "keys.php" under the application's runtime directory + * so that the same secret key can be returned in future requests. + * @param string $name the name that is associated with the secret key + * @param integer $length the length of the key that should be generated if not exists + * @return string the secret key associated with the specified name + */ + public static function getSecretKey($name, $length = 32) + { + static $keys; + $keyFile = Yii::$app->getRuntimePath() . '/keys.php'; + if ($keys === null) { + $keys = is_file($keyFile) ? require($keyFile) : array(); + } + if (!isset($keys[$name])) { + // generate a 32-char random key + $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + $keys[$name] = substr(str_shuffle(str_repeat($chars, 5)), 0, $length); + file_put_contents($keyFile, " 30) { + throw new InvalidParamException('Hash is invalid.'); + } + + $test = crypt($password, $hash); + $n = strlen($test); + if (strlen($test) < 32 || $n !== strlen($hash)) { + return false; + } + + // Use a for-loop to compare two strings to prevent timing attacks. See: + // http://codereview.stackexchange.com/questions/13512 + $check = 0; + for ($i = 0; $i < $n; ++$i) { + $check |= (ord($test[$i]) ^ ord($hash[$i])); + } + + return $check === 0; + } + + /** + * Generates a salt that can be used to generate a password hash. + * + * The PHP [crypt()](http://php.net/manual/en/function.crypt.php) built-in function + * requires, for the Blowfish hash algorithm, a salt string in a specific format: + * "$2a$", "$2x$" or "$2y$", a two digit cost parameter, "$", and 22 characters + * from the alphabet "./0-9A-Za-z". + * + * @param integer $cost the cost parameter + * @return string the random salt value. + * @throws InvalidParamException if the cost parameter is not between 4 and 30 + */ + protected static function generateSalt($cost = 13) + { + $cost = (int)$cost; + if ($cost < 4 || $cost > 30) { + throw new InvalidParamException('Cost must be between 4 and 31.'); + } + + // Get 20 * 8bits of pseudo-random entropy from mt_rand(). + $rand = ''; + for ($i = 0; $i < 20; ++$i) { + $rand .= chr(mt_rand(0, 255)); + } + + // Add the microtime for a little more entropy. + $rand .= microtime(); + // Mix the bits cryptographically into a 20-byte binary string. + $rand = sha1($rand, true); + // Form the prefix that specifies Blowfish algorithm and cost parameter. + $salt = sprintf("$2y$%02d$", $cost); + // Append the random salt data in the required base64 format. + $salt .= str_replace('+', '.', substr(base64_encode($rand), 0, 22)); + return $salt; + } +} \ No newline at end of file diff --git a/framework/util/StringHelper.php b/framework/helpers/StringHelper.php similarity index 71% rename from framework/util/StringHelper.php rename to framework/helpers/StringHelper.php index 776657e..ace34db 100644 --- a/framework/util/StringHelper.php +++ b/framework/helpers/StringHelper.php @@ -1,13 +1,11 @@ '\1oves', '/(f)oot$/i' => '\1eet', '/(c)hild$/i' => '\1hildren', diff --git a/framework/helpers/VarDumper.php b/framework/helpers/VarDumper.php new file mode 100644 index 0000000..64c3639 --- /dev/null +++ b/framework/helpers/VarDumper.php @@ -0,0 +1,134 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2011 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\helpers; + +/** + * VarDumper is intended to replace the buggy PHP function var_dump and print_r. + * It can correctly identify the recursively referenced objects in a complex + * object structure. It also has a recursive depth control to avoid indefinite + * recursive display of some peculiar variables. + * + * VarDumper can be used as follows, + * + * ~~~ + * VarDumper::dump($var); + * ~~~ + * + * @author Qiang Xue + * @since 2.0 + */ +class CVarDumper +{ + private static $_objects; + private static $_output; + private static $_depth; + + /** + * Displays a variable. + * This method achieves the similar functionality as var_dump and print_r + * but is more robust when handling complex objects such as Yii controllers. + * @param mixed $var variable to be dumped + * @param integer $depth maximum depth that the dumper should go into the variable. Defaults to 10. + * @param boolean $highlight whether the result should be syntax-highlighted + */ + public static function dump($var, $depth = 10, $highlight = false) + { + echo self::dumpAsString($var, $depth, $highlight); + } + + /** + * Dumps a variable in terms of a string. + * This method achieves the similar functionality as var_dump and print_r + * but is more robust when handling complex objects such as Yii controllers. + * @param mixed $var variable to be dumped + * @param integer $depth maximum depth that the dumper should go into the variable. Defaults to 10. + * @param boolean $highlight whether the result should be syntax-highlighted + * @return string the string representation of the variable + */ + public static function dumpAsString($var, $depth = 10, $highlight = false) + { + self::$_output = ''; + self::$_objects = array(); + self::$_depth = $depth; + self::dumpInternal($var, 0); + if ($highlight) { + $result = highlight_string("/', '', $result, 1); + } + return self::$_output; + } + + /* + * @param mixed $var variable to be dumped + * @param integer $level depth level + */ + private static function dumpInternal($var, $level) + { + switch (gettype($var)) { + case 'boolean': + self::$_output .= $var ? 'true' : 'false'; + break; + case 'integer': + self::$_output .= "$var"; + break; + case 'double': + self::$_output .= "$var"; + break; + case 'string': + self::$_output .= "'" . addslashes($var) . "'"; + break; + case 'resource': + self::$_output .= '{resource}'; + break; + case 'NULL': + self::$_output .= "null"; + break; + case 'unknown type': + self::$_output .= '{unknown}'; + break; + case 'array': + if (self::$_depth <= $level) { + self::$_output .= 'array(...)'; + } elseif (empty($var)) { + self::$_output .= 'array()'; + } else { + $keys = array_keys($var); + $spaces = str_repeat(' ', $level * 4); + self::$_output .= "array\n" . $spaces . '('; + foreach ($keys as $key) { + self::$_output .= "\n" . $spaces . ' '; + self::dumpInternal($key, 0); + self::$_output .= ' => '; + self::dumpInternal($var[$key], $level + 1); + } + self::$_output .= "\n" . $spaces . ')'; + } + break; + case 'object': + if (($id = array_search($var, self::$_objects, true)) !== false) { + self::$_output .= get_class($var) . '#' . ($id + 1) . '(...)'; + } elseif (self::$_depth <= $level) { + self::$_output .= get_class($var) . '(...)'; + } else { + $id = self::$_objects[] = $var; + $className = get_class($var); + $members = (array)$var; + $spaces = str_repeat(' ', $level * 4); + self::$_output .= "$className#$id\n" . $spaces . '('; + foreach ($members as $key => $value) { + $keyDisplay = strtr(trim($key), array("\0" => ':')); + self::$_output .= "\n" . $spaces . " [$keyDisplay] => "; + self::dumpInternal($value, $level + 1); + } + self::$_output .= "\n" . $spaces . ')'; + } + break; + } + } +} \ No newline at end of file diff --git a/framework/util/mimeTypes.php b/framework/helpers/mimeTypes.php similarity index 99% rename from framework/util/mimeTypes.php rename to framework/helpers/mimeTypes.php index 87295f8..ffdba4b 100644 --- a/framework/util/mimeTypes.php +++ b/framework/helpers/mimeTypes.php @@ -6,7 +6,7 @@ * according to file extension names. * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ * @since 2.0 */ diff --git a/framework/i18n/I18N.php b/framework/i18n/I18N.php new file mode 100644 index 0000000..0409da3 --- /dev/null +++ b/framework/i18n/I18N.php @@ -0,0 +1,119 @@ +translations['yii'])) { + $this->translations['yii'] = array( + 'class' => 'yii\i18n\PhpMessageSource', + 'sourceLanguage' => 'en_US', + 'basePath' => '@yii/messages', + ); + } + if (!isset($this->translations['app'])) { + $this->translations['app'] = array( + 'class' => 'yii\i18n\PhpMessageSource', + 'sourceLanguage' => 'en_US', + 'basePath' => '@app/messages', + ); + } + } + + public function translate($message, $params = array(), $language = null) + { + if ($language === null) { + $language = Yii::$app->language; + } + + // allow chars for category: word chars, ".", "-", "/","\" + if (strpos($message, '|') !== false && preg_match('/^([\w\-\\/\.\\\\]+)\|(.*)/', $message, $matches)) { + $category = $matches[1]; + $message = $matches[2]; + } else { + $category = 'app'; + } + + $message = $this->getMessageSource($category)->translate($category, $message, $language); + + if (!is_array($params)) { + $params = array($params); + } + + if (isset($params[0])) { + $message = $this->getPluralForm($message, $params[0], $language); + if (!isset($params['{n}'])) { + $params['{n}'] = $params[0]; + } + unset($params[0]); + } + + return $params === array() ? $message : strtr($message, $params); + } + + public function getMessageSource($category) + { + if (isset($this->translations[$category])) { + $source = $this->translations[$category]; + } else { + // try wildcard matching + foreach ($this->translations as $pattern => $config) { + if (substr($pattern, -1) === '*' && strpos($category, rtrim($pattern, '*')) === 0) { + $source = $config; + break; + } + } + } + if (isset($source)) { + return $source instanceof MessageSource ? $source : Yii::createObject($source); + } else { + throw new InvalidConfigException("Unable to locate message source for category '$category'."); + } + } + + public function getLocale($language) + { + + } + + protected function getPluralForm($message, $number, $language) + { + if (strpos($message, '|') === false) { + return $message; + } + $chunks = explode('|', $message); + $rules = $this->getLocale($language)->getPluralRules(); + foreach ($rules as $i => $rule) { + if (isset($chunks[$i]) && $this->evaluate($rule, $number)) { + return $chunks[$i]; + } + } + $n = count($rules); + return isset($chunks[$n]) ? $chunks[$n] : $chunks[0]; + } + + /** + * Evaluates a PHP expression with the given number value. + * @param string $expression the PHP expression + * @param mixed $n the number value + * @return boolean the expression result + */ + protected function evaluate($expression, $n) + { + return @eval("return $expression;"); + } +} diff --git a/framework/i18n/MessageSource.php b/framework/i18n/MessageSource.php new file mode 100644 index 0000000..cf23338 --- /dev/null +++ b/framework/i18n/MessageSource.php @@ -0,0 +1,121 @@ + + * @since 2.0 + */ +class MessageSource extends Component +{ + /** + * @event MissingTranslationEvent an event that is triggered when a message translation is not found. + */ + const EVENT_MISSING_TRANSLATION = 'missingTranslation'; + + /** + * @var boolean whether to force message translation when the source and target languages are the same. + * Defaults to false, meaning translation is only performed when source and target languages are different. + */ + public $forceTranslation = false; + /** + * @var string the language that the original messages are in. If not set, it will use the value of + * [[\yii\base\Application::sourceLanguage]]. + */ + public $sourceLanguage; + + private $_messages = array(); + + /** + * Initializes this component. + */ + public function init() + { + parent::init(); + if ($this->sourceLanguage === null) { + $this->sourceLanguage = Yii::$app->sourceLanguage; + } + } + + /** + * 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) + { + return array(); + } + + /** + * Translates a message to the specified language. + * + * Note that unless [[forceTranslation]] is true, if the target language + * is the same as the [[sourceLanguage|source language]], the message + * will NOT be translated. + * + * If a translation is not found, a [[missingTranslation]] event will be triggered. + * + * @param string $category the message category + * @param string $message the message to be translated + * @param string $language the target language + * @return string the translated message (or the original message if translation is not needed) + */ + public function translate($category, $message, $language) + { + if ($this->forceTranslation || $language !== $this->sourceLanguage) { + return $this->translateMessage($category, $message, $language); + } else { + return $message; + } + } + + /** + * Translates the specified message. + * If the message is not found, a [[missingTranslation]] event will be triggered + * and the original message will be returned. + * @param string $category the category that the message belongs to + * @param string $message the message to be translated + * @param string $language the target language + * @return string the translated message + */ + protected function translateMessage($category, $message, $language) + { + $key = $language . '/' . $category; + if (!isset($this->_messages[$key])) { + $this->_messages[$key] = $this->loadMessages($category, $language); + } + if (isset($this->_messages[$key][$message]) && $this->_messages[$key][$message] !== '') { + return $this->_messages[$key][$message]; + } elseif ($this->hasEventHandlers('missingTranslation')) { + $event = new MissingTranslationEvent(array( + 'category' => $category, + 'message' => $message, + 'language' => $language, + )); + $this->trigger(self::EVENT_MISSING_TRANSLATION, $event); + return $this->_messages[$key] = $event->message; + } else { + return $message; + } + } +} + diff --git a/framework/i18n/MissingTranslationEvent.php b/framework/i18n/MissingTranslationEvent.php new file mode 100644 index 0000000..9ac337a --- /dev/null +++ b/framework/i18n/MissingTranslationEvent.php @@ -0,0 +1,33 @@ + + * @since 2.0 + */ +class MissingTranslationEvent extends Event +{ + /** + * @var string the message to be translated. An event handler may overwrite this property + * with a translated version if possible. + */ + public $message; + /** + * @var string the category that the message belongs to + */ + public $category; + /** + * @var string the language ID (e.g. en_US) that the message is to be translated to + */ + public $language; +} diff --git a/framework/i18n/PhpMessageSource.php b/framework/i18n/PhpMessageSource.php new file mode 100644 index 0000000..6b12353 --- /dev/null +++ b/framework/i18n/PhpMessageSource.php @@ -0,0 +1,79 @@ + 'translated message 1', + * 'original message 2' => 'translated message 2', + * ); + * ~~~ + * + * @author Qiang Xue + * @since 2.0 + */ +class PhpMessageSource extends MessageSource +{ + /** + * @var string the base path for all translated messages. Defaults to null, meaning + * the "messages" subdirectory of the application directory (e.g. "protected/messages"). + */ + public $basePath = '@app/messages'; + /** + * @var array mapping between message categories and the corresponding message file paths. + * The file paths are relative to [[basePath]]. For example, + * + * ~~~ + * array( + * 'core' => 'core.php', + * 'ext' => 'extensions.php', + * ) + * ~~~ + */ + public $fileMap; + + /** + * Loads the message translation for the specified language and category. + * @param string $category the message category + * @param string $language the target language + * @return array the loaded messages + */ + protected function loadMessages($category, $language) + { + $messageFile = Yii::getAlias($this->basePath) . "/$language/"; + if (isset($this->fileMap[$category])) { + $messageFile .= $this->fileMap[$category]; + } elseif (($pos = strrpos($category, '\\')) !== false) { + $messageFile .= (substr($category, $pos) . '.php'); + } else { + $messageFile .= "$category.php"; + } + if (is_file($messageFile)) { + $messages = include($messageFile); + if (!is_array($messages)) { + $messages = array(); + } + return $messages; + } else { + Yii::error("The message file for category '$category' does not exist: $messageFile", __CLASS__); + return array(); + } + } +} \ No newline at end of file diff --git a/framework/logging/DbTarget.php b/framework/logging/DbTarget.php index 129e4d4..e4e30ce 100644 --- a/framework/logging/DbTarget.php +++ b/framework/logging/DbTarget.php @@ -1,24 +1,21 @@ * @since 2.0 @@ -26,20 +23,18 @@ use yii\base\InvalidConfigException; class DbTarget extends Target { /** - * @var string the ID of [[Connection]] application component. - * Defaults to 'db'. Please make sure that your database contains a table - * whose name is as specified in [[tableName]] and has the required table structure. - * @see tableName + * @var Connection|string the DB connection object or the application component ID of the DB connection. + * After the DbTarget object is created, if you want to change this property, you should only assign it + * with a DB connection object. */ - public $connectionID = 'db'; + public $db = 'db'; /** - * @var string the name of the DB table that stores log messages. Defaults to 'tbl_log'. - * - * The DB table should have the following structure: + * @var string name of the DB table to store cache content. + * The table should be pre-created as follows: * * ~~~ * CREATE TABLE tbl_log ( - * id INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY, + * id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, * level INTEGER, * category VARCHAR(255), * log_time INTEGER, @@ -50,42 +45,29 @@ class DbTarget extends Target * ~~~ * * Note that the 'id' column must be created as an auto-incremental column. - * The above SQL shows the syntax of MySQL. If you are using other DBMS, you need + * The above SQL uses the MySQL syntax. If you are using other DBMS, you need * to adjust it accordingly. For example, in PostgreSQL, it should be `id SERIAL PRIMARY KEY`. * * The indexes declared above are not required. They are mainly used to improve the performance * of some queries about message levels and categories. Depending on your actual needs, you may - * want to create additional indexes (e.g. index on log_time). + * want to create additional indexes (e.g. index on `log_time`). */ - public $tableName = 'tbl_log'; - - private $_db; + public $logTable = 'tbl_log'; /** - * Returns the DB connection used for saving log messages. - * @return Connection the DB connection instance - * @throws InvalidConfigException if [[connectionID]] does not point to a valid application component. + * Initializes the DbTarget component. + * This method will initialize the [[db]] property to make sure it refers to a valid DB connection. + * @throws InvalidConfigException if [[db]] is invalid. */ - public function getDb() + public function init() { - if ($this->_db === null) { - $db = \Yii::$application->getComponent($this->connectionID); - if ($db instanceof Connection) { - $this->_db = $db; - } else { - throw new InvalidConfigException("DbTarget::connectionID must refer to the ID of a DB application component."); - } + parent::init(); + if (is_string($this->db)) { + $this->db = Yii::$app->getComponent($this->db); + } + if (!$this->db instanceof Connection) { + throw new InvalidConfigException("DbTarget::db must be either a DB connection instance or the application component ID of a DB connection."); } - return $this->_db; - } - - /** - * Sets the DB connection used by the cache component. - * @param Connection $value the DB connection instance - */ - public function setDb($value) - { - $this->_db = $value; } /** @@ -95,10 +77,9 @@ class DbTarget extends Target */ public function export($messages) { - $db = $this->getDb(); - $tableName = $db->quoteTableName($this->tableName); + $tableName = $this->db->quoteTableName($this->logTable); $sql = "INSERT INTO $tableName (level, category, log_time, message) VALUES (:level, :category, :log_time, :message)"; - $command = $db->createCommand($sql); + $command = $this->db->createCommand($sql); foreach ($messages as $message) { $command->bindValues(array( ':level' => $message[1], diff --git a/framework/logging/EmailTarget.php b/framework/logging/EmailTarget.php index e02e4da..4c84739 100644 --- a/framework/logging/EmailTarget.php +++ b/framework/logging/EmailTarget.php @@ -1,9 +1,7 @@ formatMessage($message); } $body = wordwrap($body, 70); - $subject = $this->subject === null ? \Yii::t('yii', 'Application Log') : $this->subject; + $subject = $this->subject === null ? \Yii::t('yii|Application Log') : $this->subject; foreach ($this->emails as $email) { $this->sendEmail($subject, $body, $email, $this->sentFrom, $this->headers); } diff --git a/framework/logging/FileTarget.php b/framework/logging/FileTarget.php index 0eb897e..c3f4031 100644 --- a/framework/logging/FileTarget.php +++ b/framework/logging/FileTarget.php @@ -1,9 +1,7 @@ logFile === null) { - $this->logFile = \Yii::$application->getRuntimePath() . DIRECTORY_SEPARATOR . 'application.log'; + $this->logFile = \Yii::$app->getRuntimePath() . DIRECTORY_SEPARATOR . 'application.log'; } else { $this->logFile = \Yii::getAlias($this->logFile); } diff --git a/framework/logging/Logger.php b/framework/logging/Logger.php index a8ffb5e..607c388 100644 --- a/framework/logging/Logger.php +++ b/framework/logging/Logger.php @@ -1,9 +1,7 @@ _report = $value; else - throw new CException(Yii::t('yii', 'CProfileLogRoute.report "{report}" is invalid. Valid values include "summary" and "callstack".', + throw new CException(Yii::t('yii|CProfileLogRoute.report "{report}" is invalid. Valid values include "summary" and "callstack".', array('{report}' => $value))); } @@ -71,7 +69,7 @@ class CProfileLogRoute extends CWebLogRoute */ public function processLogs($logs) { - $app = \Yii::$application; + $app = \Yii::$app; if (!($app instanceof CWebApplication) || $app->getRequest()->getIsAjaxRequest()) return; @@ -108,7 +106,7 @@ class CProfileLogRoute extends CWebLogRoute $results[$last[4]] = array($token, $delta, count($stack)); } else { - throw new CException(Yii::t('yii', 'CProfileLogRoute found a mismatching code block "{token}". Make sure the calls to Yii::beginProfile() and Yii::endProfile() be properly nested.', + throw new CException(Yii::t('yii|CProfileLogRoute found a mismatching code block "{token}". Make sure the calls to Yii::beginProfile() and Yii::endProfile() be properly nested.', array('{token}' => $token))); } } @@ -151,7 +149,7 @@ class CProfileLogRoute extends CWebLogRoute else $results[$token] = array($token, 1, $delta, $delta, $delta); } else - throw new CException(Yii::t('yii', 'CProfileLogRoute found a mismatching code block "{token}". Make sure the calls to Yii::beginProfile() and Yii::endProfile() be properly nested.', + throw new CException(Yii::t('yii|CProfileLogRoute found a mismatching code block "{token}". Make sure the calls to Yii::beginProfile() and Yii::endProfile() be properly nested.', array('{token}' => $token))); } } diff --git a/framework/logging/Router.php b/framework/logging/Router.php index 2e6a8dd..2f399fe 100644 --- a/framework/logging/Router.php +++ b/framework/logging/Router.php @@ -1,9 +1,7 @@ log->targets['file']->enabled = false; + * Yii::$app->log->targets['file']->enabled = false; * ~~~ * * @author Qiang Xue diff --git a/framework/logging/Target.php b/framework/logging/Target.php index c9e175a..b88e78d 100644 --- a/framework/logging/Target.php +++ b/framework/logging/Target.php @@ -1,9 +1,7 @@ logUser && ($user = \Yii::$application->getComponent('user', false)) !== null) { + if ($this->logUser && ($user = \Yii::$app->getComponent('user', false)) !== null) { $context[] = 'User: ' . $user->getName() . ' (ID: ' . $user->getId() . ')'; } @@ -194,8 +192,7 @@ abstract class Target extends \yii\base\Component $matched = empty($this->categories); foreach ($this->categories as $category) { - $prefix = rtrim($category, '*'); - if (strpos($message[2], $prefix) === 0 && ($message[2] === $category || $prefix !== $category)) { + if ($message[2] === $category || substr($category, -1) === '*' && strpos($message[2], rtrim($category, '*')) === 0) { $matched = true; break; } diff --git a/framework/logging/WebTarget.php b/framework/logging/WebTarget.php index 6ce8ea0..b71e1a2 100644 --- a/framework/logging/WebTarget.php +++ b/framework/logging/WebTarget.php @@ -1,7 +1,5 @@ getRequest()->getIsAjaxRequest(); if ($this->showInFireBug) diff --git a/framework/test/TestCase.php b/framework/test/TestCase.php index 959bb96..f190e5a 100644 --- a/framework/test/TestCase.php +++ b/framework/test/TestCase.php @@ -3,16 +3,16 @@ * TestCase class. * * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ namespace yii\test; require_once('PHPUnit/Runner/Version.php'); -spl_autoload_unregister(array('YiiBase','autoload')); +spl_autoload_unregister(array('Yii','autoload')); require_once('PHPUnit/Autoload.php'); -spl_autoload_register(array('YiiBase','autoload')); // put yii's autoloader at the end +spl_autoload_register(array('Yii','autoload')); // put yii's autoloader at the end /** * TestCase is the base class for all test case classes. diff --git a/framework/test/WebTestCase.php b/framework/test/WebTestCase.php new file mode 100644 index 0000000..39162c9 --- /dev/null +++ b/framework/test/WebTestCase.php @@ -0,0 +1,25 @@ + + * @since 2.0 + */ +abstract class WebTestCase extends \PHPUnit_Extensions_SeleniumTestCase +{ +} diff --git a/framework/util/VarDumper.php b/framework/util/VarDumper.php deleted file mode 100644 index 7497a03..0000000 --- a/framework/util/VarDumper.php +++ /dev/null @@ -1,144 +0,0 @@ - - * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008-2011 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -namespace yii\util; - -/** - * VarDumper is intended to replace the buggy PHP function var_dump and print_r. - * It can correctly identify the recursively referenced objects in a complex - * object structure. It also has a recursive depth control to avoid indefinite - * recursive display of some peculiar variables. - * - * VarDumper can be used as follows, - *
    - * VarDumper::dump($var);
    - * 
    - * - * @author Qiang Xue - * @since 2.0 - */ -class VarDumper -{ - private static $_objects; - private static $_output; - private static $_depth; - - /** - * Displays a variable. - * This method achieves the similar functionality as var_dump and print_r - * but is more robust when handling complex objects such as Yii controllers. - * @param mixed $var variable to be dumped - * @param integer $depth maximum depth that the dumper should go into the variable. Defaults to 10. - * @param boolean $highlight whether the result should be syntax-highlighted - */ - public static function dump($var,$depth=10,$highlight=false) - { - echo self::dumpAsString($var,$depth,$highlight); - } - - /** - * Dumps a variable in terms of a string. - * This method achieves the similar functionality as var_dump and print_r - * but is more robust when handling complex objects such as Yii controllers. - * @param mixed $var variable to be dumped - * @param integer $depth maximum depth that the dumper should go into the variable. Defaults to 10. - * @param boolean $highlight whether the result should be syntax-highlighted - * @return string the string representation of the variable - */ - public static function dumpAsString($var,$depth=10,$highlight=false) - { - self::$_output=''; - self::$_objects=array(); - self::$_depth=$depth; - self::dumpInternal($var,0); - if($highlight) - { - $result=highlight_string("/','',$result,1); - } - return self::$_output; - } - - /* - * @param mixed $var variable to be dumped - * @param integer $level depth level - */ - private static function dumpInternal($var,$level) - { - switch(gettype($var)) - { - case 'boolean': - self::$_output.=$var?'true':'false'; - break; - case 'integer': - self::$_output.="$var"; - break; - case 'double': - self::$_output.="$var"; - break; - case 'string': - self::$_output.="'".addslashes($var)."'"; - break; - case 'resource': - self::$_output.='{resource}'; - break; - case 'NULL': - self::$_output.="null"; - break; - case 'unknown type': - self::$_output.='{unknown}'; - break; - case 'array': - if(self::$_depth<=$level) - self::$_output.='array(...)'; - else if(empty($var)) - self::$_output.='array()'; - else - { - $keys=array_keys($var); - $spaces=str_repeat(' ',$level*4); - self::$_output.="array\n".$spaces.'('; - foreach($keys as $key) - { - if(gettype($key)=='integer') - $key2=$key; - else - $key2="'".str_replace("'","\\'",$key)."'"; - - self::$_output.="\n".$spaces." $key2 => "; - self::$_output.=self::dumpInternal($var[$key],$level+1); - } - self::$_output.="\n".$spaces.')'; - } - break; - case 'object': - if(($id=array_search($var,self::$_objects,true))!==false) - self::$_output.=get_class($var).'#'.($id+1).'(...)'; - else if(self::$_depth<=$level) - self::$_output.=get_class($var).'(...)'; - else - { - $id=array_push(self::$_objects,$var); - $className=get_class($var); - $members=(array)$var; - $spaces=str_repeat(' ',$level*4); - self::$_output.="$className#$id\n".$spaces.'('; - foreach($members as $key=>$value) - { - $keyDisplay=strtr(trim($key),array("\0"=>':')); - self::$_output.="\n".$spaces." [$keyDisplay] => "; - self::$_output.=self::dumpInternal($value,$level+1); - } - self::$_output.="\n".$spaces.')'; - } - break; - } - } -} \ No newline at end of file diff --git a/framework/validators/BooleanValidator.php b/framework/validators/BooleanValidator.php index 90e7939..427fa44 100644 --- a/framework/validators/BooleanValidator.php +++ b/framework/validators/BooleanValidator.php @@ -1,9 +1,7 @@ strict && $value != $this->trueValue && $value != $this->falseValue || $this->strict && $value !== $this->trueValue && $value !== $this->falseValue) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii', '{attribute} must be either {true} or {false}.'); + $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} must be either {true} or {false}.'); $this->addError($object, $attribute, $message, array( '{true}' => $this->trueValue, '{false}' => $this->falseValue, @@ -70,7 +68,7 @@ class BooleanValidator extends Validator */ public function clientValidateAttribute($object, $attribute) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii', '{attribute} must be either {true} or {false}.'); + $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} must be either {true} or {false}.'); $message = strtr($message, array( '{attribute}' => $object->getAttributeLabel($attribute), '{value}' => $object->$attribute, diff --git a/framework/validators/CaptchaValidator.php b/framework/validators/CaptchaValidator.php index 3da8ed6..3f31f77 100644 --- a/framework/validators/CaptchaValidator.php +++ b/framework/validators/CaptchaValidator.php @@ -1,9 +1,7 @@ getCaptchaAction(); if (!$captcha->validate($value, $this->caseSensitive)) { - $message = $this->message !== null ? $this->message : \Yii::t('yii', 'The verification code is incorrect.'); + $message = $this->message !== null ? $this->message : \Yii::t('yii|The verification code is incorrect.'); $this->addError($object, $attribute, $message); } } @@ -61,13 +59,13 @@ class CaptchaValidator extends Validator public function getCaptchaAction() { if (strpos($this->captchaAction, '/') !== false) { // contains controller or module - $ca = \Yii::$application->createController($this->captchaAction); + $ca = \Yii::$app->createController($this->captchaAction); if ($ca !== null) { list($controller, $actionID) = $ca; $action = $controller->createAction($actionID); } } else { - $action = \Yii::$application->getController()->createAction($this->captchaAction); + $action = \Yii::$app->getController()->createAction($this->captchaAction); } if ($action === null) { @@ -85,7 +83,7 @@ class CaptchaValidator extends Validator public function clientValidateAttribute($object, $attribute) { $captcha = $this->getCaptchaAction(); - $message = $this->message !== null ? $this->message : \Yii::t('yii', 'The verification code is incorrect.'); + $message = $this->message !== null ? $this->message : \Yii::t('yii|The verification code is incorrect.'); $message = strtr($message, array( '{attribute}' => $object->getAttributeLabel($attribute), '{value}' => $object->$attribute, diff --git a/framework/validators/CompareValidator.php b/framework/validators/CompareValidator.php index 9345b73..43f2edf 100644 --- a/framework/validators/CompareValidator.php +++ b/framework/validators/CompareValidator.php @@ -1,9 +1,7 @@ strict && $value !== $compareValue) || (!$this->strict && $value != $compareValue)) { - $message = ($this->message !== null) ? $this->message : Yii::t('yii', '{attribute} must be repeated exactly.'); + $message = ($this->message !== null) ? $this->message : Yii::t('yii|{attribute} must be repeated exactly.'); $this->addError($object, $attribute, $message, array('{compareAttribute}' => $compareLabel)); } break; case '!=': if (($this->strict && $value === $compareValue) || (!$this->strict && $value == $compareValue)) { - $message = ($this->message !== null) ? $this->message : Yii::t('yii', '{attribute} must not be equal to "{compareValue}".'); + $message = ($this->message !== null) ? $this->message : Yii::t('yii|{attribute} must not be equal to "{compareValue}".'); $this->addError($object, $attribute, $message, array('{compareAttribute}' => $compareLabel, '{compareValue}' => $compareValue)); } break; case '>': if ($value <= $compareValue) { - $message = ($this->message !== null) ? $this->message : Yii::t('yii', '{attribute} must be greater than "{compareValue}".'); + $message = ($this->message !== null) ? $this->message : Yii::t('yii|{attribute} must be greater than "{compareValue}".'); $this->addError($object, $attribute, $message, array('{compareAttribute}' => $compareLabel, '{compareValue}' => $compareValue)); } break; case '>=': if ($value < $compareValue) { - $message = ($this->message !== null) ? $this->message : Yii::t('yii', '{attribute} must be greater than or equal to "{compareValue}".'); + $message = ($this->message !== null) ? $this->message : Yii::t('yii|{attribute} must be greater than or equal to "{compareValue}".'); $this->addError($object, $attribute, $message, array('{compareAttribute}' => $compareLabel, '{compareValue}' => $compareValue)); } break; case '<': if ($value >= $compareValue) { - $message = ($this->message !== null) ? $this->message : Yii::t('yii', '{attribute} must be less than "{compareValue}".'); + $message = ($this->message !== null) ? $this->message : Yii::t('yii|{attribute} must be less than "{compareValue}".'); $this->addError($object, $attribute, $message, array('{compareAttribute}' => $compareLabel, '{compareValue}' => $compareValue)); } break; case '<=': if ($value > $compareValue) { - $message = ($this->message !== null) ? $this->message : Yii::t('yii', '{attribute} must be less than or equal to "{compareValue}".'); + $message = ($this->message !== null) ? $this->message : Yii::t('yii|{attribute} must be less than or equal to "{compareValue}".'); $this->addError($object, $attribute, $message, array('{compareAttribute}' => $compareLabel, '{compareValue}' => $compareValue)); } break; @@ -158,37 +156,37 @@ class CompareValidator extends Validator case '=': case '==': if ($message === null) { - $message = Yii::t('yii', '{attribute} must be repeated exactly.'); + $message = Yii::t('yii|{attribute} must be repeated exactly.'); } $condition = 'value!=' . $compareValue; break; case '!=': if ($message === null) { - $message = Yii::t('yii', '{attribute} must not be equal to "{compareValue}".'); + $message = Yii::t('yii|{attribute} must not be equal to "{compareValue}".'); } $condition = 'value==' . $compareValue; break; case '>': if ($message === null) { - $message = Yii::t('yii', '{attribute} must be greater than "{compareValue}".'); + $message = Yii::t('yii|{attribute} must be greater than "{compareValue}".'); } $condition = 'value<=' . $compareValue; break; case '>=': if ($message === null) { - $message = Yii::t('yii', '{attribute} must be greater than or equal to "{compareValue}".'); + $message = Yii::t('yii|{attribute} must be greater than or equal to "{compareValue}".'); } $condition = 'value<' . $compareValue; break; case '<': if ($message === null) { - $message = Yii::t('yii', '{attribute} must be less than "{compareValue}".'); + $message = Yii::t('yii|{attribute} must be less than "{compareValue}".'); } $condition = 'value>=' . $compareValue; break; case '<=': if ($message === null) { - $message = Yii::t('yii', '{attribute} must be less than or equal to "{compareValue}".'); + $message = Yii::t('yii|{attribute} must be less than or equal to "{compareValue}".'); } $condition = 'value>' . $compareValue; break; diff --git a/framework/validators/DateValidator.php b/framework/validators/DateValidator.php index f4fa866..7899c95 100644 --- a/framework/validators/DateValidator.php +++ b/framework/validators/DateValidator.php @@ -1,9 +1,7 @@ message !== null) ? $this->message : \Yii::t('yii', 'The format of {attribute} is invalid.'); + $message = ($this->message !== null) ? $this->message : \Yii::t('yii|The format of {attribute} is invalid.'); $this->addError($object, $attribute, $message); } } diff --git a/framework/validators/DefaultValueValidator.php b/framework/validators/DefaultValueValidator.php index 1673182..be06768 100644 --- a/framework/validators/DefaultValueValidator.php +++ b/framework/validators/DefaultValueValidator.php @@ -1,9 +1,7 @@ validateValue($value)) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii', '{attribute} is not a valid email address.'); + $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} is not a valid email address.'); $this->addError($object, $attribute, $message); } } @@ -100,7 +98,7 @@ class EmailValidator extends Validator */ public function clientValidateAttribute($object, $attribute) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii', '{attribute} is not a valid email address.'); + $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} is not a valid email address.'); $message = strtr($message, array( '{attribute}' => $object->getAttributeLabel($attribute), '{value}' => $object->$attribute, diff --git a/framework/validators/ExistValidator.php b/framework/validators/ExistValidator.php index be710bd..8df3e19 100644 --- a/framework/validators/ExistValidator.php +++ b/framework/validators/ExistValidator.php @@ -1,9 +1,7 @@ where(array($column->name => $value)); if (!$query->exists()) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii', '{attribute} "{value}" is invalid.'); + $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} "{value}" is invalid.'); $this->addError($object, $attribute, $message); } } diff --git a/framework/validators/FileValidator.php b/framework/validators/FileValidator.php index 106b9a6..b05ac2a 100644 --- a/framework/validators/FileValidator.php +++ b/framework/validators/FileValidator.php @@ -1,9 +1,7 @@ emptyAttribute($object, $attribute); if (count($files) > $this->maxFiles) { - $message = $this->tooMany !== null ? $this->tooMany : \Yii::t('yii', '{attribute} cannot accept more than {limit} files.'); + $message = $this->tooMany !== null ? $this->tooMany : \Yii::t('yii|{attribute} cannot accept more than {limit} files.'); $this->addError($object, $attribute, $message, array('{attribute}' => $attribute, '{limit}' => $this->maxFiles)); } else foreach ($files as $file) @@ -145,20 +143,20 @@ class CFileValidator extends Validator return $this->emptyAttribute($object, $attribute); elseif ($error == UPLOAD_ERR_INI_SIZE || $error == UPLOAD_ERR_FORM_SIZE || $this->maxSize !== null && $file->getSize() > $this->maxSize) { - $message = $this->tooLarge !== null ? $this->tooLarge : \Yii::t('yii', 'The file "{file}" is too large. Its size cannot exceed {limit} bytes.'); + $message = $this->tooLarge !== null ? $this->tooLarge : \Yii::t('yii|The file "{file}" is too large. Its size cannot exceed {limit} bytes.'); $this->addError($object, $attribute, $message, array('{file}' => $file->getName(), '{limit}' => $this->getSizeLimit())); } elseif ($error == UPLOAD_ERR_PARTIAL) - throw new CException(\Yii::t('yii', 'The file "{file}" was only partially uploaded.', array('{file}' => $file->getName()))); + throw new CException(\Yii::t('yii|The file "{file}" was only partially uploaded.', array('{file}' => $file->getName()))); elseif ($error == UPLOAD_ERR_NO_TMP_DIR) - throw new CException(\Yii::t('yii', 'Missing the temporary folder to store the uploaded file "{file}".', array('{file}' => $file->getName()))); + throw new CException(\Yii::t('yii|Missing the temporary folder to store the uploaded file "{file}".', array('{file}' => $file->getName()))); elseif ($error == UPLOAD_ERR_CANT_WRITE) - throw new CException(\Yii::t('yii', 'Failed to write the uploaded file "{file}" to disk.', array('{file}' => $file->getName()))); + throw new CException(\Yii::t('yii|Failed to write the uploaded file "{file}" to disk.', array('{file}' => $file->getName()))); elseif (defined('UPLOAD_ERR_EXTENSION') && $error == UPLOAD_ERR_EXTENSION) // available for PHP 5.2.0 or above - throw new CException(\Yii::t('yii', 'File upload was stopped by extension.')); + throw new CException(\Yii::t('yii|File upload was stopped by extension.')); if ($this->minSize !== null && $file->getSize() < $this->minSize) { - $message = $this->tooSmall !== null ? $this->tooSmall : \Yii::t('yii', 'The file "{file}" is too small. Its size cannot be smaller than {limit} bytes.'); + $message = $this->tooSmall !== null ? $this->tooSmall : \Yii::t('yii|The file "{file}" is too small. Its size cannot be smaller than {limit} bytes.'); $this->addError($object, $attribute, $message, array('{file}' => $file->getName(), '{limit}' => $this->minSize)); } @@ -170,7 +168,7 @@ class CFileValidator extends Validator $types = $this->types; if (!in_array(strtolower($file->getExtensionName()), $types)) { - $message = $this->wrongType !== null ? $this->wrongType : \Yii::t('yii', 'The file "{file}" cannot be uploaded. Only files with these extensions are allowed: {extensions}.'); + $message = $this->wrongType !== null ? $this->wrongType : \Yii::t('yii|The file "{file}" cannot be uploaded. Only files with these extensions are allowed: {extensions}.'); $this->addError($object, $attribute, $message, array('{file}' => $file->getName(), '{extensions}' => implode(', ', $types))); } } @@ -185,7 +183,7 @@ class CFileValidator extends Validator { if (!$this->allowEmpty) { - $message = $this->message !== null ? $this->message : \Yii::t('yii', '{attribute} cannot be blank.'); + $message = $this->message !== null ? $this->message : \Yii::t('yii|{attribute} cannot be blank.'); $this->addError($object, $attribute, $message); } } diff --git a/framework/validators/FilterValidator.php b/framework/validators/FilterValidator.php index d20defd..c891979 100644 --- a/framework/validators/FilterValidator.php +++ b/framework/validators/FilterValidator.php @@ -1,9 +1,7 @@ integerOnly) { if (!preg_match($this->integerPattern, "$value")) { - $message = $this->message !== null ? $this->message : Yii::t('yii', '{attribute} must be an integer.'); + $message = $this->message !== null ? $this->message : Yii::t('yii|{attribute} must be an integer.'); $this->addError($object, $attribute, $message); } } else { if (!preg_match($this->numberPattern, "$value")) { - $message = $this->message !== null ? $this->message : Yii::t('yii', '{attribute} must be a number.'); + $message = $this->message !== null ? $this->message : Yii::t('yii|{attribute} must be a number.'); $this->addError($object, $attribute, $message); } } if ($this->min !== null && $value < $this->min) { - $message = $this->tooSmall !== null ? $this->tooSmall : Yii::t('yii', '{attribute} is too small (minimum is {min}).'); + $message = $this->tooSmall !== null ? $this->tooSmall : Yii::t('yii|{attribute} is too small (minimum is {min}).'); $this->addError($object, $attribute, $message, array('{min}' => $this->min)); } if ($this->max !== null && $value > $this->max) { - $message = $this->tooBig !== null ? $this->tooBig : Yii::t('yii', '{attribute} is too big (maximum is {max}).'); + $message = $this->tooBig !== null ? $this->tooBig : Yii::t('yii|{attribute} is too big (maximum is {max}).'); $this->addError($object, $attribute, $message, array('{max}' => $this->max)); } } @@ -103,8 +101,8 @@ class NumberValidator extends Validator $label = $object->getAttributeLabel($attribute); if (($message = $this->message) === null) { - $message = $this->integerOnly ? Yii::t('yii', '{attribute} must be an integer.') - : Yii::t('yii', '{attribute} must be a number.'); + $message = $this->integerOnly ? Yii::t('yii|{attribute} must be an integer.') + : Yii::t('yii|{attribute} must be a number.'); } $message = strtr($message, array( '{attribute}' => $label, @@ -118,7 +116,7 @@ if(!value.match($pattern)) { "; if ($this->min !== null) { if (($tooSmall = $this->tooSmall) === null) { - $tooSmall = Yii::t('yii', '{attribute} is too small (minimum is {min}).'); + $tooSmall = Yii::t('yii|{attribute} is too small (minimum is {min}).'); } $tooSmall = strtr($tooSmall, array( '{attribute}' => $label, @@ -133,7 +131,7 @@ if(value<{$this->min}) { } if ($this->max !== null) { if (($tooBig = $this->tooBig) === null) { - $tooBig = Yii::t('yii', '{attribute} is too big (maximum is {max}).'); + $tooBig = Yii::t('yii|{attribute} is too big (maximum is {max}).'); } $tooBig = strtr($tooBig, array( '{attribute}' => $label, diff --git a/framework/validators/RangeValidator.php b/framework/validators/RangeValidator.php index b2ff773..e23567c 100644 --- a/framework/validators/RangeValidator.php +++ b/framework/validators/RangeValidator.php @@ -1,9 +1,7 @@ not && !in_array($value, $this->range, $this->strict)) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii', '{attribute} should be in the list.'); + $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} should be in the list.'); $this->addError($object, $attribute, $message); } elseif ($this->not && in_array($value, $this->range, $this->strict)) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii', '{attribute} should NOT be in the list.'); + $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} should NOT be in the list.'); $this->addError($object, $attribute, $message); } } @@ -80,7 +78,7 @@ class RangeValidator extends Validator } if (($message = $this->message) === null) { - $message = $this->not ? \Yii::t('yii', '{attribute} should NOT be in the list.') : \Yii::t('yii', '{attribute} should be in the list.'); + $message = $this->not ? \Yii::t('yii|{attribute} should NOT be in the list.') : \Yii::t('yii|{attribute} should be in the list.'); } $message = strtr($message, array( '{attribute}' => $object->getAttributeLabel($attribute), diff --git a/framework/validators/RegularExpressionValidator.php b/framework/validators/RegularExpressionValidator.php index fbdb062..df2b657 100644 --- a/framework/validators/RegularExpressionValidator.php +++ b/framework/validators/RegularExpressionValidator.php @@ -1,9 +1,7 @@ not && !preg_match($this->pattern, $value)) || ($this->not && preg_match($this->pattern, $value))) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii', '{attribute} is invalid.'); + $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} is invalid.'); $this->addError($object, $attribute, $message); } } @@ -69,7 +67,7 @@ class RegularExpressionValidator extends Validator throw new \yii\base\Exception('The "pattern" property must be specified with a valid regular expression.'); } - $message = ($this->message !== null) ? $this->message : \Yii::t('yii', '{attribute} is invalid.'); + $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} is invalid.'); $message = strtr($message, array( '{attribute}' => $object->getAttributeLabel($attribute), '{value}' => $object->$attribute, diff --git a/framework/validators/RequiredValidator.php b/framework/validators/RequiredValidator.php index f0f4bfd..66b9c3c 100644 --- a/framework/validators/RequiredValidator.php +++ b/framework/validators/RequiredValidator.php @@ -1,9 +1,7 @@ $attribute; if ($this->requiredValue === null) { if ($this->strict && $value === null || !$this->strict && $this->isEmpty($value, true)) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii', '{attribute} cannot be blank.'); + $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} cannot be blank.'); $this->addError($object, $attribute, $message); } } else { if (!$this->strict && $value != $this->requiredValue || $this->strict && $value !== $this->requiredValue) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii', '{attribute} must be "{requiredValue}".'); + $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} must be "{requiredValue}".'); $this->addError($object, $attribute, $message, array( '{requiredValue}' => $this->requiredValue, )); @@ -71,7 +69,7 @@ class RequiredValidator extends Validator $message = $this->message; if ($this->requiredValue !== null) { if ($message === null) { - $message = \Yii::t('yii', '{attribute} must be "{requiredValue}".'); + $message = \Yii::t('yii|{attribute} must be "{requiredValue}".'); } $message = strtr($message, array( '{attribute}' => $object->getAttributeLabel($attribute), @@ -85,7 +83,7 @@ if (value != " . json_encode($this->requiredValue) . ") { "; } else { if ($message === null) { - $message = \Yii::t('yii', '{attribute} cannot be blank.'); + $message = \Yii::t('yii|{attribute} cannot be blank.'); } $message = strtr($message, array( '{attribute}' => $object->getAttributeLabel($attribute), diff --git a/framework/validators/StringValidator.php b/framework/validators/StringValidator.php index fe69a4d..9135b9e 100644 --- a/framework/validators/StringValidator.php +++ b/framework/validators/StringValidator.php @@ -1,9 +1,7 @@ message !== null) ? $this->message : \Yii::t('yii', '{attribute} must be a string.'); + $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} must be a string.'); $this->addError($object, $attribute, $message); return; } if (function_exists('mb_strlen') && $this->encoding !== false) { - $length = mb_strlen($value, $this->encoding ? $this->encoding : \Yii::$application->charset); + $length = mb_strlen($value, $this->encoding ? $this->encoding : \Yii::$app->charset); } else { $length = strlen($value); } if ($this->min !== null && $length < $this->min) { - $message = ($this->tooShort !== null) ? $this->tooShort : \Yii::t('yii', '{attribute} is too short (minimum is {min} characters).'); + $message = ($this->tooShort !== null) ? $this->tooShort : \Yii::t('yii|{attribute} is too short (minimum is {min} characters).'); $this->addError($object, $attribute, $message, array('{min}' => $this->min)); } if ($this->max !== null && $length > $this->max) { - $message = ($this->tooLong !== null) ? $this->tooLong : \Yii::t('yii', '{attribute} is too long (maximum is {max} characters).'); + $message = ($this->tooLong !== null) ? $this->tooLong : \Yii::t('yii|{attribute} is too long (maximum is {max} characters).'); $this->addError($object, $attribute, $message, array('{max}' => $this->max)); } if ($this->is !== null && $length !== $this->is) { - $message = ($this->notEqual !== null) ? $this->notEqual : \Yii::t('yii', '{attribute} is of the wrong length (should be {length} characters).'); + $message = ($this->notEqual !== null) ? $this->notEqual : \Yii::t('yii|{attribute} is of the wrong length (should be {length} characters).'); $this->addError($object, $attribute, $message, array('{length}' => $this->is)); } } @@ -113,7 +111,7 @@ class StringValidator extends Validator $value = $object->$attribute; if (($notEqual = $this->notEqual) === null) { - $notEqual = \Yii::t('yii', '{attribute} is of the wrong length (should be {length} characters).'); + $notEqual = \Yii::t('yii|{attribute} is of the wrong length (should be {length} characters).'); } $notEqual = strtr($notEqual, array( '{attribute}' => $label, @@ -122,7 +120,7 @@ class StringValidator extends Validator )); if (($tooShort = $this->tooShort) === null) { - $tooShort = \Yii::t('yii', '{attribute} is too short (minimum is {min} characters).'); + $tooShort = \Yii::t('yii|{attribute} is too short (minimum is {min} characters).'); } $tooShort = strtr($tooShort, array( '{attribute}' => $label, @@ -131,7 +129,7 @@ class StringValidator extends Validator )); if (($tooLong = $this->tooLong) === null) { - $tooLong = \Yii::t('yii', '{attribute} is too long (maximum is {max} characters).'); + $tooLong = \Yii::t('yii|{attribute} is too long (maximum is {max} characters).'); } $tooLong = strtr($tooLong, array( '{attribute}' => $label, diff --git a/framework/validators/UniqueValidator.php b/framework/validators/UniqueValidator.php index 5d5e603..bc12f5a 100644 --- a/framework/validators/UniqueValidator.php +++ b/framework/validators/UniqueValidator.php @@ -1,9 +1,7 @@ message !== null ? $this->message : \Yii::t('yii', '{attribute} "{value}" has already been taken.'); + $message = $this->message !== null ? $this->message : \Yii::t('yii|{attribute} "{value}" has already been taken.'); $this->addError($object, $attribute, $message); } } diff --git a/framework/validators/UrlValidator.php b/framework/validators/UrlValidator.php index c6242a2..0ba039b 100644 --- a/framework/validators/UrlValidator.php +++ b/framework/validators/UrlValidator.php @@ -1,9 +1,7 @@ validateValue($value)) !== false) { $object->$attribute = $value; } else { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii', '{attribute} is not a valid URL.'); + $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} is not a valid URL.'); $this->addError($object, $attribute, $message); } } @@ -97,7 +95,7 @@ class UrlValidator extends Validator */ public function clientValidateAttribute($object, $attribute) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii', '{attribute} is not a valid URL.'); + $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} is not a valid URL.'); $message = strtr($message, array( '{attribute}' => $object->getAttributeLabel($attribute), '{value}' => $object->$attribute, diff --git a/framework/validators/Validator.php b/framework/validators/Validator.php index a03da7a..b688f32 100644 --- a/framework/validators/Validator.php +++ b/framework/validators/Validator.php @@ -1,9 +1,7 @@ owner; +$context = $this->context; +$title = $context->htmlEncode($exception instanceof \yii\base\Exception || $exception instanceof \yii\base\ErrorException ? $exception->getName() : get_class($exception)); ?> - <?php echo get_class($exception)?> + <?php echo $title?> ", Html::style($content)); + $this->assertEquals("", Html::style($content, array('type' => 'text/less'))); + } + + public function testScript() + { + $content = 'a <>'; + $this->assertEquals("", Html::script($content)); + $this->assertEquals("", Html::script($content, array('type' => 'text/js'))); + } + + public function testCssFile() + { + $this->assertEquals('', Html::cssFile('http://example.com')); + $this->assertEquals('', Html::cssFile('')); + } + + public function testJsFile() + { + $this->assertEquals('', Html::jsFile('http://example.com')); + $this->assertEquals('', Html::jsFile('')); + } + + public function testBeginForm() + { + $this->assertEquals('
    ', Html::beginForm()); + $this->assertEquals('', Html::beginForm('/example', 'get')); + $hiddens = array( + '', + '', + ); + $this->assertEquals('' . "\n" . implode("\n", $hiddens), Html::beginForm('/example?id=1&title=%3C', 'get')); + } + + public function testEndForm() + { + $this->assertEquals('
    ', Html::endForm()); + } + + public function testA() + { + $this->assertEquals('something<>', Html::a('something<>')); + $this->assertEquals('something', Html::a('something', '/example')); + $this->assertEquals('something', Html::a('something', '')); + } + + public function testMailto() + { + $this->assertEquals('test<>', Html::mailto('test<>')); + $this->assertEquals('test<>', Html::mailto('test<>', 'test>')); + } + + public function testImg() + { + $this->assertEquals('', Html::img('/example')); + $this->assertEquals('', Html::img('')); + $this->assertEquals('something', Html::img('/example', array('alt' => 'something', 'width' => 10))); + } + + public function testLabel() + { + $this->assertEquals('', Html::label('something<>')); + $this->assertEquals('', Html::label('something<>', 'a')); + $this->assertEquals('', Html::label('something<>', 'a', array('class' => 'test'))); + } + + public function testButton() + { + $this->assertEquals('', Html::button()); + $this->assertEquals('', Html::button('test', 'value', 'content<>')); + $this->assertEquals('', Html::button('test', 'value', 'content<>', array('type' => 'submit', 'class' => "t"))); + } + + public function testSubmitButton() + { + $this->assertEquals('', Html::submitButton()); + $this->assertEquals('', Html::submitButton('test', 'value', 'content<>', array('class' => 't'))); + } + + public function testResetButton() + { + $this->assertEquals('', Html::resetButton()); + $this->assertEquals('', Html::resetButton('test', 'value', 'content<>', array('class' => 't'))); + } + + public function testInput() + { + $this->assertEquals('', Html::input('text')); + $this->assertEquals('', Html::input('text', 'test', 'value', array('class' => 't'))); + } + + public function testButtonInput() + { + $this->assertEquals('', Html::buttonInput('test')); + $this->assertEquals('', Html::buttonInput('test', 'text', array('class' => 'a'))); + } + + public function testSubmitInput() + { + $this->assertEquals('', Html::submitInput()); + $this->assertEquals('', Html::submitInput('test', 'text', array('class' => 'a'))); + } + + public function testResetInput() + { + $this->assertEquals('', Html::resetInput()); + $this->assertEquals('', Html::resetInput('test', 'text', array('class' => 'a'))); + } + + public function testTextInput() + { + $this->assertEquals('', Html::textInput('test')); + $this->assertEquals('', Html::textInput('test', 'value', array('class' => 't'))); + } + + public function testHiddenInput() + { + $this->assertEquals('', Html::hiddenInput('test')); + $this->assertEquals('', Html::hiddenInput('test', 'value', array('class' => 't'))); + } + + public function testPasswordInput() + { + $this->assertEquals('', Html::passwordInput('test')); + $this->assertEquals('', Html::passwordInput('test', 'value', array('class' => 't'))); + } + + public function testFileInput() + { + $this->assertEquals('', Html::fileInput('test')); + $this->assertEquals('', Html::fileInput('test', 'value', array('class' => 't'))); + } + + public function testTextarea() + { + $this->assertEquals('', Html::textarea('test')); + $this->assertEquals('', Html::textarea('test', 'value<>', array('class' => 't'))); + } + + public function testRadio() + { + $this->assertEquals('', Html::radio('test')); + $this->assertEquals('', Html::radio('test', true, null, array('class' => 'a'))); + $this->assertEquals('', Html::radio('test', true, 2, array('class' => 'a' , 'uncheck' => '0'))); + } + + public function testCheckbox() + { + $this->assertEquals('', Html::checkbox('test')); + $this->assertEquals('', Html::checkbox('test', true, null, array('class' => 'a'))); + $this->assertEquals('', Html::checkbox('test', true, 2, array('class' => 'a', 'uncheck' => '0'))); + } + + public function testDropDownList() + { + $expected = << + + +EOD; + $this->assertEquals($expected, Html::dropDownList('test')); + $expected = << + + + +EOD; + $this->assertEquals($expected, Html::dropDownList('test', null, $this->getDataItems())); + $expected = << + + + +EOD; + $this->assertEquals($expected, Html::dropDownList('test', 'value2', $this->getDataItems())); + } + + public function testListBox() + { + $expected = << + + +EOD; + $this->assertEquals($expected, Html::listBox('test')); + $expected = << + + + +EOD; + $this->assertEquals($expected, Html::listBox('test', null, $this->getDataItems(), array('size' => 5))); + $expected = << + + + +EOD; + $this->assertEquals($expected, Html::listBox('test', null, $this->getDataItems2())); + $expected = << + + + +EOD; + $this->assertEquals($expected, Html::listBox('test', 'value2', $this->getDataItems())); + $expected = << + + + +EOD; + $this->assertEquals($expected, Html::listBox('test', array('value1', 'value2'), $this->getDataItems())); + + $expected = << + + +EOD; + $this->assertEquals($expected, Html::listBox('test', null, array(), array('multiple' => true))); + $expected = << +EOD; + $this->assertEquals($expected, Html::listBox('test', '', array(), array('unselect' => '0'))); + } + + public function testCheckboxList() + { + $this->assertEquals('', Html::checkboxList('test')); + + $expected = << text1 + +EOD; + $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems())); + + $expected = << text1<> + +EOD; + $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems2())); + + $expected = <<
    + +EOD; + $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems(), array( + 'separator' => "
    \n", + 'unselect' => '0', + ))); + + $expected = <<text1 +1 +EOD; + $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems(), array( + 'item' => function ($index, $label, $name, $checked, $value) { + return $index . Html::label($label . ' ' . Html::checkbox($name, $checked, $value)); + } + ))); + } + + public function testRadioList() + { + $this->assertEquals('', Html::radioList('test')); + + $expected = << text1 + +EOD; + $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems())); + + $expected = << text1<> + +EOD; + $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems2())); + + $expected = <<
    + +EOD; + $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems(), array( + 'separator' => "
    \n", + 'unselect' => '0', + ))); + + $expected = <<text1 +1 +EOD; + $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems(), array( + 'item' => function ($index, $label, $name, $checked, $value) { + return $index . Html::label($label . ' ' . Html::radio($name, $checked, $value)); + } + ))); + } + + public function testRenderOptions() + { + $data = array( + 'value1' => 'label1', + 'group1' => array( + 'value11' => 'label11', + 'group11' => array( + 'value111' => 'label111', + ), + 'group12' => array(), + ), + 'value2' => 'label2', + 'group2' => array(), + ); + $expected = <<please select<> + + + + + + + + + + + + + + +EOD; + $attributes = array( + 'prompt' => 'please select<>', + 'options' => array( + 'value111' => array('class' => 'option'), + ), + 'groups' => array( + 'group12' => array('class' => 'group'), + ), + ); + $this->assertEquals($expected, Html::renderSelectOptions(array('value111', 'value1'), $data, $attributes)); + } + + public function testRenderAttributes() + { + $this->assertEquals('', Html::renderTagAttributes(array())); + $this->assertEquals(' name="test" value="1<>"', Html::renderTagAttributes(array('name' => 'test', 'empty' => null, 'value' => '1<>'))); + Html::$showBooleanAttributeValues = false; + $this->assertEquals(' checked disabled', Html::renderTagAttributes(array('checked' => 'checked', 'disabled' => true, 'hidden' => false))); + Html::$showBooleanAttributeValues = true; + } + + protected function getDataItems() + { + return array( + 'value1' => 'text1', + 'value2' => 'text2', + ); + } + + protected function getDataItems2() + { + return array( + 'value1<>' => 'text1<>', + 'value 2' => 'text 2', + ); + } +} diff --git a/tests/unit/framework/validators/EmailValidatorTest.php b/tests/unit/framework/validators/EmailValidatorTest.php new file mode 100644 index 0000000..fbc2f53 --- /dev/null +++ b/tests/unit/framework/validators/EmailValidatorTest.php @@ -0,0 +1,28 @@ +assertTrue($validator->validateValue('sam@rmcreative.ru')); + $this->assertTrue($validator->validateValue('5011@gmail.com')); + $this->assertFalse($validator->validateValue('rmcreative.ru')); + } + + public function testValidateValueMx() + { + $validator = new EmailValidator(); + $validator->checkMX = true; + + $this->assertTrue($validator->validateValue('sam@rmcreative.ru')); + $this->assertFalse($validator->validateValue('test@example.com')); + } +} \ No newline at end of file diff --git a/tests/unit/framework/web/UrlManagerTest.php b/tests/unit/framework/web/UrlManagerTest.php new file mode 100644 index 0000000..95b3bf6 --- /dev/null +++ b/tests/unit/framework/web/UrlManagerTest.php @@ -0,0 +1,210 @@ + '/', + 'cache' => null, + )); + $url = $manager->createUrl('post/view'); + $this->assertEquals('/?r=post/view', $url); + $url = $manager->createUrl('post/view', array('id' => 1, 'title' => 'sample post')); + $this->assertEquals('/?r=post/view&id=1&title=sample+post', $url); + + // default setting with '/test/' as base url + $manager = new UrlManager(array( + 'baseUrl' => '/test/', + 'cache' => null, + )); + $url = $manager->createUrl('post/view', array('id' => 1, 'title' => 'sample post')); + $this->assertEquals('/test/?r=post/view&id=1&title=sample+post', $url); + + // pretty URL without rules + $manager = new UrlManager(array( + 'enablePrettyUrl' => true, + 'baseUrl' => '/', + 'cache' => null, + )); + $url = $manager->createUrl('post/view', array('id' => 1, 'title' => 'sample post')); + $this->assertEquals('/post/view?id=1&title=sample+post', $url); + $manager = new UrlManager(array( + 'enablePrettyUrl' => true, + 'baseUrl' => '/test/', + 'cache' => null, + )); + $url = $manager->createUrl('post/view', array('id' => 1, 'title' => 'sample post')); + $this->assertEquals('/test/post/view?id=1&title=sample+post', $url); + $manager = new UrlManager(array( + 'enablePrettyUrl' => true, + 'baseUrl' => '/test/index.php', + 'cache' => null, + )); + $url = $manager->createUrl('post/view', array('id' => 1, 'title' => 'sample post')); + $this->assertEquals('/test/index.php/post/view?id=1&title=sample+post', $url); + + // todo: test showScriptName + + // pretty URL with rules + $manager = new UrlManager(array( + 'enablePrettyUrl' => true, + 'cache' => null, + 'rules' => array( + array( + 'pattern' => 'post//', + 'route' => 'post/view', + ), + ), + 'baseUrl' => '/', + )); + $url = $manager->createUrl('post/view', array('id' => 1, 'title' => 'sample post')); + $this->assertEquals('/post/1/sample+post', $url); + $url = $manager->createUrl('post/index', array('page' => 1)); + $this->assertEquals('/post/index?page=1', $url); + + // pretty URL with rules and suffix + $manager = new UrlManager(array( + 'enablePrettyUrl' => true, + 'cache' => null, + 'rules' => array( + array( + 'pattern' => 'post/<id>/<title>', + 'route' => 'post/view', + ), + ), + 'baseUrl' => '/', + 'suffix' => '.html', + )); + $url = $manager->createUrl('post/view', array('id' => 1, 'title' => 'sample post')); + $this->assertEquals('/post/1/sample+post.html', $url); + $url = $manager->createUrl('post/index', array('page' => 1)); + $this->assertEquals('/post/index.html?page=1', $url); + } + + public function testCreateAbsoluteUrl() + { + $manager = new UrlManager(array( + 'baseUrl' => '/', + 'hostInfo' => 'http://www.example.com', + 'cache' => null, + )); + $url = $manager->createAbsoluteUrl('post/view', array('id' => 1, 'title' => 'sample post')); + $this->assertEquals('http://www.example.com/?r=post/view&id=1&title=sample+post', $url); + } + + public function testParseRequest() + { + $manager = new UrlManager(array( + 'cache' => null, + )); + $request = new Request; + + // default setting without 'r' param + unset($_GET['r']); + $result = $manager->parseRequest($request); + $this->assertEquals(array('', array()), $result); + + // default setting with 'r' param + $_GET['r'] = 'site/index'; + $result = $manager->parseRequest($request); + $this->assertEquals(array('site/index', array()), $result); + + // default setting with 'r' param as an array + $_GET['r'] = array('site/index'); + $result = $manager->parseRequest($request); + $this->assertEquals(array('', array()), $result); + + // pretty URL without rules + $manager = new UrlManager(array( + 'enablePrettyUrl' => true, + 'cache' => null, + )); + // empty pathinfo + $request->pathInfo = ''; + $result = $manager->parseRequest($request); + $this->assertEquals(array('', array()), $result); + // normal pathinfo + $request->pathInfo = 'site/index'; + $result = $manager->parseRequest($request); + $this->assertEquals(array('site/index', array()), $result); + // pathinfo with module + $request->pathInfo = 'module/site/index'; + $result = $manager->parseRequest($request); + $this->assertEquals(array('module/site/index', array()), $result); + // pathinfo with trailing slashes + $request->pathInfo = 'module/site/index/'; + $result = $manager->parseRequest($request); + $this->assertEquals(array('module/site/index', array()), $result); + + // pretty URL rules + $manager = new UrlManager(array( + 'enablePrettyUrl' => true, + 'cache' => null, + 'rules' => array( + array( + 'pattern' => 'post/<id>/<title>', + 'route' => 'post/view', + ), + ), + )); + // matching pathinfo + $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 with trailing slashes + $request->pathInfo = 'post/123/this+is+sample/'; + $result = $manager->parseRequest($request); + $this->assertEquals(array('post/view', array('id' => '123', 'title' => 'this+is+sample')), $result); + // empty pathinfo + $request->pathInfo = ''; + $result = $manager->parseRequest($request); + $this->assertEquals(array('', array()), $result); + // normal pathinfo + $request->pathInfo = 'site/index'; + $result = $manager->parseRequest($request); + $this->assertEquals(array('site/index', array()), $result); + // pathinfo with module + $request->pathInfo = 'module/site/index'; + $result = $manager->parseRequest($request); + $this->assertEquals(array('module/site/index', array()), $result); + + // pretty URL rules + $manager = new UrlManager(array( + 'enablePrettyUrl' => true, + 'suffix' => '.html', + 'cache' => null, + 'rules' => array( + array( + 'pattern' => 'post/<id>/<title>', + 'route' => 'post/view', + ), + ), + )); + // matching pathinfo + $request->pathInfo = 'post/123/this+is+sample.html'; + $result = $manager->parseRequest($request); + $this->assertEquals(array('post/view', array('id' => '123', 'title' => 'this+is+sample')), $result); + // matching pathinfo without suffix + $request->pathInfo = 'post/123/this+is+sample'; + $result = $manager->parseRequest($request); + $this->assertFalse($result); + // empty pathinfo + $request->pathInfo = ''; + $result = $manager->parseRequest($request); + $this->assertEquals(array('', array()), $result); + // normal pathinfo + $request->pathInfo = 'site/index.html'; + $result = $manager->parseRequest($request); + $this->assertEquals(array('site/index', array()), $result); + // pathinfo without suffix + $request->pathInfo = 'site/index'; + $result = $manager->parseRequest($request); + $this->assertFalse($result); + } +} diff --git a/tests/unit/framework/web/UrlRuleTest.php b/tests/unit/framework/web/UrlRuleTest.php new file mode 100644 index 0000000..825199e --- /dev/null +++ b/tests/unit/framework/web/UrlRuleTest.php @@ -0,0 +1,615 @@ +<?php + +namespace yiiunit\framework\web; + +use yii\web\UrlManager; +use yii\web\UrlRule; +use yii\web\Request; + +class UrlRuleTest extends \yiiunit\TestCase +{ + public function testCreateUrl() + { + $manager = new UrlManager(array('cache' => null)); + $suites = $this->getTestsForCreateUrl(); + foreach ($suites as $i => $suite) { + list ($name, $config, $tests) = $suite; + $rule = new UrlRule($config); + foreach ($tests as $j => $test) { + list ($route, $params, $expected) = $test; + $url = $rule->createUrl($manager, $route, $params); + $this->assertEquals($expected, $url, "Test#$i-$j: $name"); + } + } + } + + public function testParseRequest() + { + $manager = new UrlManager(array('cache' => null)); + $request = new Request; + $suites = $this->getTestsForParseRequest(); + foreach ($suites as $i => $suite) { + list ($name, $config, $tests) = $suite; + $rule = new UrlRule($config); + foreach ($tests as $j => $test) { + $request->pathInfo = $test[0]; + $route = $test[1]; + $params = isset($test[2]) ? $test[2] : array(); + $result = $rule->parseRequest($manager, $request); + if ($route === false) { + $this->assertFalse($result, "Test#$i-$j: $name"); + } else { + $this->assertEquals(array($route, $params), $result, "Test#$i-$j: $name"); + } + } + } + } + + protected function getTestsForCreateUrl() + { + // structure of each test + // message for the test + // config for the URL rule + // list of inputs and outputs + // route + // params + // expected output + return array( + array( + 'empty pattern', + array( + 'pattern' => '', + 'route' => 'post/index', + ), + array( + array('post/index', array(), ''), + array('comment/index', array(), false), + array('post/index', array('page' => 1), '?page=1'), + ), + ), + array( + 'without param', + array( + 'pattern' => 'posts', + 'route' => 'post/index', + ), + array( + array('post/index', array(), 'posts'), + array('comment/index', array(), false), + array('post/index', array('page' => 1), 'posts?page=1'), + ), + ), + array( + 'parsing only', + array( + 'pattern' => 'posts', + 'route' => 'post/index', + 'mode' => UrlRule::PARSING_ONLY, + ), + array( + array('post/index', array(), false), + ), + ), + array( + 'with param', + array( + 'pattern' => 'post/<page>', + 'route' => 'post/index', + ), + array( + array('post/index', array(), false), + array('comment/index', array(), false), + array('post/index', array('page' => 1), 'post/1'), + array('post/index', array('page' => 1, 'tag' => 'a'), 'post/1?tag=a'), + ), + ), + array( + 'with param requirement', + array( + 'pattern' => 'post/<page:\d+>', + 'route' => 'post/index', + ), + array( + array('post/index', array('page' => 'abc'), false), + array('post/index', array('page' => 1), 'post/1'), + array('post/index', array('page' => 1, 'tag' => 'a'), 'post/1?tag=a'), + ), + ), + array( + 'with multiple params', + array( + 'pattern' => 'post/<page:\d+>-<tag>', + 'route' => 'post/index', + ), + array( + array('post/index', array('page' => '1abc'), false), + array('post/index', array('page' => 1), false), + array('post/index', array('page' => 1, 'tag' => 'a'), 'post/1-a'), + ), + ), + array( + 'with optional param', + array( + 'pattern' => 'post/<page:\d+>/<tag>', + 'route' => 'post/index', + 'defaults' => array('page' => 1), + ), + array( + array('post/index', array('page' => 1), false), + array('post/index', array('page' => '1abc', 'tag' => 'a'), false), + array('post/index', array('page' => 1, 'tag' => 'a'), 'post/a'), + array('post/index', array('page' => 2, 'tag' => 'a'), 'post/2/a'), + ), + ), + array( + 'with optional param not in pattern', + array( + 'pattern' => 'post/<tag>', + 'route' => 'post/index', + 'defaults' => array('page' => 1), + ), + array( + array('post/index', array('page' => 1), false), + array('post/index', array('page' => '1abc', 'tag' => 'a'), false), + array('post/index', array('page' => 2, 'tag' => 'a'), false), + array('post/index', array('page' => 1, 'tag' => 'a'), 'post/a'), + ), + ), + array( + 'multiple optional params', + array( + 'pattern' => 'post/<page:\d+>/<tag>/<sort:yes|no>', + 'route' => 'post/index', + 'defaults' => array('page' => 1, 'sort' => 'yes'), + ), + array( + array('post/index', array('page' => 1), false), + array('post/index', array('page' => '1abc', 'tag' => 'a'), false), + array('post/index', array('page' => 1, 'tag' => 'a', 'sort' => 'YES'), false), + array('post/index', array('page' => 1, 'tag' => 'a', 'sort' => 'yes'), 'post/a'), + array('post/index', array('page' => 2, 'tag' => 'a', 'sort' => 'yes'), 'post/2/a'), + array('post/index', array('page' => 2, 'tag' => 'a', 'sort' => 'no'), 'post/2/a/no'), + array('post/index', array('page' => 1, 'tag' => 'a', 'sort' => 'no'), 'post/a/no'), + ), + ), + array( + 'optional param and required param separated by dashes', + array( + 'pattern' => 'post/<page:\d+>-<tag>', + 'route' => 'post/index', + 'defaults' => array('page' => 1), + ), + array( + array('post/index', array('page' => 1), false), + array('post/index', array('page' => '1abc', 'tag' => 'a'), false), + array('post/index', array('page' => 1, 'tag' => 'a'), 'post/-a'), + array('post/index', array('page' => 2, 'tag' => 'a'), 'post/2-a'), + ), + ), + array( + 'optional param at the end', + array( + 'pattern' => 'post/<tag>/<page:\d+>', + 'route' => 'post/index', + 'defaults' => array('page' => 1), + ), + array( + array('post/index', array('page' => 1), false), + array('post/index', array('page' => '1abc', 'tag' => 'a'), false), + array('post/index', array('page' => 1, 'tag' => 'a'), 'post/a'), + array('post/index', array('page' => 2, 'tag' => 'a'), 'post/a/2'), + ), + ), + array( + 'consecutive optional params', + array( + 'pattern' => 'post/<page:\d+>/<tag>', + 'route' => 'post/index', + 'defaults' => array('page' => 1, 'tag' => 'a'), + ), + array( + array('post/index', array('page' => 1), false), + array('post/index', array('page' => '1abc', 'tag' => 'a'), false), + array('post/index', array('page' => 1, 'tag' => 'a'), 'post'), + array('post/index', array('page' => 2, 'tag' => 'a'), 'post/2'), + array('post/index', array('page' => 1, 'tag' => 'b'), 'post/b'), + array('post/index', array('page' => 2, 'tag' => 'b'), 'post/2/b'), + ), + ), + array( + 'consecutive optional params separated by dash', + array( + 'pattern' => 'post/<page:\d+>-<tag>', + 'route' => 'post/index', + 'defaults' => array('page' => 1, 'tag' => 'a'), + ), + array( + array('post/index', array('page' => 1), false), + array('post/index', array('page' => '1abc', 'tag' => 'a'), false), + array('post/index', array('page' => 1, 'tag' => 'a'), 'post/-'), + array('post/index', array('page' => 1, 'tag' => 'b'), 'post/-b'), + array('post/index', array('page' => 2, 'tag' => 'a'), 'post/2-'), + array('post/index', array('page' => 2, 'tag' => 'b'), 'post/2-b'), + ), + ), + array( + 'route has parameters', + array( + 'pattern' => '<controller>/<action>', + 'route' => '<controller>/<action>', + 'defaults' => array(), + ), + array( + array('post/index', array('page' => 1), 'post/index?page=1'), + array('module/post/index', array(), false), + ), + ), + array( + 'route has parameters with regex', + array( + 'pattern' => '<controller:post|comment>/<action>', + 'route' => '<controller>/<action>', + 'defaults' => array(), + ), + array( + array('post/index', array('page' => 1), 'post/index?page=1'), + array('comment/index', array('page' => 1), 'comment/index?page=1'), + array('test/index', array('page' => 1), false), + array('post', array(), false), + array('module/post/index', array(), false), + array('post/index', array('controller' => 'comment'), 'post/index?controller=comment'), + ), + ), + array( + 'route has default parameter', + array( + 'pattern' => '<controller:post|comment>/<action>', + 'route' => '<controller>/<action>', + 'defaults' => array('action' => 'index'), + ), + array( + array('post/view', array('page' => 1), 'post/view?page=1'), + array('comment/view', array('page' => 1), 'comment/view?page=1'), + array('test/view', array('page' => 1), false), + array('test/index', array('page' => 1), false), + array('post/index', array('page' => 1), 'post?page=1'), + ), + ), + array( + 'empty pattern with suffix', + array( + 'pattern' => '', + 'route' => 'post/index', + 'suffix' => '.html', + ), + array( + array('post/index', array(), ''), + array('comment/index', array(), false), + array('post/index', array('page' => 1), '?page=1'), + ), + ), + array( + 'regular pattern with suffix', + array( + 'pattern' => 'posts', + 'route' => 'post/index', + 'suffix' => '.html', + ), + array( + array('post/index', array(), 'posts.html'), + array('comment/index', array(), false), + array('post/index', array('page' => 1), 'posts.html?page=1'), + ), + ), + array( + 'empty pattern with slash suffix', + array( + 'pattern' => '', + 'route' => 'post/index', + 'suffix' => '/', + ), + array( + array('post/index', array(), ''), + array('comment/index', array(), false), + array('post/index', array('page' => 1), '?page=1'), + ), + ), + array( + 'regular pattern with slash suffix', + array( + 'pattern' => 'posts', + 'route' => 'post/index', + 'suffix' => '/', + ), + array( + array('post/index', array(), 'posts/'), + array('comment/index', array(), false), + array('post/index', array('page' => 1), 'posts/?page=1'), + ), + ), + ); + } + + protected function getTestsForParseRequest() + { + // structure of each test + // message for the test + // config for the URL rule + // list of inputs and outputs + // pathInfo + // expected route, or false if the rule doesn't apply + // expected params, or not set if empty + return array( + array( + 'empty pattern', + array( + 'pattern' => '', + 'route' => 'post/index', + ), + array( + array('', 'post/index'), + array('a', false), + ), + ), + array( + 'without param', + array( + 'pattern' => 'posts', + 'route' => 'post/index', + ), + array( + array('posts', 'post/index'), + array('a', false), + ), + ), + array( + 'creation only', + array( + 'pattern' => 'posts', + 'route' => 'post/index', + 'mode' => UrlRule::CREATION_ONLY, + ), + array( + array('posts', false), + ), + ), + array( + 'with param', + array( + 'pattern' => 'post/<page>', + 'route' => 'post/index', + ), + array( + array('post/1', 'post/index', array('page' => '1')), + array('post/a', 'post/index', array('page' => 'a')), + array('post', false), + array('posts', false), + ), + ), + array( + 'with param requirement', + array( + 'pattern' => 'post/<page:\d+>', + 'route' => 'post/index', + ), + array( + array('post/1', 'post/index', array('page' => '1')), + array('post/a', false), + array('post/1/a', false), + ), + ), + array( + 'with multiple params', + array( + 'pattern' => 'post/<page:\d+>-<tag>', + 'route' => 'post/index', + ), + array( + array('post/1-a', 'post/index', array('page' => '1', 'tag' => 'a')), + array('post/a', false), + array('post/1', false), + array('post/1/a', false), + ), + ), + array( + 'with optional param', + array( + 'pattern' => 'post/<page:\d+>/<tag>', + 'route' => 'post/index', + 'defaults' => array('page' => 1), + ), + array( + array('post/1/a', 'post/index', array('page' => '1', 'tag' => 'a')), + array('post/2/a', 'post/index', array('page' => '2', 'tag' => 'a')), + array('post/a', 'post/index', array('page' => '1', 'tag' => 'a')), + array('post/1', 'post/index', array('page' => '1', 'tag' => '1')), + ), + ), + array( + 'with optional param not in pattern', + array( + 'pattern' => 'post/<tag>', + 'route' => 'post/index', + 'defaults' => array('page' => 1), + ), + array( + array('post/a', 'post/index', array('page' => '1', 'tag' => 'a')), + array('post/1', 'post/index', array('page' => '1', 'tag' => '1')), + array('post', false), + ), + ), + array( + 'multiple optional params', + array( + 'pattern' => 'post/<page:\d+>/<tag>/<sort:yes|no>', + 'route' => 'post/index', + 'defaults' => array('page' => 1, 'sort' => 'yes'), + ), + array( + array('post/1/a/yes', 'post/index', array('page' => '1', 'tag' => 'a', 'sort' => 'yes')), + array('post/2/a/no', 'post/index', array('page' => '2', 'tag' => 'a', 'sort' => 'no')), + array('post/2/a', 'post/index', array('page' => '2', 'tag' => 'a', 'sort' => 'yes')), + array('post/a/no', 'post/index', array('page' => '1', 'tag' => 'a', 'sort' => 'no')), + array('post/a', 'post/index', array('page' => '1', 'tag' => 'a', 'sort' => 'yes')), + array('post', false), + ), + ), + array( + 'optional param and required param separated by dashes', + array( + 'pattern' => 'post/<page:\d+>-<tag>', + 'route' => 'post/index', + 'defaults' => array('page' => 1), + ), + array( + array('post/1-a', 'post/index', array('page' => '1', 'tag' => 'a')), + array('post/2-a', 'post/index', array('page' => '2', 'tag' => 'a')), + array('post/-a', 'post/index', array('page' => '1', 'tag' => 'a')), + array('post/a', false), + array('post-a', false), + ), + ), + array( + 'optional param at the end', + array( + 'pattern' => 'post/<tag>/<page:\d+>', + 'route' => 'post/index', + 'defaults' => array('page' => 1), + ), + array( + array('post/a/1', 'post/index', array('page' => '1', 'tag' => 'a')), + array('post/a/2', 'post/index', array('page' => '2', 'tag' => 'a')), + array('post/a', 'post/index', array('page' => '1', 'tag' => 'a')), + array('post/2', 'post/index', array('page' => '1', 'tag' => '2')), + array('post', false), + ), + ), + array( + 'consecutive optional params', + array( + 'pattern' => 'post/<page:\d+>/<tag>', + 'route' => 'post/index', + 'defaults' => array('page' => 1, 'tag' => 'a'), + ), + array( + array('post/2/b', 'post/index', array('page' => '2', 'tag' => 'b')), + array('post/2', 'post/index', array('page' => '2', 'tag' => 'a')), + array('post', 'post/index', array('page' => '1', 'tag' => 'a')), + array('post/b', 'post/index', array('page' => '1', 'tag' => 'b')), + array('post//b', false), + ), + ), + array( + 'consecutive optional params separated by dash', + array( + 'pattern' => 'post/<page:\d+>-<tag>', + 'route' => 'post/index', + 'defaults' => array('page' => 1, 'tag' => 'a'), + ), + array( + array('post/2-b', 'post/index', array('page' => '2', 'tag' => 'b')), + array('post/2-', 'post/index', array('page' => '2', 'tag' => 'a')), + array('post/-b', 'post/index', array('page' => '1', 'tag' => 'b')), + array('post/-', 'post/index', array('page' => '1', 'tag' => 'a')), + array('post', false), + ), + ), + array( + 'route has parameters', + array( + 'pattern' => '<controller>/<action>', + 'route' => '<controller>/<action>', + 'defaults' => array(), + ), + array( + array('post/index', 'post/index'), + array('module/post/index', false), + ), + ), + array( + 'route has parameters with regex', + array( + 'pattern' => '<controller:post|comment>/<action>', + 'route' => '<controller>/<action>', + 'defaults' => array(), + ), + array( + array('post/index', 'post/index'), + array('comment/index', 'comment/index'), + array('test/index', false), + array('post', false), + array('module/post/index', false), + ), + ), + array( + 'route has default parameter', + array( + 'pattern' => '<controller:post|comment>/<action>', + 'route' => '<controller>/<action>', + 'defaults' => array('action' => 'index'), + ), + array( + array('post/view', 'post/view'), + array('comment/view', 'comment/view'), + array('test/view', false), + array('post', 'post/index'), + array('posts', false), + array('test', false), + array('index', false), + ), + ), + array( + 'empty pattern with suffix', + array( + 'pattern' => '', + 'route' => 'post/index', + 'suffix' => '.html', + ), + array( + array('', 'post/index'), + array('.html', false), + array('a.html', false), + ), + ), + array( + 'regular pattern with suffix', + array( + 'pattern' => 'posts', + 'route' => 'post/index', + 'suffix' => '.html', + ), + array( + array('posts.html', 'post/index'), + array('posts', false), + array('posts.HTML', false), + array('a.html', false), + array('a', false), + ), + ), + array( + 'empty pattern with slash suffix', + array( + 'pattern' => '', + 'route' => 'post/index', + 'suffix' => '/', + ), + array( + array('', 'post/index'), + array('a', false), + ), + ), + array( + 'regular pattern with slash suffix', + array( + 'pattern' => 'posts', + 'route' => 'post/index', + 'suffix' => '/', + ), + array( + array('posts', 'post/index'), + array('a', false), + ), + ), + ); + } +} diff --git a/tests/unit/runtime/.gitignore b/tests/unit/runtime/.gitignore new file mode 100644 index 0000000..72e8ffc --- /dev/null +++ b/tests/unit/runtime/.gitignore @@ -0,0 +1 @@ +* diff --git a/tests/web/app/assets/.gitignore b/tests/web/app/assets/.gitignore new file mode 100644 index 0000000..72e8ffc --- /dev/null +++ b/tests/web/app/assets/.gitignore @@ -0,0 +1 @@ +* diff --git a/tests/web/app/index.php b/tests/web/app/index.php new file mode 100644 index 0000000..4cfa1ab --- /dev/null +++ b/tests/web/app/index.php @@ -0,0 +1,6 @@ +<?php + +require(__DIR__ . '/../../../framework/yii.php'); + +$application = new yii\web\Application('test', __DIR__ . '/protected'); +$application->run(); diff --git a/tests/web/app/protected/config/main.php b/tests/web/app/protected/config/main.php new file mode 100644 index 0000000..eed6d54 --- /dev/null +++ b/tests/web/app/protected/config/main.php @@ -0,0 +1,3 @@ +<?php + +return array(); \ No newline at end of file diff --git a/tests/web/app/protected/controllers/SiteController.php b/tests/web/app/protected/controllers/SiteController.php new file mode 100644 index 0000000..050bf90 --- /dev/null +++ b/tests/web/app/protected/controllers/SiteController.php @@ -0,0 +1,30 @@ +<?php + +use yii\helpers\Html; + +class DefaultController extends \yii\web\Controller +{ + public function actionIndex() + { + echo 'hello world'; + } + + public function actionForm() + { + echo Html::beginForm(); + echo Html::checkboxList('test', array( + 'value 1' => 'item 1', + 'value 2' => 'item 2', + 'value 3' => 'item 3', + ), isset($_POST['test']) ? $_POST['test'] : null, + function ($index, $label, $name, $value, $checked) { + return Html::label( + $label . ' ' . Html::checkbox($name, $value, $checked), + null, array('class' => 'inline checkbox') + ); + }); + echo Html::submitButton(); + echo Html::endForm(); + print_r($_POST); + } +} \ No newline at end of file diff --git a/tests/web/app/protected/runtime/.gitignore b/tests/web/app/protected/runtime/.gitignore new file mode 100644 index 0000000..72e8ffc --- /dev/null +++ b/tests/web/app/protected/runtime/.gitignore @@ -0,0 +1 @@ +* diff --git a/tests/web/app/protected/views/site/index.php b/tests/web/app/protected/views/site/index.php new file mode 100644 index 0000000..5decb56 --- /dev/null +++ b/tests/web/app/protected/views/site/index.php @@ -0,0 +1,8 @@ +<?php +/** + * Created by JetBrains PhpStorm. + * User: qiang + * Date: 3/16/13 + * Time: 10:41 AM + * To change this template use File | Settings | File Templates. + */ \ No newline at end of file diff --git a/todo.md b/todo.md index 60e37c5..f66d3c1 100644 --- a/todo.md +++ b/todo.md @@ -1,33 +1,20 @@ -- db - * pgsql, sql server, oracle, db2 drivers - * unit tests on different DB drivers - * document-based (should allow storage-specific methods additionally to generic ones) - * mongodb (put it under framework/db/mongodb) - * key-value-based (should allow storage-specific methods additionally to generic ones) - * redis (put it under framework/db/redis or perhaps framework/caching?) -- base - * TwigViewRenderer - * SmartyViewRenderer -- logging - * WebTarget (TBD after web is in place): should consider using javascript and make it into a toolbar - * ProfileTarget (TBD after web is in place): should consider using javascript and make it into a toolbar - * unit tests - caching - * a console command to clear cached data - * unit tests + * dependency unit tests - validators + * Refactor validators to add validateValue() for every validator, if possible. Check if value is an array. * FileValidator: depends on CUploadedFile * CaptchaValidator: depends on CaptchaAction * DateValidator: should we use CDateTimeParser, or simply use strtotime()? * CompareValidator::clientValidateAttribute(): depends on CHtml::activeId() +memo + * Minimal PHP version required: 5.3.7 (http://www.php.net/manual/en/function.crypt.php) --- - base * module - Module should be able to define its own configuration including routes. Application should be able to overwrite it. * application - * security - built-in console commands + api doc builder * support for markdown syntax @@ -35,12 +22,10 @@ * consider to be released as a separate tool for user app docs - i18n * consider using PHP built-in support and data - * message translations, choice format * formatting: number and date * parsing?? * make dates/date patterns uniform application-wide including JUI, formats etc. - helpers - * array * image * string * file @@ -53,8 +38,6 @@ * move generation API out of gii, provide yiic commands to use it. Use same templates for gii/yiic. * i18n variant of templates * allow to generate module-specific CRUD -- markup and HTML helpers - * use HTML5 instead of XHTML - assets * ability to manage scripts order (store these in a vector?) * http://ryanbigg.com/guides/asset_pipeline.html, http://guides.rubyonrails.org/asset_pipeline.html, use content hash instead of mtime + directory hash.