diff --git a/framework/YiiBase.php b/framework/YiiBase.php index 9d3698b..d01648c 100644 --- a/framework/YiiBase.php +++ b/framework/YiiBase.php @@ -8,9 +8,8 @@ */ use yii\base\Exception; -use yii\logging\Logger; -use yii\base\InvalidCallException; use yii\base\InvalidConfigException; +use yii\logging\Logger; /** * Gets the application start timestamp. @@ -30,6 +29,11 @@ defined('YII_TRACE_LEVEL') or define('YII_TRACE_LEVEL', 0); * This constant defines the framework installation directory. */ defined('YII_PATH') or define('YII_PATH', __DIR__); +/** + * This constant defines whether error handling should be enabled. Defaults to true. + */ +defined('YII_ENABLE_ERROR_HANDLER') or define('YII_ENABLE_ERROR_HANDLER', true); + /** * YiiBase is the core helper class for the Yii framework. @@ -121,8 +125,8 @@ class YiiBase * * To import a class or a directory, one can use either path alias or class name (can be namespaced): * - * - `@app/components/GoogleMap`: importing the `GoogleMap` class with a path alias; - * - `@app/components/*`: importing the whole `components` directory with a path alias; + * - `@application/components/GoogleMap`: importing the `GoogleMap` class with a path alias; + * - `@application/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. * @@ -189,14 +193,14 @@ class YiiBase * * Note, this method does not ensure the existence of the resulting path. * @param string $alias alias - * @param boolean $throwException whether to throw exception if the alias is invalid. * @return string|boolean path corresponding to the alias, false if the root alias is not previously registered. - * @throws Exception if the alias is invalid and $throwException is true. * @see setAlias */ - public static function getAlias($alias, $throwException = false) + public static function getAlias($alias) { - if (isset(self::$aliases[$alias])) { + if (!is_string($alias)) { + return false; + } elseif (isset(self::$aliases[$alias])) { return self::$aliases[$alias]; } elseif ($alias === '' || $alias[0] !== '@') { // not an alias return $alias; @@ -206,11 +210,7 @@ class YiiBase return self::$aliases[$alias] = self::$aliases[$rootAlias] . substr($alias, $pos); } } - if ($throwException) { - throw new Exception("Invalid path alias: $alias"); - } else { - return false; - } + return false; } /** @@ -322,12 +322,12 @@ class YiiBase * the class. For example, * * - `\app\components\GoogleMap`: fully-qualified namespaced class. - * - `@app/components/GoogleMap`: an alias + * - `@application/components/GoogleMap`: an alias * * Below are some usage examples: * * ~~~ - * $object = \Yii::createObject('@app/components/GoogleMap'); + * $object = \Yii::createObject('@application/components/GoogleMap'); * $object = \Yii::createObject(array( * 'class' => '\app\components\GoogleMap', * 'apiKey' => 'xyz', @@ -361,7 +361,7 @@ class YiiBase $class = $config['class']; unset($config['class']); } else { - throw new InvalidCallException('Object configuration must be an array containing a "class" element.'); + throw new InvalidConfigException('Object configuration must be an array containing a "class" element.'); } if (!class_exists($class, false)) { diff --git a/framework/base/Action.php b/framework/base/Action.php index 1948c9d..8d4ec5a 100644 --- a/framework/base/Action.php +++ b/framework/base/Action.php @@ -9,8 +9,6 @@ namespace yii\base; -use yii\util\ReflectionHelper; - /** * Action is the base class for all controller action classes. * @@ -21,6 +19,14 @@ use yii\util\ReflectionHelper; * will be invoked by the controller when the action is requested. * The `run()` method can have parameters which will be filled up * with user input values automatically according to their names. + * For example, if the `run()` method is declared as follows: + * + * ~~~ + * public function run($id, $type = 'book') { ... } + * ~~~ + * + * And the parameters provided for the action are: `array('id' => 1)`. + * Then the `run()` method will be invoked as `run(1)` automatically. * * @author Qiang Xue * @since 2.0 @@ -37,6 +43,7 @@ class Action extends Component public $controller; /** + * Constructor. * @param string $id the ID of this action * @param Controller $controller the controller that owns this action * @param array $config name-value pairs that will be used to initialize the object properties @@ -51,20 +58,45 @@ class Action extends Component /** * Runs this action with the specified parameters. * This method is mainly invoked by the controller. - * @param array $params action parameters + * @param array $params the parameters to be bound to the action's run() method. * @return integer the exit status (0 means normal, non-zero means abnormal). + * @throws InvalidConfigException if the action class does not have a run() method */ public function runWithParams($params) { - try { - $ps = ReflectionHelper::extractMethodParams($this, 'run', $params); - } catch (Exception $e) { - $this->controller->invalidActionParams($this, $e); - return 1; + if (!method_exists($this, 'run')) { + throw new InvalidConfigException(get_class($this) . ' must define a "run()" method.'); } - if ($params !== $ps) { - $this->controller->extraActionParams($this, $ps, $params); + $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; + } } - return (int)call_user_func_array(array($this, 'run'), $ps); + $this->controller->validateActionParams($this, $missing, $params); + return $args; } } diff --git a/framework/base/ActionEvent.php b/framework/base/ActionEvent.php index 1b3e07d..ee945a8 100644 --- a/framework/base/ActionEvent.php +++ b/framework/base/ActionEvent.php @@ -12,8 +12,7 @@ namespace yii\base; /** * ActionEvent represents the event parameter used for an action event. * - * By setting the [[isValid]] property, one may control whether to continue the life cycle of - * the action currently being executed. + * By setting the [[isValid]] property, one may control whether to continue running the action. * * @author Qiang Xue * @since 2.0 @@ -25,7 +24,7 @@ class ActionEvent extends Event */ public $action; /** - * @var boolean whether the action is in valid state and its life cycle should proceed. + * @var boolean whether to continue running the action. */ public $isValid = true; @@ -34,7 +33,7 @@ class ActionEvent extends Event * @param Action $action the action associated with this action event. * @param array $config name-value pairs that will be used to initialize the object properties */ - public function __construct(Action $action, $config = array()) + public function __construct($action, $config = array()) { $this->action = $action; parent::__construct($config); diff --git a/framework/base/ActionFilter.php b/framework/base/ActionFilter.php deleted file mode 100644 index 9cb5331..0000000 --- a/framework/base/ActionFilter.php +++ /dev/null @@ -1,90 +0,0 @@ - - * @since 2.0 - */ -class ActionFilter extends Behavior -{ - /** - * @var Controller the owner of this behavior. For action filters, this should be a controller object. - */ - public $owner; - /** - * @var array IDs of actions that this filter applies to. - * If this property is empty or not set, it means this filter applies to all actions. - * Note that if an action appears in [[except]], the filter will not apply to this action, even - * if the action also appears in [[only]]. - * @see exception - */ - public $only; - /** - * @var array IDs of actions that this filter does NOT apply to. - */ - public $except; - - public function init() - { - $this->owner->on('authorize', array($this, 'handleEvent')); - $this->owner->on('beforeAction', array($this, 'handleEvent')); - $this->owner->on('beforeRender', array($this, 'handleEvent')); - $this->owner->getEventHandlers('afterRender')->insertAt(0, array($this, 'handleEvent')); - $this->owner->getEventHandlers('afterAction')->insertAt(0, array($this, 'handleEvent')); - } - - public function authorize($event) - { - } - - public function beforeAction($event) - { - } - - public function beforeRender($event) - { - } - - public function afterRender($event) - { - } - - public function afterAction($event) - { - } - - public function handleEvent($event) - { - if ($this->applyTo($event->action)) { - $this->{$event->name}($event); - } - } - - public function applyTo(Action $action) - { - return (empty($this->only) || in_array($action->id, $this->only, false) !== false) - && (empty($this->except) || in_array($action->id, $this->except, false) === false); - } -} \ No newline at end of file diff --git a/framework/base/Application.php b/framework/base/Application.php index 2e92aab..f64e352 100644 --- a/framework/base/Application.php +++ b/framework/base/Application.php @@ -9,8 +9,8 @@ namespace yii\base; -use yii\base\InvalidCallException; -use yii\util\StringHelper; +use Yii; +use yii\util\FileHelper; /** * Application is the base class for all application classes. @@ -36,7 +36,7 @@ use yii\util\StringHelper; * Yii framework messages. This application component is dynamically loaded when needed. * * - * Application will undergo the following lifecycles when processing a user request: + * Application will undergo the following life cycles when processing a user request: *
    *
  1. load application configuration;
  2. *
  3. set up class autoloader and error handling;
  4. @@ -49,28 +49,6 @@ use yii\util\StringHelper; * Starting from lifecycle 3, if a PHP error or an uncaught exception occurs, * the application will switch to its error handling logic and jump to step 6 afterwards. * - * @property string $basePath Returns the root path of the application. - * @property CCache $cache Returns the cache component. - * @property CPhpMessageSource $coreMessages Returns the core message translations. - * @property CDateFormatter $dateFormatter Returns the locale-dependent date formatter. - * @property \yii\db\Connection $db Returns the database connection component. - * @property CErrorHandler $errorHandler Returns the error handler component. - * @property string $extensionPath Returns the root directory that holds all third-party extensions. - * @property string $id Returns the unique identifier for the application. - * @property string $language Returns the language that the user is using and the application should be targeted to. - * @property CLocale $locale Returns the locale instance. - * @property string $localeDataPath Returns the directory that contains the locale data. - * @property CMessageSource $messages Returns the application message translations component. - * @property CNumberFormatter $numberFormatter The locale-dependent number formatter. - * @property CHttpRequest $request Returns the request component. - * @property string $runtimePath Returns the directory that stores runtime files. - * @property CSecurityManager $securityManager Returns the security manager component. - * @property CStatePersister $statePersister Returns the state persister component. - * @property string $timeZone Returns the time zone used by this application. - * @property UrlManager $urlManager Returns the URL manager component. - * @property string $baseUrl Returns the relative URL for the application - * @property string $homeUrl the homepage URL - * * @author Qiang Xue * @since 2.0 */ @@ -97,11 +75,9 @@ class Application extends Module */ public $sourceLanguage = 'en_us'; /** - * @var array IDs of application components that need to be loaded when the application starts. - * The default value is `array('errorHandler')`, which loads the [[errorHandler]] component - * to ensure errors and exceptions can be handled nicely. + * @var array IDs of the components that need to be loaded when the application starts. */ - public $preload = array('errorHandler'); + public $preload = array(); /** * @var Controller the currently active controller instance */ @@ -128,12 +104,19 @@ class Application extends Module */ public function __construct($id, $basePath, $config = array()) { - \Yii::$application = $this; + Yii::$application = $this; $this->id = $id; $this->setBasePath($basePath); + + if (YII_ENABLE_ERROR_HANDLER) { + set_exception_handler(array($this, 'handleException')); + set_error_handler(array($this, 'handleError'), error_reporting()); + } + $this->registerDefaultAliases(); $this->registerCoreComponents(); - parent::__construct($id, $this, $config); + + Component::__construct($config); } /** @@ -203,28 +186,6 @@ class Application extends Module } /** - * Runs a controller with the given route and parameters. - * @param string $route the route (e.g. `post/create`) - * @param array $params the parameters to be passed to the controller action - * @return integer the exit status (0 means normal, non-zero values mean abnormal) - * @throws InvalidRequestException if the route cannot be resolved into a controller - */ - public function runController($route, $params = array()) - { - $result = $this->createController($route); - if ($result === false) { - throw new InvalidRequestException(\Yii::t('yii', 'Unable to resolve the request.')); - } - /** @var $controller Controller */ - list($controller, $action) = $result; - $priorController = $this->controller; - $this->controller = $controller; - $status = $controller->run($action, $params); - $this->controller = $priorController; - return $status; - } - - /** * Returns the directory that stores runtime files. * @return string the directory that stores runtime files. Defaults to 'protected/runtime'. */ @@ -239,15 +200,15 @@ class Application extends Module /** * Sets the directory that stores runtime files. * @param string $path the directory that stores runtime files. - * @throws InvalidCallException if the directory does not exist or is not writable + * @throws InvalidConfigException if the directory does not exist or is not writable */ public function setRuntimePath($path) { - $p = \Yii::getAlias($path); - if ($p === false || !is_dir($p) || !is_writable($path)) { - throw new InvalidCallException("Application runtime path \"$path\" is invalid. Please make sure it is a directory writable by the Web server process."); - } else { + $p = FileHelper::ensureDirectory($path); + if (is_writable($p)) { $this->_runtimePath = $p; + } else { + throw new InvalidConfigException("Runtime path must be writable by the Web server process: $path"); } } @@ -296,34 +257,61 @@ class Application extends Module date_default_timezone_set($value); } - /** - * Returns the locale instance. - * @param string $localeID the locale ID (e.g. en_US). If null, the {@link getLanguage application language ID} will be used. - * @return CLocale the locale instance - */ - public function getLocale($localeID = null) - { - return CLocale::getInstance($localeID === null ? $this->getLanguage() : $localeID); - } - - /** - * @return CNumberFormatter the locale-dependent number formatter. - * The current {@link getLocale application locale} will be used. - */ - public function getNumberFormatter() - { - return $this->getLocale()->getNumberFormatter(); - } - - /** - * Returns the locale-dependent date formatter. - * @return CDateFormatter the locale-dependent date formatter. - * The current {@link getLocale application locale} will be used. - */ - public function getDateFormatter() - { - return $this->getLocale()->getDateFormatter(); - } + // /** + // * Returns the security manager component. + // * @return SecurityManager the security manager application component. + // */ + // public function getSecurityManager() + // { + // return $this->getComponent('securityManager'); + // } + // + // /** + // * Returns the locale instance. + // * @param string $localeID the locale ID (e.g. en_US). If null, the {@link getLanguage application language ID} will be used. + // * @return CLocale the locale instance + // */ + // public function getLocale($localeID = null) + // { + // return CLocale::getInstance($localeID === null ? $this->getLanguage() : $localeID); + // } + // + // /** + // * @return CNumberFormatter the locale-dependent number formatter. + // * The current {@link getLocale application locale} will be used. + // */ + // public function getNumberFormatter() + // { + // return $this->getLocale()->getNumberFormatter(); + // } + // + // /** + // * Returns the locale-dependent date formatter. + // * @return CDateFormatter the locale-dependent date formatter. + // * The current {@link getLocale application locale} will be used. + // */ + // public function getDateFormatter() + // { + // 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. @@ -353,15 +341,6 @@ class Application extends Module } /** - * Returns the security manager component. - * @return SecurityManager the security manager application component. - */ - public function getSecurityManager() - { - return $this->getComponent('securityManager'); - } - - /** * Returns the cache component. * @return \yii\caching\Cache the cache application component. Null if the component is not enabled. */ @@ -371,24 +350,6 @@ class Application extends Module } /** - * 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 request component. * @return Request the request component */ @@ -402,9 +363,9 @@ class Application extends Module */ public function registerDefaultAliases() { - \Yii::$aliases['@application'] = $this->getBasePath(); - \Yii::$aliases['@entry'] = dirname($_SERVER['SCRIPT_FILENAME']); - \Yii::$aliases['@www'] = ''; + Yii::$aliases['@application'] = $this->getBasePath(); + Yii::$aliases['@entry'] = dirname($_SERVER['SCRIPT_FILENAME']); + Yii::$aliases['@www'] = ''; } /** @@ -417,15 +378,6 @@ class Application extends Module 'errorHandler' => array( 'class' => 'yii\base\ErrorHandler', ), - 'request' => array( - 'class' => 'yii\base\Request', - ), - 'response' => array( - 'class' => 'yii\base\Response', - ), - 'format' => array( - 'class' => 'yii\base\Formatter', - ), 'coreMessages' => array( 'class' => 'yii\i18n\PhpMessageSource', 'language' => 'en_us', @@ -444,124 +396,88 @@ class Application extends Module } /** - * Performs a controller action specified by a route. - * This method parses the specified route and creates the corresponding controller and action - * instances under the context of the specified module. It then runs the created action - * with the given parameters. - * @param string $route the route that specifies the action. - * @param array $params the parameters to be passed to the action - * @param Module $module the module which serves as the context of the route - * @return integer the action - * @throws InvalidConfigException if the module's defaultRoute is empty or the controller's defaultAction is empty - * @throws InvalidRequestException if the requested route cannot be resolved into an action successfully + * Handles PHP execution errors such as warnings, notices. + * + * This method is used as a PHP error handler. It will simply raise an `ErrorException`. + * + * @param integer $code the level of the error raised + * @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 */ - public function runAction($route, $params = array(), $module = null) + public function handleError($code, $message, $file, $line) { - if ($module === null) { - $module = $this; - } - $route = trim($route, '/'); - if ($route === '') { - $route = trim($module->defaultRoute, '/'); - if ($route == '') { - throw new InvalidConfigException(get_class($module) . '::defaultRoute cannot be empty.'); - } - } - if (($pos = strpos($route, '/')) !== false) { - $id = substr($route, 0, $pos); - $route = substr($route, $pos + 1); - } else { - $id = $route; - $route = ''; - } - - $childModule = $module->getModule($id); - if ($childModule !== null) { - return $this->runAction($route, $params, $childModule); - } - - /** @var $controller Controller */ - if (isset($module->controllerMap[$id])) { - $controller = \Yii::createObject($module->controllerMap[$id], $id, $module); - } else { - $controller = $this->createController($id, $module); - if ($controller === null) { - throw new InvalidRequestException("Unable to resolve the request: $route"); - } - } - - if (isset($controller)) { - $action = $this->createAction($route, $controller); - if ($action !== null) { - return $action->runWithParams($params); - } + if (error_reporting() !== 0) { + throw new \ErrorException($message, 0, $code, $file, $line); } - - throw new InvalidRequestException("Unable to resolve the request: $route"); } - /** - * Creates a controller instance based on the controller ID. + * Handles uncaught PHP exceptions. * - * The controller is created within the given 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. + * This method is implemented as a PHP exception handler. It requires + * that constant YII_ENABLE_ERROR_HANDLER be defined true. * - * @param string $id the controller ID - * @param Module $module the module that owns the controller - * @return Controller the newly created controller instance + * @param \Exception $exception exception that is not caught */ - public function createController($id, $module) + public function handleException($exception) { - if (isset($module->controllerMap[$id])) { - return \Yii::createObject($module->controllerMap[$id], $id, $module); - } elseif (preg_match('/^[a-z0-9\\-_]+$/', $id)) { - $className = StringHelper::id2camel($id) . 'Controller'; - $classFile = $module->controllerPath . DIRECTORY_SEPARATOR . $className . '.php'; - if (is_file($classFile)) { - $className = $module->controllerNamespace . '\\' . $className; - if (!class_exists($className, false)) { - require($classFile); - } - if (class_exists($className, false) && is_subclass_of($className, '\yii\base\Controller')) { - return new $className($id, $module); - } + // disable error capturing to avoid recursive errors while handling exceptions + restore_error_handler(); + restore_exception_handler(); + + try { + $this->logException($exception); + + if (($handler = $this->getErrorHandler()) !== null) { + $handler->handle($exception); + } else { + $this->renderException($exception); } + + $this->end(1); + + } catch(\Exception $e) { + // exception could be thrown in end() or ErrorHandler::handle() + $msg = (string)$e; + $msg .= "\nPrevious exception:\n"; + $msg .= (string)$exception; + $msg .= "\n\$_SERVER = " . var_export($_SERVER, true); + error_log($msg); + exit(1); } - return null; } /** - * Creates an action based on the given action ID. - * The action is created within the given controller. The method first attempts to - * create the action based on [[Controller::actions()]]. If not available, - * it will look for the inline action method within the controller. - * @param string $id the action ID - * @param Controller $controller the controller that owns the action - * @return Action the newly created action instance - * @throws InvalidConfigException if [[Controller::defaultAction]] is empty. + * Renders an exception without using rich format. + * @param \Exception $exception the exception to be rendered. */ - public function createAction($id, $controller) + public function renderException($exception) { - if ($id === '') { - $id = $controller->defaultAction; - if ($id == '') { - throw new InvalidConfigException(get_class($controller) . '::defaultAction cannot be empty.'); - } + if ($exception instanceof Exception && ($exception->causedByUser || !YII_DEBUG)) { + $message = $exception->getName() . ': ' . $exception->getMessage(); + } else { + $message = YII_DEBUG ? (string)$exception : 'Error: ' . $exception->getMessage(); } - if (isset($controller->actionMap[$id])) { - return \Yii::createObject($controller->actionMap[$id], $id, $controller); - } elseif (preg_match('/^[a-z0-9\\-_]+$/', $id)) { - $methodName = 'action' . StringHelper::id2camel($id); - if (method_exists($controller, $methodName)) { - $method = new \ReflectionMethod($controller, $methodName); - if ($method->getName() === $methodName) { - return new InlineAction($id, $controller); - } - } + if (PHP_SAPI) { + echo $message . "\n"; + } else { + echo '
    ' . htmlspecialchars($message, ENT_QUOTES, $this->charset) . '
    '; + } + } + + // todo: to be polished + protected function logException($exception) + { + $category = get_class($exception); + if ($exception instanceof HttpException) { + /** @var $exception HttpException */ + $category .= '\\' . $exception->statusCode; + } elseif ($exception instanceof \ErrorException) { + /** @var $exception \ErrorException */ + $category .= '\\' . $exception->getSeverity(); } - return null; + Yii::error((string)$exception, $category); } } diff --git a/framework/base/Controller.php b/framework/base/Controller.php index 0df287a..804b339 100644 --- a/framework/base/Controller.php +++ b/framework/base/Controller.php @@ -9,16 +9,12 @@ namespace yii\base; +use Yii; +use yii\util\StringHelper; + /** * Controller is the base class for classes containing controller logic. * - * Controller implements the action life cycles, which consist of the following steps: - * - * 1. [[authorize]] - * 2. [[beforeAction]] - * 3. [[afterAction]] - * - * @property array $actionParams the request parameters (name-value pairs) to be used for action parameter binding * @property string $route the route (module ID, controller ID and action ID) of the current request. * @property string $uniqueId the controller ID that is prefixed with the module ID (if any). * @@ -27,6 +23,9 @@ namespace yii\base; */ class Controller extends Component { + const EVENT_BEFORE_ACTION = 'beforeAction'; + const EVENT_AFTER_ACTION = 'afterAction'; + /** * @var string the ID of this controller */ @@ -91,200 +90,184 @@ class Controller extends Component } /** - * Runs the controller with the specified action and parameters. - * @param Action|string $action the action to be executed. This can be either an action object - * or the ID of the action. + * Runs an action with the specified action ID and parameters. + * If the action ID is empty, the method will use [[defaultAction]]. + * @param string $id the ID of the action to be executed. * @param array $params the parameters (name-value pairs) to be passed to the action. - * If null, the result of [[getActionParams()]] will be used as action parameters. - * @return integer the exit status of the action. 0 means normal, other values mean abnormal. - * @see missingAction + * @return integer the status of the action execution. 0 means normal, other values mean abnormal. + * @throws InvalidRouteException if the requested action ID cannot be resolved into an action successfully. * @see createAction */ - public function run($action, $params = null) + public function runAction($id, $params = array()) { - if (is_string($action)) { - if (($a = $this->createAction($action)) !== null) { - $action = $a; + $action = $this->createAction($id); + if ($action !== null) { + $oldAction = $this->action; + $this->action = $action; + + if ($this->beforeAction($action)) { + $status = $action->runWithParams($params); + $this->afterAction($action); } else { - $this->missingAction($action); - return 1; + $status = 1; } - } - $priorAction = $this->action; - $this->action = $action; + $this->action = $oldAction; - if ($this->authorize($action) && $this->beforeAction($action)) { - if ($params === null) { - $params = $this->getActionParams(); - } - $status = $action->runWithParams($params); - $this->afterAction($action); + return $status; } else { - $status = 1; + throw new InvalidRouteException('Unable to resolve the request: ' . $this->getUniqueId() . '/' . $id); } - - $this->action = $priorAction; - - return $status; } /** - * Creates the action instance based on the action ID. - * The action can be either an inline action or an object. - * The latter is created by looking up the action map specified in [[actions]]. - * @param string $actionID ID of the action. If empty, it will take the value of [[defaultAction]]. - * @return Action the action instance, null if the action does not exist. - * @see actions + * Runs a request specified in terms of a route. + * The route can be either an ID of an action within this controller or a complete route consisting + * of module IDs, controller ID and action ID. If the route starts with a slash '/', the parsing of + * the route will start from the application; otherwise, it will start from the parent module of this controller. + * @param string $route the route to be handled, e.g., 'view', 'comment/view', '/admin/comment/view'. + * @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. + * @see runAction + * @see forward */ - public function createAction($actionID) + public function run($route, $params = array()) { - if ($actionID === '') { - $actionID = $this->defaultAction; - } - if (isset($this->actionMap[$actionID])) { - return \Yii::createObject($this->actionMap[$actionID], $actionID, $this); - } elseif (method_exists($this, 'action' . $actionID)) { - return new InlineAction($actionID, $this); + $pos = strpos($route, '/'); + if ($pos === false) { + return $this->runAction($route, $params); + } elseif ($pos > 0) { + return $this->module->runAction($route, $params); } else { - return null; + return \Yii::$application->runAction(ltrim($route, '/'), $params); } } /** - * Returns the request parameters that will be used for action parameter binding. - * Default implementation simply returns an empty array. - * Child classes may override this method to customize the parameters to be provided - * for action parameter binding (e.g. `$_GET`). - * @return array the request parameters (name-value pairs) to be used for action parameter binding + * Forwards the current execution flow to handle a new request specified by a route. + * The only difference between this method and [[run()]] is that after calling this method, + * the application will exit. + * @param string $route the route to be handled, e.g., 'view', 'comment/view', '/admin/comment/view'. + * @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. + * @see run */ - public function getActionParams() + public function forward($route, $params = array()) { - return array(); + $status = $this->run($route, $params); + exit($status); } /** - * This method is invoked when the request parameters do not satisfy the requirement of the specified action. - * The default implementation will throw an exception. - * @param Action $action the action being executed - * @param Exception $exception the exception about the invalid parameters - * @throws Exception whenever this method is invoked + * Creates an action based on the given action ID. + * The method first checks if the action ID has been declared in [[actions()]]. If so, + * it will use the configuration declared there to create the action object. + * If not, it will look for a controller method whose name is in the format of `actionXyz` + * where `Xyz` stands for the action ID. If found, an [[InlineAction]] representing that + * method will be created and returned. + * @param string $id the action ID + * @return Action the newly created action instance. Null if the ID doesn't resolve into any action. */ - public function invalidActionParams($action, $exception) + public function createAction($id) { - throw $exception; - } + if ($id === '') { + $id = $this->defaultAction; + } - /** - * This method is invoked when extra parameters are provided to an action when it is executed. - * The default implementation does nothing. - * @param Action $action the action being executed - * @param array $expected the expected action parameters (name => value) - * @param array $actual the actual action parameters (name => value) - */ - public function extraActionParams($action, $expected, $actual) - { + $actionMap = $this->actions(); + if (isset($actionMap[$id])) { + return Yii::createObject($actionMap[$id], $id, $this); + } elseif (preg_match('/^[a-z0-9\\-_]+$/', $id)) { + $methodName = 'action' . StringHelper::id2camel($id); + if (method_exists($this, $methodName)) { + $method = new \ReflectionMethod($this, $methodName); + if ($method->getName() === $methodName) { + return new InlineAction($id, $this, $methodName); + } + } + } + return null; } /** - * Handles the request whose action is not recognized. - * This method is invoked when the controller cannot find the requested action. - * The default implementation simply throws an exception. - * @param string $actionID the missing action name - * @throws InvalidRequestException whenever this method is invoked + * 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 missingAction($actionID) + public function beforeAction($action) { - throw new InvalidRequestException(\Yii::t('yii', 'The system is unable to find the requested action "{action}".', - array('{action}' => $actionID == '' ? $this->defaultAction : $actionID))); + $event = new ActionEvent($action); + $this->trigger(self::EVENT_BEFORE_ACTION, $event); + return $event->isValid; } /** - * @return string the controller ID that is prefixed with the module ID (if any). + * 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 getUniqueId() + public function afterAction($action) { - return $this->module instanceof Application ? $this->id : $this->module->getUniqueId() . '/' . $this->id; + $this->trigger(self::EVENT_AFTER_ACTION, new ActionEvent($action)); } /** - * Returns the route of the current request. - * @return string the route (module ID, controller ID and action ID) of the current request. + * Returns the request parameters that will be used for action parameter binding. + * Default implementation simply returns an empty array. + * Child classes may override this method to customize the parameters to be provided + * for action parameter binding (e.g. `$_GET`). + * @return array the request parameters (name-value pairs) to be used for action parameter binding */ - public function getRoute() + public function getActionParams() { - return $this->action !== null ? $this->getUniqueId() . '/' . $this->action->id : $this->getUniqueId(); + return array(); } /** - * Processes the request using another controller action. - * @param string $route the route of the new controller action. This can be an action ID, or a complete route - * with module ID (optional in the current module), controller ID and action ID. If the former, - * the action is assumed to be located within the current controller. - * @param array $params the parameters to be passed to the action. - * If null, the result of [[getActionParams()]] will be used as action parameters. - * Note that the parameters must be name-value pairs with the names corresponding to - * the parameter names as declared by the action. - * @param boolean $exit whether to end the application after this call. Defaults to true. + * 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) */ - public function forward($route, $params = array(), $exit = true) + public function validateActionParams($action, $missingParams, $unknownParams) { - if (strpos($route, '/') === false) { - $status = $this->run($route, $params); - } else { - if ($route[0] !== '/' && !$this->module instanceof Application) { - $route = '/' . $this->module->getUniqueId() . '/' . $route; - } - $status = \Yii::$application->runController($route, $params); - } - if ($exit) { - \Yii::$application->end($status); - } } /** - * This method is invoked when checking the access for the action to be executed. - * @param Action $action the action to be executed. - * @return boolean whether the action is allowed to be executed. + * @return string the controller ID that is prefixed with the module ID (if any). */ - public function authorize($action) + public function getUniqueId() { - $event = new ActionEvent($action); - $this->trigger(__METHOD__, $event); - return $event->isValid; + return $this->module instanceof Application ? $this->id : $this->module->getUniqueId() . '/' . $this->id; } /** - * 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. + * Returns the route of the current request. + * @return string the route (module ID, controller ID and action ID) of the current request. */ - public function beforeAction($action) + public function getRoute() { - $event = new ActionEvent($action); - $this->trigger(__METHOD__, $event); - return $event->isValid; + return $this->action !== null ? $this->getUniqueId() . '/' . $this->action->id : $this->getUniqueId(); } /** - * 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. + * Renders a view and applies layout if available. + * + * @param $view + * @param array $params + * @return string */ - public function afterAction($action) - { - $this->trigger(__METHOD__, new ActionEvent($action)); - } - public function render($view, $params = array()) { return $this->createView()->render($view, $params); } - public function renderText($text) + public function renderContent($content) { - return $this->createView()->renderText($text); + return $this->createView()->renderContent($content); } public function renderPartial($view, $params = array()) @@ -296,4 +279,15 @@ class Controller extends Component { return new View($this); } + + /** + * Returns the directory containing view files for this controller. + * The default implementation returns the directory named as controller [[id]] under the [[module]]'s + * [[viewPath]] directory. + * @return string the directory containing the view files for this controller. + */ + public function getViewPath() + { + return $this->module->getViewPath() . DIRECTORY_SEPARATOR . $this->id; + } } diff --git a/framework/base/ErrorHandler.php b/framework/base/ErrorHandler.php index 9f1621a..0b6bf97 100644 --- a/framework/base/ErrorHandler.php +++ b/framework/base/ErrorHandler.php @@ -52,90 +52,46 @@ class ErrorHandler extends Component * @var \Exception the exception that is being handled currently */ public $exception; - /** - * @var boolean whether to log errors also using error_log(). Defaults to true. - * Note that errors captured by the error handler are always logged by [[\Yii::error()]]. - */ - public $logErrors = true; - - public function init() - { - set_exception_handler(array($this, 'handleException')); - set_error_handler(array($this, 'handleError'), error_reporting()); - } - /** - * Handles PHP execution errors such as warnings, notices. - * - * This method is used as a PHP error handler. It will simply raise an `ErrorException`. - * - * @param integer $code the level of the error raised - * @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 - */ - public function handleError($code, $message, $file, $line) - { - if(error_reporting()!==0) { - throw new \ErrorException($message, 0, $code, $file, $line); - } - } /** * @param \Exception $exception */ - public function handleException($exception) + public function handle($exception) { - // disable error capturing to avoid recursive errors while handling exceptions - restore_error_handler(); - restore_exception_handler(); - $this->exception = $exception; - $this->logException($exception); if ($this->discardExistingOutput) { $this->clearOutput(); } - try { - $this->render($exception); - } catch (\Exception $e) { - // use the most primitive way to display exception thrown in the error view - $this->renderAsText($e); - } - - - try { - \Yii::$application->end(1); - } catch (Exception $e2) { - // use the most primitive way to log error occurred in end() - $msg = get_class($e2) . ': ' . $e2->getMessage() . ' (' . $e2->getFile() . ':' . $e2->getLine() . ")\n"; - $msg .= $e2->getTraceAsString() . "\n"; - $msg .= "Previous error:\n"; - $msg .= $e2->getTraceAsString() . "\n"; - $msg .= '$_SERVER=' . var_export($_SERVER, true); - error_log($msg); - exit(1); - } + $this->render($exception); } protected function render($exception) { if ($this->errorAction !== null) { - \Yii::$application->runController($this->errorAction); + \Yii::$application->runAction($this->errorAction); } elseif (\Yii::$application 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') { - $this->renderAsText($exception); + \Yii::$application->renderException($exception); } else { - $this->renderAsHtml($exception); + $view = new View($this); + if (!YII_DEBUG || $exception instanceof Exception && $exception->causedByUser) { + $viewName = $this->errorView; + } else { + $viewName = $this->exceptionView; + } + echo $view->render($viewName, array( + 'exception' => $exception, + )); } } else { - $this->renderAsText($exception); + \Yii::$application->renderException($exception); } } @@ -286,22 +242,6 @@ class ErrorHandler extends Component return htmlspecialchars($text, ENT_QUOTES, \Yii::$application->charset); } - public function logException($exception) - { - $category = get_class($exception); - if ($exception instanceof HttpException) { - /** @var $exception HttpException */ - $category .= '\\' . $exception->statusCode; - } elseif ($exception instanceof \ErrorException) { - /** @var $exception \ErrorException */ - $category .= '\\' . $exception->getSeverity(); - } - \Yii::error((string)$exception, $category); - if ($this->logErrors) { - error_log($exception); - } - } - public function clearOutput() { // the following manual level counting is to deal with zlib.output_compression set to On @@ -313,22 +253,14 @@ class ErrorHandler extends Component /** * @param \Exception $exception */ - public function renderAsText($exception) + public function renderAsHtml($exception) { - if (YII_DEBUG) { - echo $exception; + $view = new View($this); + if (!YII_DEBUG || $exception instanceof Exception && $exception->causedByUser) { + $viewName = $this->errorView; } else { - echo get_class($exception) . ': ' . $exception->getMessage(); + $viewName = $this->exceptionView; } - } - - /** - * @param \Exception $exception - */ - public function renderAsHtml($exception) - { - $view = new View; - $view->context = $this; $name = !YII_DEBUG || $exception instanceof HttpException ? $this->errorView : $this->exceptionView; echo $view->render($name, array( 'exception' => $exception, diff --git a/framework/base/Exception.php b/framework/base/Exception.php index a740a35..ab681e2 100644 --- a/framework/base/Exception.php +++ b/framework/base/Exception.php @@ -17,5 +17,17 @@ namespace yii\base; */ class Exception extends \Exception { + /** + * @var boolean whether this exception is caused by end user's mistake (e.g. wrong URL) + */ + public $causedByUser = false; + + /** + * @return string the user-friendly name of this exception + */ + public function getName() + { + return \Yii::t('yii', 'Exception'); + } } diff --git a/framework/base/HttpException.php b/framework/base/HttpException.php index 52ac690..d2de5bc 100644 --- a/framework/base/HttpException.php +++ b/framework/base/HttpException.php @@ -25,6 +25,10 @@ class HttpException extends Exception * @var integer HTTP status code, such as 403, 404, 500, etc. */ public $statusCode; + /** + * @var boolean whether this exception is caused by end user's mistake (e.g. wrong URL) + */ + public $causedByUser = true; /** * Constructor. diff --git a/framework/base/InlineAction.php b/framework/base/InlineAction.php index 8764ac2..4cd5413 100644 --- a/framework/base/InlineAction.php +++ b/framework/base/InlineAction.php @@ -9,13 +9,11 @@ namespace yii\base; -use yii\util\ReflectionHelper; - /** * InlineAction represents an action that is defined as a controller method. * - * The name of the controller method should be in the format of `actionXyz` - * where `Xyz` stands for the action ID (e.g. `actionIndex`). + * The name of the controller method is available via [[actionMethod]] which + * is set by the [[controller]] who creates this action. * * @author Qiang Xue * @since 2.0 @@ -23,6 +21,23 @@ use yii\util\ReflectionHelper; class InlineAction extends Action { /** + * @var string the controller method that this inline action is associated with + */ + public $actionMethod; + + /** + * @param string $id the ID of this action + * @param Controller $controller the controller that owns this action + * @param string $actionMethod the controller method that this inline action is associated with + * @param array $config name-value pairs that will be used to initialize the object properties + */ + public function __construct($id, $controller, $actionMethod, $config = array()) + { + $this->actionMethod = $actionMethod; + parent::__construct($id, $controller, $config); + } + + /** * Runs this action with the specified parameters. * This method is mainly invoked by the controller. * @param array $params action parameters @@ -30,16 +45,8 @@ class InlineAction extends Action */ public function runWithParams($params) { - try { - $method = 'action' . $this->id; - $ps = ReflectionHelper::extractMethodParams($this->controller, $method, $params); - } catch (Exception $e) { - $this->controller->invalidActionParams($this, $e); - return 1; - } - if ($params !== $ps) { - $this->controller->extraActionParams($this, $ps, $params); - } - return (int)call_user_func_array(array($this->controller, $method), $ps); + $method = new \ReflectionMethod($this->controller, $this->actionMethod); + $args = $this->bindActionParams($method, $params); + return (int)$method->invokeArgs($this->controller, $args); } } diff --git a/framework/base/InvalidCallException.php b/framework/base/InvalidCallException.php index 24e7b6e..a1df021 100644 --- a/framework/base/InvalidCallException.php +++ b/framework/base/InvalidCallException.php @@ -17,5 +17,12 @@ namespace yii\base; */ class InvalidCallException extends \Exception { + /** + * @return string the user-friendly name of this exception + */ + public function getName() + { + return \Yii::t('yii', 'Invalid Call'); + } } diff --git a/framework/base/InvalidConfigException.php b/framework/base/InvalidConfigException.php index 5256d7e..3c100d1 100644 --- a/framework/base/InvalidConfigException.php +++ b/framework/base/InvalidConfigException.php @@ -17,5 +17,12 @@ namespace yii\base; */ class InvalidConfigException extends \Exception { + /** + * @return string the user-friendly name of this exception + */ + public function getName() + { + return \Yii::t('yii', 'Invalid Configuration'); + } } diff --git a/framework/base/InvalidRequestException.php b/framework/base/InvalidRequestException.php index 2e2a04a..fd468a1 100644 --- a/framework/base/InvalidRequestException.php +++ b/framework/base/InvalidRequestException.php @@ -17,5 +17,17 @@ namespace yii\base; */ class InvalidRequestException extends \Exception { + /** + * @var boolean whether this exception is caused by end user's mistake (e.g. wrong URL) + */ + public $causedByUser = true; + + /** + * @return string the user-friendly name of this exception + */ + public function getName() + { + return \Yii::t('yii', 'Invalid Request'); + } } diff --git a/framework/base/InvalidRouteException.php b/framework/base/InvalidRouteException.php new file mode 100644 index 0000000..e20b2b7 --- /dev/null +++ b/framework/base/InvalidRouteException.php @@ -0,0 +1,33 @@ + + * @since 2.0 + */ +class InvalidRouteException extends \Exception +{ + /** + * @var boolean whether this exception is caused by end user's mistake (e.g. wrong URL) + */ + public $causedByUser = true; + + /** + * @return string the user-friendly name of this exception + */ + public function getName() + { + return \Yii::t('yii', 'Invalid Route'); + } +} + diff --git a/framework/base/Module.php b/framework/base/Module.php index 6337a9b..dcb468c 100644 --- a/framework/base/Module.php +++ b/framework/base/Module.php @@ -9,18 +9,29 @@ namespace yii\base; +use Yii; +use yii\util\StringHelper; use yii\util\FileHelper; /** * Module is the base class for module and application classes. * - * Module mainly manages application components and sub-modules that belongs to a module. + * A module represents a sub-application which contains MVC elements by itself, such as + * models, views, controllers, etc. + * + * A module may consist of [[modules|sub-modules]]. + * + * [[components|Components]] may be registered with the module so that they are globally + * accessible within the module. * * @property string $uniqueId An ID that uniquely identifies this module among all modules within * the current application. * @property string $basePath The root directory of the module. Defaults to the directory containing the module class. + * @property string $controllerPath The directory containing the controller classes. Defaults to "[[basePath]]/controllers". + * @property string $viewPath The directory containing the view files within this module. Defaults to "[[basePath]]/views". + * @property string $layoutPath The directory containing the layout view files within this module. Defaults to "[[viewPath]]/layouts". * @property array $modules The configuration of the currently installed modules (module ID => configuration). - * @property array $components The application components (indexed by their IDs). + * @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. * @@ -34,7 +45,7 @@ abstract class Module extends Component */ public $params = array(); /** - * @var array the IDs of the application components that should be preloaded when this module is created. + * @var array the IDs of the components that should be preloaded when this module is created. */ public $preload = array(); /** @@ -86,27 +97,27 @@ abstract class Module extends Component /** * @var string the root directory of the module. */ - protected $_basePath; + private $_basePath; /** * @var string the root directory that contains view files for this module */ - protected $_viewPath; + private $_viewPath; /** * @var string the root directory that contains layout view files for this module. */ - protected $_layoutPath; + private $_layoutPath; /** * @var string the directory containing controller classes in the module. */ - protected $_controllerPath; + private $_controllerPath; /** * @var array child modules of this module */ - protected $_modules = array(); + private $_modules = array(); /** - * @var array application components of this module + * @var array components registered under this module */ - protected $_components = array(); + private $_components = array(); /** * Constructor. @@ -123,9 +134,9 @@ abstract class Module extends Component /** * Getter magic method. - * This method is overridden to support accessing application components + * This method is overridden to support accessing components * like reading module properties. - * @param string $name application component or property name + * @param string $name component or property name * @return mixed the named property value */ public function __get($name) @@ -140,7 +151,7 @@ abstract class Module extends Component /** * Checks if a property value is null. * This method overrides the parent implementation by checking - * if the named application component is loaded. + * if the named component is loaded. * @param string $name the property name or the event name * @return boolean whether the property value is null */ @@ -161,18 +172,21 @@ abstract class Module extends Component */ public function init() { - \Yii::setAlias('@' . $this->id, $this->getBasePath()); + 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->module && !$this->module instanceof Application) { - return $this->module->getUniqueId() . "/{$this->id}"; + if ($this instanceof Application) { + return ''; + } elseif ($this->module) { + return $this->module->getUniqueId() . '/' . $this->id; } else { return $this->id; } @@ -229,8 +243,8 @@ abstract class Module extends Component } /** - * @return string the root directory of view files. Defaults to 'moduleDir/views' where - * moduleDir is the directory containing the module class. + * 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() { @@ -252,8 +266,8 @@ abstract class Module extends Component } /** - * @return string the root directory of layout files. Defaults to 'moduleDir/views/layouts' where - * moduleDir is the directory containing the module class. + * 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() { @@ -277,19 +291,19 @@ abstract class Module extends Component /** * 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()]]. + * 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); + Yii::import($alias); } } /** * Defines path aliases. - * This method calls [[\Yii::setAlias()]] to register the 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. @@ -297,7 +311,7 @@ abstract class Module extends Component * * ~~~ * array( - * '@models' => '@app/models', // an existing alias + * '@models' => '@application/models', // an existing alias * '@backend' => __DIR__ . '/../backend', // a directory * ) * ~~~ @@ -305,7 +319,7 @@ abstract class Module extends Component public function setAliases($aliases) { foreach ($aliases as $name => $alias) { - \Yii::setAlias($name, $alias); + Yii::setAlias($name, $alias); } } @@ -334,8 +348,8 @@ abstract class Module extends Component if ($this->_modules[$id] instanceof Module) { return $this->_modules[$id]; } elseif ($load) { - \Yii::trace("Loading \"$id\" module", __CLASS__); - return $this->_modules[$id] = \Yii::createObject($this->_modules[$id], $id, $this); + Yii::trace("Loading module: $id", __CLASS__); + return $this->_modules[$id] = Yii::createObject($this->_modules[$id], $id, $this); } } return null; @@ -388,7 +402,7 @@ abstract class Module extends Component * * 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()]] + * 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. @@ -418,8 +432,8 @@ abstract class Module extends Component /** * Checks whether the named component exists. - * @param string $id application component ID - * @return boolean whether the named application component exists. Both loaded and unloaded components + * @param string $id component ID + * @return boolean whether the named component exists. Both loaded and unloaded components * are considered. */ public function hasComponent($id) @@ -428,11 +442,10 @@ abstract class Module extends Component } /** - * Retrieves the named application component. - * @param string $id application component ID (case-sensitive) + * 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 application component instance, null if the application component - * does not exist. + * @return Component|null the component instance, null if the component does not exist. * @see hasComponent() */ public function getComponent($id, $load = true) @@ -441,22 +454,22 @@ abstract class Module extends Component if ($this->_components[$id] instanceof Component) { return $this->_components[$id]; } elseif ($load) { - \Yii::trace("Loading \"$id\" application component", __CLASS__); - return $this->_components[$id] = \Yii::createObject($this->_components[$id]); + Yii::trace("Loading component: $id", __CLASS__); + return $this->_components[$id] = Yii::createObject($this->_components[$id]); } } return null; } /** - * Registers an application component in this module. + * Registers a component with this module. * @param string $id component ID - * @param Component|array|null $component the component to be added to the module. This can + * @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 + * 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) @@ -469,11 +482,11 @@ abstract class Module extends Component } /** - * Returns the application components. + * 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 application components (indexed by their IDs) + * @return array the components (indexed by their IDs) */ public function getComponents($loadedOnly = false) { @@ -491,11 +504,11 @@ abstract class Module extends Component } /** - * Registers a set of application components in this module. + * Registers a set of components in this module. * - * Each application component should be specified as a name-value pair, where + * 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()]] + * 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. @@ -515,7 +528,7 @@ abstract class Module extends Component * ) * ~~~ * - * @param array $components application components (id => component configuration or instance) + * @param array $components components (id => component configuration or instance) */ public function setComponents($components) { @@ -525,7 +538,7 @@ abstract class Module extends Component } /** - * Loads application components that are declared in [[preload]]. + * Loads components that are declared in [[preload]]. */ public function preloadComponents() { @@ -533,4 +546,79 @@ abstract class Module extends Component $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; + } } diff --git a/framework/base/NotSupportedException.php b/framework/base/NotSupportedException.php index aa2badb..56e7e36 100644 --- a/framework/base/NotSupportedException.php +++ b/framework/base/NotSupportedException.php @@ -17,5 +17,12 @@ namespace yii\base; */ class NotSupportedException extends \Exception { + /** + * @return string the user-friendly name of this exception + */ + public function getName() + { + return \Yii::t('yii', 'Not Supported'); + } } diff --git a/framework/base/Theme.php b/framework/base/Theme.php index c1fc94a..03f8f55 100644 --- a/framework/base/Theme.php +++ b/framework/base/Theme.php @@ -9,55 +9,114 @@ namespace yii\base; +use Yii; use yii\base\InvalidConfigException; +use yii\util\FileHelper; /** * Theme represents an application theme. * + * A theme is directory consisting of view and layout files which are meant to replace their + * non-themed counterparts. + * + * Theme uses [[pathMap]] to achieve the file replacement. A view or layout file will be replaced + * with its themed version if part of its path matches one of the keys in [[pathMap]]. + * Then the matched part will be replaced with the corresponding array value. + * + * For example, if [[pathMap]] is `array('/www/views' => '/www/themes/basic')`, + * then the themed version for a view file `/www/views/site/index.php` will be + * `/www/themes/basic/site/index.php`. + * + * @property string $baseUrl the base URL for this theme. This is mainly used by [[getUrl()]]. + * * @author Qiang Xue * @since 2.0 */ class Theme extends Component { + /** + * @var string the root path of this theme. + * @see pathMap + */ public $basePath; - public $baseUrl; + /** + * @var array the mapping between view directories and their corresponding themed versions. + * If not set, it will be initialized as a mapping from [[Application::basePath]] to [[basePath]]. + * This property is used by [[apply()]] when a view is trying to apply the theme. + */ + public $pathMap; + + private $_baseUrl; + /** + * Initializes the theme. + * @throws InvalidConfigException if [[basePath]] is not set. + */ public function init() { - if ($this->basePath !== null) { - $this->basePath = \Yii::getAlias($this->basePath, true); - } else { - throw new InvalidConfigException("Theme.basePath must be set."); + parent::init(); + if (empty($this->pathMap)) { + if ($this->basePath !== null) { + $this->basePath = FileHelper::ensureDirectory($this->basePath); + $this->pathMap = array(Yii::$application->getBasePath() => $this->basePath); + } else { + throw new InvalidConfigException("Theme::basePath must be set."); + } } - if ($this->baseUrl !== null) { - $this->baseUrl = \Yii::getAlias($this->baseUrl, true); - } else { - throw new InvalidConfigException("Theme.baseUrl must be set."); + $paths = array(); + foreach ($this->pathMap as $from => $to) { + $paths[FileHelper::normalizePath($from) . DIRECTORY_SEPARATOR] = FileHelper::normalizePath($to) . DIRECTORY_SEPARATOR; } + $this->pathMap = $paths; + } + + /** + * Returns the base URL for this theme. + * The method [[getUrl()]] will prefix this to the given URL. + * @return string the base URL for this theme. + */ + public function getBaseUrl() + { + return $this->_baseUrl; + } + + /** + * Sets the base URL for this theme. + * @param string $value the base URL for this theme. + */ + public function setBaseUrl($value) + { + $this->_baseUrl = rtrim(Yii::getAlias($value), '/'); } /** - * @param Application|Module|Controller|Object $context - * @return string + * Converts a file to a themed file if possible. + * If there is no corresponding themed file, the original file will be returned. + * @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 getViewPath($context = null) + public function apply($path) { - $viewPath = $this->basePath . DIRECTORY_SEPARATOR . 'views'; - if ($context === null || $context instanceof Application) { - return $viewPath; - } elseif ($context instanceof Controller || $context instanceof Module) { - return $viewPath . DIRECTORY_SEPARATOR . $context->getUniqueId(); - } else { - return $viewPath . DIRECTORY_SEPARATOR . str_replace('\\', '_', get_class($context)); + $path = FileHelper::normalizePath($path); + foreach ($this->pathMap as $from => $to) { + if (strpos($path, $from) === 0) { + $n = strlen($from); + $file = $to . substr($path, $n); + if (is_file($file)) { + return $file; + } + } } + return $path; } /** - * @param Module $module - * @return string + * Converts a relative URL into an absolute URL using [[basePath]]. + * @param string $url the relative URL to be converted. + * @return string the absolute URL */ - public function getLayoutPath($module = null) + public function getUrl($url) { - return $this->getViewPath($module) . DIRECTORY_SEPARATOR . 'layouts'; + return $this->baseUrl . '/' . ltrim($url, '/'); } } diff --git a/framework/base/UnknownMethodException.php b/framework/base/UnknownMethodException.php index 8667d24..459f791 100644 --- a/framework/base/UnknownMethodException.php +++ b/framework/base/UnknownMethodException.php @@ -17,5 +17,12 @@ namespace yii\base; */ class UnknownMethodException extends \Exception { + /** + * @return string the user-friendly name of this exception + */ + public function getName() + { + return \Yii::t('yii', 'Unknown Method'); + } } diff --git a/framework/base/UnknownPropertyException.php b/framework/base/UnknownPropertyException.php index 69581f9..de8de1c 100644 --- a/framework/base/UnknownPropertyException.php +++ b/framework/base/UnknownPropertyException.php @@ -17,5 +17,12 @@ namespace yii\base; */ class UnknownPropertyException extends \Exception { + /** + * @return string the user-friendly name of this exception + */ + public function getName() + { + return \Yii::t('yii', 'Unknown Property'); + } } diff --git a/framework/base/View.php b/framework/base/View.php index db0741a..dccfb26 100644 --- a/framework/base/View.php +++ b/framework/base/View.php @@ -9,27 +9,29 @@ namespace yii\base; +use Yii; use yii\util\FileHelper; use yii\base\Application; /** + * View represents a view object in the MVC pattern. + * + * View provides a set of methods (e.g. [[render()]]) for rendering purpose. + * * @author Qiang Xue * @since 2.0 */ class View extends Component { /** - * @var Controller|Widget|Object the context under which this view is being rendered + * @var object the object that owns this view. This can be a controller, a widget, or any other object. */ - public $context; + public $owner; /** - * @var string|array the directories where the view file should be looked for when a *relative* view name is given. - * This can be either a string representing a single directory, or an array representing multiple directories. - * If the latter, the view file will be looked for in the directories in the order they are specified. - * Path aliases can be used. If this property is not set, relative view names should be treated as absolute ones. - * @see roothPath + * @var string the layout to be applied when [[render()]] or [[renderContent()]] is called. + * If not set, it will use the [[Module::layout]] of the currently active module. */ - public $basePath; + public $layout; /** * @var string the language that the view should be rendered in. If not set, it will use * the value of [[Application::language]]. @@ -45,45 +47,67 @@ class View extends Component * Note that when this is true, if a localized view cannot be found, the original view will be rendered. * No error will be reported. */ - public $localizeView = true; + public $enableI18N = true; /** * @var boolean whether to theme the view when possible. Defaults to true. - * Note that theming will be disabled if [[Application::theme]] is null. + * Note that theming will be disabled if [[Application::theme]] is not set. */ - public $themeView = true; + public $enableTheme = true; /** * @var mixed custom parameters that are available in the view template */ public $params; + /** * @var Widget[] the widgets that are currently not ended */ - protected $widgetStack = array(); + private $_widgetStack = array(); /** * Constructor. - * @param Controller|Widget|Object $context the context under which this view is being rendered (e.g. controller, widget) + * @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($context = null, $config = array()) + public function __construct($owner, $config = array()) { - $this->context = $context; + $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->renderText($content); + return $this->renderContent($content); } - public function renderText($text) + /** + * 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() + */ + public function renderContent($content) { $layoutFile = $this->findLayoutFile(); if ($layoutFile !== false) { - return $this->renderFile($layoutFile, array('content' => $text)); + return $this->renderFile($layoutFile, array('content' => $content)); } else { - return $text; + return $content; } } @@ -94,18 +118,12 @@ class View extends Component * It then calls [[renderFile()]] to render the view file. The rendering result is returned * as a string. If the view file does not exist, an exception will be thrown. * - * To determine which view file should be rendered, the method calls [[findViewFile()]] which - * will search in the directories as specified by [[basePath]]. - * - * View name can be a path alias representing an absolute file path (e.g. `@app/views/layout/index`), - * or a path relative to [[basePath]]. The file suffix is optional and defaults to `.php` if not given - * in the view name. - * - * @param string $view the view to be rendered. This can be either a path alias or a path relative to [[basePath]]. + * @param 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 InvalidCallException if the view file cannot be found + * @see findViewFile() */ public function renderPartial($view, $params = array()) { @@ -119,21 +137,44 @@ class View extends Component /** * Renders a view file. - * @param string $file the view file path - * @param array $params the parameters to be extracted and made available in the view file + * This method will extract the given parameters and include the view file. + * It captures the output of the included view file and returns it as a string. + * @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 */ - public function renderFile($file, $params = array()) + public function renderFile($_file_, $_params_ = array()) { - return $this->renderFileInternal($file, $params); + ob_start(); + ob_implicit_flush(false); + extract($_params_, EXTR_OVERWRITE); + require($_file_); + return ob_get_clean(); } + /** + * Creates a widget. + * This method will use [[Yii::createObject()]] to create the widget. + * @param string $class the widget class name or path alias + * @param array $properties the initial property values of the widget. + * @return Widget the newly created widget instance + */ public function createWidget($class, $properties = array()) { $properties['class'] = $class; - return \Yii::createObject($properties, $this->context); + return Yii::createObject($properties, $this->owner); } + /** + * Creates and runs a widget. + * Compared with [[createWidget()]], this method does one more thing: it will + * run the widget after it is created. + * @param string $class the widget class name or path alias + * @param array $properties the initial property values of the widget. + * @param boolean $captureOutput whether to capture the output of the widget and return it as a string + * @return string|Widget if $captureOutput is true, the output of the widget will be returned; + * otherwise the widget object will be returned. + */ public function widget($class, $properties = array(), $captureOutput = false) { if ($captureOutput) { @@ -151,14 +192,16 @@ class View extends Component /** * Begins a widget. - * @param string $class the widget class - * @param array $properties the initial property values of the widget + * This method is similar to [[createWidget()]] except that it will expect a matching + * [[endWidget()]] call after this. + * @param string $class the widget class name or path alias + * @param array $properties the initial property values of the widget. * @return Widget the widget instance */ public function beginWidget($class, $properties = array()) { $widget = $this->createWidget($class, $properties); - $this->widgetStack[] = $widget; + $this->_widgetStack[] = $widget; return $widget; } @@ -172,293 +215,256 @@ class View extends Component */ public function endWidget() { - /** @var $widget Widget */ - if (($widget = array_pop($this->widgetStack)) !== null) { + $widget = array_pop($this->_widgetStack); + if ($widget instanceof Widget) { $widget->run(); return $widget; } else { throw new Exception("Unmatched beginWidget() and endWidget() calls."); } } +// +// /** +// * Begins recording a clip. +// * This method is a shortcut to beginning [[yii\widgets\Clip]] +// * @param string $id the clip ID. +// * @param array $properties initial property values for [[yii\widgets\Clip]] +// */ +// public function beginClip($id, $properties = array()) +// { +// $properties['id'] = $id; +// $this->beginWidget('yii\widgets\Clip', $properties); +// } +// +// /** +// * Ends recording a clip. +// */ +// public function endClip() +// { +// $this->endWidget(); +// } +// +// /** +// * Begins fragment caching. +// * This method will display cached content if it is available. +// * If not, it will start caching and would expect an [[endCache()]] +// * call to end the cache and save the content into cache. +// * A typical usage of fragment caching is as follows, +// * +// * ~~~ +// * if($this->beginCache($id)) { +// * // ...generate content here +// * $this->endCache(); +// * } +// * ~~~ +// * +// * @param string $id a unique ID identifying the fragment to be cached. +// * @param array $properties initial property values for [[yii\widgets\OutputCache]] +// * @return boolean whether we need to generate content for caching. False if cached version is available. +// * @see endCache +// */ +// public function beginCache($id, $properties = array()) +// { +// $properties['id'] = $id; +// $cache = $this->beginWidget('yii\widgets\OutputCache', $properties); +// if ($cache->getIsContentCached()) { +// $this->endCache(); +// return false; +// } else { +// return true; +// } +// } +// +// /** +// * Ends fragment caching. +// * This is an alias to [[endWidget()]] +// * @see beginCache +// */ +// public function endCache() +// { +// $this->endWidget(); +// } +// +// /** +// * Begins the rendering of content that is to be decorated by the specified view. +// * @param mixed $view the name of the view that will be used to decorate the content. The actual view script +// * is resolved via {@link getViewFile}. If this parameter is null (default), +// * the default layout will be used as the decorative view. +// * Note that if the current controller does not belong to +// * any module, the default layout refers to the application's {@link CWebApplication::layout default layout}; +// * If the controller belongs to a module, the default layout refers to the module's +// * {@link CWebModule::layout default layout}. +// * @param array $params the variables (name=>value) to be extracted and made available in the decorative view. +// * @see endContent +// * @see yii\widgets\ContentDecorator +// */ +// public function beginContent($view, $params = array()) +// { +// $this->beginWidget('yii\widgets\ContentDecorator', array( +// 'view' => $view, +// 'params' => $params, +// )); +// } +// +// /** +// * Ends the rendering of content. +// * @see beginContent +// */ +// public function endContent() +// { +// $this->endWidget(); +// } /** - * 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, + * Finds the view file based on the given view name. * - * ~~~ - * if($this->beginCache($id)) { - * // ...generate content here - * $this->endCache(); - * } - * ~~~ + * 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 $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(); - } - - /** - * Renders a view file. - * This method will extract the given parameters and include the view file. - * It captures the output of the included view file and returns it as a string. - * @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 - */ - protected function renderFileInternal($_file_, $_params_ = array()) - { - ob_start(); - ob_implicit_flush(false); - extract($_params_, EXTR_OVERWRITE); - require($_file_); - return ob_get_clean(); - } - - /** - * Finds the view file based on the given view name. * @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|boolean the view file if it exists. False if the view file cannot be found. + * @return string the view file path if it exists. False if the view file cannot be found. + * @throws InvalidConfigException if the view file does not exist */ public function findViewFile($view) { - if (($extension = FileHelper::getExtension($view)) === '') { + if (FileHelper::getExtension($view) === '') { $view .= '.php'; } if (strncmp($view, '@', 1) === 0) { - $file = \Yii::getAlias($view); + // e.g. "@application/views/common" + if (($file = Yii::getAlias($view)) === false) { + throw new InvalidConfigException("Invalid path alias: $view"); + } } elseif (strncmp($view, '/', 1) !== 0) { - $file = $this->findRelativeViewFile($view); - } else { - $file = $this->findAbsoluteViewFile($view); - } - - if ($file === false || !is_file($file)) { - return false; - } elseif ($this->localizeView) { - return FileHelper::localize($file, $this->language, $this->sourceLanguage); + // 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 { - return $file; + // e.g. "//layouts/main" + $file = Yii::$application->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); } - } - /** - * Finds the view file corresponding to the given relative view name. - * The method will look for the view file under a set of directories returned by [[resolveBasePath()]]. - * If no base path is given, the view will be treated as an absolute view and the result of - * [[findAbsoluteViewFile()]] will be returned. - * @param string $view the relative view name - * @return string|boolean the view file path, or false if the view file cannot be found - */ - protected function findRelativeViewFile($view) - { - $paths = $this->resolveBasePath(); - if ($paths === array()) { - return $this->findAbsoluteViewFile($view); - } - if ($this->themeView && $this->context !== null && ($theme = \Yii::$application->getTheme()) !== null) { - array_unshift($paths, $theme->getViewPath($this->context)); - } - foreach ($paths as $path) { - $file = \Yii::getAlias($path . '/' . $view); - if ($file !== false && is_file($file)) { - return $file; + if (is_file($file)) { + if ($this->enableTheme && ($theme = Yii::$application->getTheme()) !== null) { + $file = $theme->apply($file); } - } - return $paths === array() ? $this->findAbsoluteViewFile($view) : false; - } - - /** - * Finds the view file corresponding to the given absolute view name. - * If the view name starts with double slashes `//`, the method will look for the view file - * under [[Application::getViewPath()]]. Otherwise, it will look for the view file under the - * view path of the currently active module. - * @param string $view the absolute view name - * @return string|boolean the view file path, or false if the view file cannot be found - */ - protected function findAbsoluteViewFile($view) - { - $app = \Yii::$application; - if (strncmp($view, '//', 2) !== 0 && $app->controller !== null) { - $module = $app->controller->module; + return $this->enableI18N ? FileHelper::localize($file, $this->language, $this->sourceLanguage) : $file; } else { - $module = $app; + throw new InvalidConfigException("View file for view '$view' does not exist: $file"); } - if ($this->themeView && ($theme = $app->getTheme()) !== null) { - $paths[] = $theme->getViewPath($module); - } - $paths[] = $module->getViewPath(); - $view = ltrim($view, '/'); - foreach ($paths as $path) { - $file = \Yii::getAlias($path . '/' . $view); - if ($file !== false && is_file($file)) { - return $file; - } - } - return false; } /** - * Resolves the base paths that will be used to determine view files for relative view names. - * The method resolves the base path using the following algorithm: + * Finds the layout file that can be applied to the view. * - * - If [[basePath]] is not empty, it is returned; - * - If [[context]] is a controller, it will return the subdirectory named as - * [[Controller::uniqueId]] under the controller's module view path; - * - If [[context]] is an object, it will return the `views` subdirectory under - * the directory containing the object class file. - * - Otherwise, it will return false. - * @return array the base paths - */ - protected function resolveBasePath() - { - if (!empty($this->basePath)) { - return (array)$this->basePath; - } elseif ($this->context instanceof Controller) { - return array($this->context->module->getViewPath() . '/' . $this->context->getUniqueId()); - } elseif ($this->context !== null) { - $class = new \ReflectionClass($this->context); - return array(dirname($class->getFileName()) . '/views'); - } else { - return array(); - } - } - - /** - * Finds the layout file for the current [[context]]. - * The method will return false if [[context]] is not a controller. - * When [[context]] is a controller, the following algorithm is used to determine the layout file: + * 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: * - * - If `context.layout` is false, it will return false; - * - If `context.layout` is a string, it will look for the layout file under the [[Module::layoutPath|layout path]] - * of the controller's parent module; - * - If `context.layout` is null, the following steps are taken to resolve the actual layout to be returned: - * * Check the `layout` property of the parent module. If it is null, check the grand parent module and so on - * until a non-null layout is encountered. Let's call this module the *effective module*. - * * If the layout is null or false, it will return false; - * * Otherwise, it will look for the layout file under the layout path of the effective module. + * - 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. * - * The themed layout file will be returned if theme is enabled and the theme contains such a layout file. + * If the layout name does not contain a file extension, it will default to `.php`. * - * @return string|boolean the layout file path, or false if the context does not need layout. - * @throws InvalidCallException if the layout file cannot be found + * If [[enableTheme]] is true and there is an active application them, the method will also + * attempt to use a themed version of the layout file, when available. + * + * And if [[enableI18N]] is true, the method will attempt to use a translated version of the layout file, + * when available. + * + * @return string|boolean the layout file path, or false if layout is not needed. + * @throws InvalidConfigException if the layout file cannot be found */ public function findLayoutFile() { - if (!$this->context instanceof Controller || $this->context->layout === false) { - return false; - } - $module = $this->context->module; - while ($module !== null && $module->layout === null) { - $module = $module->module; + /** @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 ($module === null || $module->layout === null || $module->layout === false) { + + if (!isset($view)) { return false; } - $view = $module->layout; - if (($extension = FileHelper::getExtension($view)) === '') { + if (FileHelper::getExtension($view) === '') { $view .= '.php'; } if (strncmp($view, '@', 1) === 0) { - $file = \Yii::getAlias($view); + if (($file = Yii::getAlias($view)) === false) { + throw new InvalidConfigException("Invalid path alias: $view"); + } } elseif (strncmp($view, '/', 1) === 0) { - $file = $this->findAbsoluteViewFile($view); + $file = Yii::$application->getLayoutPath() . DIRECTORY_SEPARATOR . $view; } else { - if ($this->themeView && ($theme = \Yii::$application->getTheme()) !== null) { - $paths[] = $theme->getLayoutPath($module); - } - $paths[] = $module->getLayoutPath(); - $file = false; - foreach ($paths as $path) { - $f = \Yii::getAlias($path . '/' . $view); - if ($f !== false && is_file($f)) { - $file = $f; - break; - } - } + $file = $module->getLayoutPath() . DIRECTORY_SEPARATOR . $view; } - if ($file === false || !is_file($file)) { - throw new InvalidCallException("Unable to find the layout file for layout '{$module->layout}' (specified by " . get_class($module) . ")"); - } elseif ($this->localizeView) { - return FileHelper::localize($file, $this->language, $this->sourceLanguage); + + 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 { - return $file; + throw new InvalidConfigException("Layout file for layout '$view' does not exist: $file"); } } } \ No newline at end of file diff --git a/framework/base/Widget.php b/framework/base/Widget.php index 3608205..bdec634 100644 --- a/framework/base/Widget.php +++ b/framework/base/Widget.php @@ -49,7 +49,7 @@ class Widget extends Component public function getId($autoGenerate = true) { if ($autoGenerate && $this->_id === null) { - $this->_id = 'yw' . self::$_counter++; + $this->_id = 'w' . self::$_counter++; } return $this->_id; } @@ -80,7 +80,7 @@ class Widget extends Component * To determine which view file should be rendered, the method calls [[findViewFile()]] which * will search in the directories as specified by [[basePath]]. * - * View name can be a path alias representing an absolute file path (e.g. `@app/views/layout/index`), + * View name can be a path alias representing an absolute file path (e.g. `@application/views/layout/index`), * or a path relative to [[basePath]]. The file suffix is optional and defaults to `.php` if not given * in the view name. * @@ -102,4 +102,16 @@ class Widget extends Component { return new View($this); } + + /** + * Returns the directory containing the view files for this widget. + * The default implementation returns the 'views' subdirectory under the directory containing the widget class file. + * @return string the directory containing the view files for this widget. + */ + public function getViewPath() + { + $className = get_class($this); + $class = new \ReflectionClass($className); + return dirname($class->getFileName()) . DIRECTORY_SEPARATOR . 'views'; + } } \ No newline at end of file diff --git a/framework/console/Application.php b/framework/console/Application.php index 23d80e0..237be05 100644 --- a/framework/console/Application.php +++ b/framework/console/Application.php @@ -10,7 +10,7 @@ namespace yii\console; use yii\base\Exception; -use yii\util\ReflectionHelper; +use yii\base\InvalidRouteException; /** * Application represents a console application. @@ -85,40 +85,36 @@ class Application extends \yii\base\Application * Processes the request. * The request is represented in terms of a controller route and action parameters. * @return integer the exit status of the controller action (0 means normal, non-zero values mean abnormal) - * @throws Exception if the route cannot be resolved into a controller */ public function processRequest() { /** @var $request Request */ $request = $this->getRequest(); if ($request->getIsConsoleRequest()) { - return $this->runController($request->route, $request->params); + return $this->runAction($request->route, $request->params); } else { - die('This script must be run from the command line.'); + echo "Error: this script must be run from the command line."; + return 1; } } /** - * Runs a controller with the given route and parameters. - * @param string $route the route (e.g. `post/create`) - * @param array $params the parameters to be passed to the controller action - * @return integer the exit status (0 means normal, non-zero values mean abnormal) - * @throws Exception if the route cannot be resolved into a controller + * 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. */ - public function runController($route, $params = array()) + public function runAction($route, $params = array()) { - $result = $this->createController($route); - if ($result === false) { - throw new Exception(\Yii::t('yii', 'Unable to resolve the request.')); + try { + return parent::runAction($route, $params); + } catch (InvalidRouteException $e) { + echo "Error: unknown command \"$route\".\n"; + return 1; } - /** @var $controller \yii\console\Controller */ - list($controller, $action) = $result; - $priorController = $this->controller; - $this->controller = $controller; - $params = ReflectionHelper::initObjectWithParams($controller, $params); - $status = $controller->run($action, $params); - $this->controller = $priorController; - return $status; } /** @@ -152,4 +148,9 @@ class Application extends \yii\base\Application ), )); } + + public function usageError($message) + { + + } } diff --git a/framework/console/Controller.php b/framework/console/Controller.php index 250cefe..16968f2 100644 --- a/framework/console/Controller.php +++ b/framework/console/Controller.php @@ -9,8 +9,10 @@ namespace yii\console; +use Yii; use yii\base\Action; -use yii\base\Exception; +use yii\base\InvalidRequestException; +use yii\base\InvalidRouteException; /** * Controller is the base class of console command classes. @@ -30,72 +32,56 @@ use yii\base\Exception; class Controller extends \yii\base\Controller { /** - * This method is invoked when the request parameters do not satisfy the requirement of the specified action. - * The default implementation will throw an exception. - * @param Action $action the action being executed - * @param Exception $exception the exception about the invalid parameters + * @var boolean whether the call of [[confirm()]] requires a user input. + * If false, [[confirm()]] will always return true no matter what user enters or not. */ - public function invalidActionParams($action, $exception) - { - echo \Yii::t('yii', 'Error: {message}', array( - '{message}' => $exception->getMessage(), - )); - \Yii::$application->end(1); - } + public $interactive = true; /** - * This method is invoked when extra parameters are provided to an action while it is executed. - * @param Action $action the action being executed - * @param array $expected the expected action parameters (name => value) - * @param array $actual the actual action parameters (name => value) + * Runs an action with the specified action ID and parameters. + * If the action ID is empty, the method will use [[defaultAction]]. + * @param string $id the ID of the action to be executed. + * @param array $params the parameters (name-value pairs) to be passed to the action. + * @return integer the status of the action execution. 0 means normal, other values mean abnormal. + * @throws InvalidRouteException if the requested action ID cannot be resolved into an action successfully. + * @see createAction */ - public function extraActionParams($action, $expected, $actual) + public function runAction($id, $params = array()) { - unset($expected['args'], $actual['args']); - - $keys = array_diff(array_keys($actual), array_keys($expected)); - if (!empty($keys)) { - echo \Yii::t('yii', 'Error: Unknown parameter(s): {params}', array( - '{params}' => implode(', ', $keys), - )) . "\n"; - \Yii::$application->end(1); + if ($params !== array()) { + $class = new \ReflectionClass($this); + 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]); + } + } + } } + return parent::runAction($id, $params); } /** - * Reads input via the readline PHP extension if that's available, or fgets() if readline is not installed. - * - * @param string $message to echo out before waiting for user input - * @param string $default the default string to be returned when user does not write anything. - * Defaults to null, means that default string is disabled. - * @return mixed line read as a string, or false if input has been closed + * 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 */ - public function prompt($message, $default = null) + public function validateActionParams($action, $missingParams, $unknownParams) { - if($default !== null) { - $message .= " [$default] "; - } - else { - $message .= ' '; - } - - if(extension_loaded('readline')) { - $input = readline($message); - if($input !== false) { - readline_add_history($input); - } - } - else { - echo $message; - $input = fgets(STDIN); - } - - if($input === false) { - return false; - } - else { - $input = trim($input); - return ($input === '' && $default !== null) ? $default : $input; + if (!empty($missingParams)) { + throw new InvalidRequestException(Yii::t('yii', 'Missing required options: {params}', array( + '{params}' => implode(', ', $missingParams), + ))); + } elseif (!empty($unknownParams)) { + throw new InvalidRequestException(Yii::t('yii', 'Unknown options: {params}', array( + '{params}' => implode(', ', $unknownParams), + ))); } } @@ -108,9 +94,23 @@ class Controller extends \yii\base\Controller */ public function confirm($message, $default = false) { - echo $message . ' (yes|no) [' . ($default ? 'yes' : 'no') . ']:'; + if ($this->interactive) { + echo $message . ' (yes|no) [' . ($default ? 'yes' : 'no') . ']:'; + $input = trim(fgets(STDIN)); + return empty($input) ? $default : !strncasecmp($input, 'y', 1); + } else { + return true; + } + } + + public function usageError($message) + { + echo "\nError: $message\n"; + Yii::$application->end(1); + } - $input = trim(fgets(STDIN)); - return empty($input) ? $default : !strncasecmp($input, 'y', 1); + public function globalOptions() + { + return array(); } } \ No newline at end of file diff --git a/framework/console/controllers/CreateController.php b/framework/console/controllers/CreateController.php index a513e40..7bd7fd0 100644 --- a/framework/console/controllers/CreateController.php +++ b/framework/console/controllers/CreateController.php @@ -165,8 +165,8 @@ class CreateController extends Controller } /** - * @param string $path1 abosolute path - * @param string $path2 abosolute path + * @param string $path1 absolute path + * @param string $path2 absolute path * * @return string relative path */ diff --git a/framework/console/controllers/HelpController.php b/framework/console/controllers/HelpController.php index f4d1eb8..6e4b397 100644 --- a/framework/console/controllers/HelpController.php +++ b/framework/console/controllers/HelpController.php @@ -12,6 +12,7 @@ namespace yii\console\controllers; use yii\base\Application; use yii\base\InlineAction; use yii\console\Controller; +use yii\util\StringHelper; /** * This command provides help information about console commands. @@ -54,16 +55,16 @@ class HelpController extends Controller } else { $result = \Yii::$application->createController($args[0]); if ($result === false) { - echo "Unknown command: " . $args[0] . "\n"; + echo "Error: no help for unknown command \"{$args[0]}\".\n"; return 1; } - list($controller, $action) = $result; + list($controller, $actionID) = $result; - if ($action === '') { + if ($actionID === '') { $status = $this->getControllerHelp($controller); } else { - $status = $this->getActionHelp($controller, $action); + $status = $this->getActionHelp($controller, $actionID); } } return $status; @@ -87,13 +88,13 @@ class HelpController extends Controller */ public function getActions($controller) { - $actions = array_keys($controller->actionMap); + $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) { - $actions[] = lcfirst(substr($name, 6)); + if ($method->isPublic() && !$method->isStatic() && strpos($name, 'action') === 0 && $name !== 'actions') { + $actions[] = StringHelper::camel2id(substr($name, 6)); } } sort($actions); @@ -107,11 +108,7 @@ class HelpController extends Controller */ protected function getModuleCommands($module) { - if ($module instanceof Application) { - $prefix = ''; - } else { - $prefix = $module->getUniqueId() . '/'; - } + $prefix = $module instanceof Application ? '' : $module->getUniqueID() . '/'; $commands = array(); foreach (array_keys($module->controllerMap) as $id) { @@ -145,12 +142,12 @@ class HelpController extends Controller { $commands = $this->getCommands(); if ($commands !== array()) { - echo "\n Usage: yiic [...options...]\n\n"; - echo "The following commands are available:\n"; + 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 individual command help, enter:\n"; + echo "\nTo see the help of each command, enter:\n"; echo "\n yiic help \n"; } else { echo "\nNo commands are found.\n"; @@ -195,7 +192,7 @@ class HelpController extends Controller $prefix = $controller->getUniqueId(); foreach ($actions as $action) { if ($controller->defaultAction === $action) { - echo " * $prefix/$action (default)\n"; + echo " * $prefix (default)\n"; } else { echo " * $prefix/$action\n"; } @@ -216,7 +213,7 @@ class HelpController extends Controller { $action = $controller->createAction($actionID); if ($action === null) { - echo "Unknown sub-command: " . $controller->getUniqueId() . "/$actionID\n"; + echo 'Error: no help for unknown sub-command "' . $controller->getUniqueId() . "/$actionID\".\n"; return 1; } if ($action instanceof InlineAction) { @@ -312,7 +309,7 @@ class HelpController extends Controller { $options = array(); foreach ($class->getProperties() as $property) { - if (!$property->isPublic() || $property->isStatic() || $property->getDeclaringClass()->getName() === 'yii\base\Controller') { + if (!$property->isPublic() || $property->isStatic() || $property->getDeclaringClass()->getName() !== get_class($controller)) { continue; } $name = $property->getName(); diff --git a/framework/console/controllers/MigrateController.php b/framework/console/controllers/MigrateController.php index 1da8001..e104856 100644 --- a/framework/console/controllers/MigrateController.php +++ b/framework/console/controllers/MigrateController.php @@ -10,6 +10,7 @@ namespace yii\console\controllers; +use Yii; use yii\console\Controller; /** @@ -60,25 +61,25 @@ use yii\console\Controller; */ class MigrateController extends Controller { - const BASE_MIGRATION='m000000_000000_base'; + 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'; + 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'; + 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'; + 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). @@ -88,26 +89,29 @@ class MigrateController extends Controller /** * @var string the default command action. It defaults to 'up'. */ - public $defaultAction='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 $interactive = true; + public function beforeAction($action) { - $path = \Yii::getAlias($this->migrationPath); - if($path===false || !is_dir($path)) { - echo 'Error: The migration directory does not exist: ' . $this->migrationPath . "\n"; - \Yii::$application->end(1); + 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; } - $this->migrationPath=$path; - - $yiiVersion = \Yii::getVersion(); - echo "\nYii Migration Tool v2.0 (based on Yii v{$yiiVersion})\n\n"; - - return parent::beforeAction($action); } /** @@ -115,34 +119,32 @@ class MigrateController extends Controller */ public function actionUp($args) { - if(($migrations = $this->getNewMigrations())===array()) - { + if (($migrations = $this->getNewMigrations()) === array()) { echo "No new migration found. Your system is up-to-date.\n"; - \Yii::$application->end(); + Yii::$application->end(); } - $total=count($migrations); - $step=isset($args[0]) ? (int)$args[0] : 0; - if($step>0) { - $migrations=array_slice($migrations,0,$step); + $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"; + $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) + 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) - { + 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; } @@ -153,29 +155,27 @@ class MigrateController extends Controller public function actionDown($args) { - $step=isset($args[0]) ? (int)$args[0] : 1; - if($step<1) + $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()) - { + if (($migrations = $this->getMigrationHistory($step)) === array()) { echo "No migration has been done before.\n"; return; } - $migrations=array_keys($migrations); + $migrations = array_keys($migrations); - $n=count($migrations); - echo "Total $n ".($n===1 ? 'migration':'migrations')." to be reverted:\n"; - foreach($migrations as $migration) + $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) - { + 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; } @@ -186,37 +186,33 @@ class MigrateController extends Controller public function actionRedo($args) { - $step=isset($args[0]) ? (int)$args[0] : 1; - if($step<1) + $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()) - { + if (($migrations = $this->getMigrationHistory($step)) === array()) { echo "No migration has been done before.\n"; return; } - $migrations=array_keys($migrations); + $migrations = array_keys($migrations); - $n=count($migrations); - echo "Total $n ".($n===1 ? 'migration':'migrations')." to be redone:\n"; - foreach($migrations as $migration) + $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) - { + 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) - { + foreach (array_reverse($migrations) as $migration) { + if ($this->migrateUp($migration) === false) { echo "\nMigration failed. All later migrations are canceled.\n"; return; } @@ -227,38 +223,37 @@ class MigrateController extends Controller public function actionTo($args) { - if(isset($args[0])) - $version=$args[0]; - else + 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 + $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)); + $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) + $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 + } else { $this->actionDown(array($i)); + } return; } } @@ -268,32 +263,30 @@ class MigrateController extends Controller public function actionMark($args) { - if(isset($args[0])) - $version=$args[0]; - else + 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 + } + $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(); + $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) - { + $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(), + 'version' => $migrations[$j], + 'apply_time' => time(), )); } echo "The migration history is set at $originalVersion.\nNo actual migration was performed.\n"; @@ -303,20 +296,17 @@ class MigrateController extends Controller } // try mark down - $migrations=array_keys($this->getMigrationHistory(-1)); - foreach($migrations as $i=>$migration) - { - if(strpos($migration,$version.'_')===0) - { - if($i===0) + $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])); + } 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"; } } @@ -329,125 +319,115 @@ class MigrateController extends Controller public function actionHistory($args) { - $limit=isset($args[0]) ? (int)$args[0] : -1; - $migrations=$this->getMigrationHistory($limit); - if($migrations===array()) + $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"; + } 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()) + $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 { + $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"; } - else - echo "Found $n new ".($n===1 ? 'migration' : 'migrations').":\n"; - foreach($migrations as $migration) - echo " ".$migration."\n"; + foreach ($migrations as $migration) { + echo " " . $migration . "\n"; + } } } public function actionCreate($args) { - if(isset($args[0])) - $name=$args[0]; - else + if (isset($args[0])) { + $name = $args[0]; + } else { $this->usageError('Please provide the name of the new migration.'); + } - if(!preg_match('/^\w+$/',$name)) + 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'; + $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'?")) - { + if ($this->confirm("Create new migration '$file'?")) { file_put_contents($file, $content); echo "New migration created successfully.\n"; } } - public function confirm($message) - { - if(!$this->interactive) - return true; - return parent::confirm($message); - } - protected function migrateUp($class) { - if($class===self::BASE_MIGRATION) + if ($class === self::BASE_MIGRATION) { return; + } echo "*** applying $class\n"; - $start=microtime(true); - $migration=$this->instantiateMigration($class); - if($migration->up()!==false) - { + $start = microtime(true); + $migration = $this->instantiateMigration($class); + if ($migration->up() !== false) { $this->getDb()->createCommand()->insert($this->migrationTable, array( - 'version'=>$class, - 'apply_time'=>time(), + '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"; + $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) + 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"; + $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'; + $file = $this->migrationPath . DIRECTORY_SEPARATOR . $class . '.php'; require_once($file); - $migration=new $class; + $migration = new $class; $migration->setDb($this->getDb()); return $migration; } @@ -456,21 +436,24 @@ class MigrateController extends Controller * @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) + if ($this->_db !== null) { 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"); + } 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) - { + $db = $this->getDb(); + if ($db->schema->getTable($this->migrationTable) === null) { $this->createMigrationHistoryTable(); } return CHtml::listData($db->createCommand() @@ -483,34 +466,36 @@ class MigrateController extends Controller 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 = $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(), + $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==='..') + $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]; + } + $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); @@ -519,9 +504,9 @@ class MigrateController extends Controller protected function getTemplate() { - if($this->templateFile!==null) - return file_get_contents(Yii::getPathOfAlias($this->templateFile).'.php'); - else + if ($this->templateFile !== null) { + return file_get_contents(Yii::getPathOfAlias($this->templateFile) . '.php'); + } else { return << - * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -namespace yii\console\controllers; - -use yii\console\Controller; - -/** - * This command executes the specified Web application and provides a shell for interaction. - * - * @property string $help The help information for the shell command. - * - * @author Qiang Xue - * @since 2.0 - */ -class ShellController extends Controller -{ - /** - * @return string the help information for the shell command - */ - public function getHelp() - { - return <<usageError("{$args[0]} does not exist or is not an entry script file."); - - // fake the web server setting - $cwd=getcwd(); - chdir(dirname($entryScript)); - $_SERVER['SCRIPT_NAME']='/'.basename($entryScript); - $_SERVER['REQUEST_URI']=$_SERVER['SCRIPT_NAME']; - $_SERVER['SCRIPT_FILENAME']=$entryScript; - $_SERVER['HTTP_HOST']='localhost'; - $_SERVER['SERVER_NAME']='localhost'; - $_SERVER['SERVER_PORT']=80; - - // reset context to run the web application - restore_error_handler(); - restore_exception_handler(); - Yii::setApplication(null); - Yii::setPathOfAlias('application',null); - - ob_start(); - $config=require($entryScript); - ob_end_clean(); - - // oops, the entry script turns out to be a config file - if(is_array($config)) - { - chdir($cwd); - $_SERVER['SCRIPT_NAME']='/index.php'; - $_SERVER['REQUEST_URI']=$_SERVER['SCRIPT_NAME']; - $_SERVER['SCRIPT_FILENAME']=$cwd.DIRECTORY_SEPARATOR.'index.php'; - Yii::createWebApplication($config); - } - - restore_error_handler(); - restore_exception_handler(); - - $yiiVersion=Yii::getVersion(); - echo <<runShell(); - } - - protected function runShell() - { - // disable E_NOTICE so that the shell is more friendly - error_reporting(E_ALL ^ E_NOTICE); - - $_runner_=new CConsoleCommandRunner; - $_runner_->addCommands(dirname(__FILE__).'/shell'); - $_runner_->addCommands(Yii::getPathOfAlias('application.commands.shell')); - if(($_path_=@getenv('YIIC_SHELL_COMMAND_PATH'))!==false) - $_runner_->addCommands($_path_); - $_commands_=$_runner_->commands; - $log=\Yii::$application->log; - - while(($_line_=$this->prompt("\n>>"))!==false) - { - $_line_=trim($_line_); - if($_line_==='exit') - return; - try - { - $_args_=preg_split('/[\s,]+/',rtrim($_line_,';'),-1,PREG_SPLIT_NO_EMPTY); - if(isset($_args_[0]) && isset($_commands_[$_args_[0]])) - { - $_command_=$_runner_->createCommand($_args_[0]); - array_shift($_args_); - $_command_->init(); - $_command_->run($_args_); - } - else - echo eval($_line_.';'); - } - catch(Exception $e) - { - if($e instanceof ShellException) - echo $e->getMessage(); - else - echo $e; - } - } - } -} - -class ShellException extends CException -{ -} \ No newline at end of file diff --git a/framework/db/Command.php b/framework/db/Command.php index 0fb099a..3531fa7 100644 --- a/framework/db/Command.php +++ b/framework/db/Command.php @@ -485,16 +485,41 @@ class Command extends \yii\base\Component * * @param string $table the table that new rows will be inserted into. * @param array $columns the column data (name=>value) to be inserted into the table. - * @param array $params the parameters to be bound to the command * @return Command the command object itself */ - public function insert($table, $columns, $params = array()) + public function insert($table, $columns) { + $params = array(); $sql = $this->db->getQueryBuilder()->insert($table, $columns, $params); return $this->setSql($sql)->bindValues($params); } /** + * Creates a batch INSERT command. + * For example, + * + * ~~~ + * $connection->createCommand()->batchInsert('tbl_user', array('name', 'age'), array( + * array('Tom', 30), + * array('Jane', 20), + * array('Linda', 25), + * ))->execute(); + * ~~~ + * + * Not that the values in each row must match the corresponding column names. + * + * @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 + * @return Command the command object itself + */ + public function batchInsert($table, $columns, $rows) + { + $sql = $this->db->getQueryBuilder()->batchInsert($table, $columns, $rows); + return $this->setSql($sql); + } + + /** * Creates an UPDATE command. * For example, * diff --git a/framework/db/Exception.php b/framework/db/Exception.php index bdc1277..209dc40 100644 --- a/framework/db/Exception.php +++ b/framework/db/Exception.php @@ -34,4 +34,12 @@ class Exception extends \yii\base\Exception $this->errorInfo = $errorInfo; parent::__construct($message, $code); } + + /** + * @return string the user-friendly name of this exception + */ + public function getName() + { + return \Yii::t('yii', 'Database Exception'); + } } \ No newline at end of file diff --git a/framework/db/QueryBuilder.php b/framework/db/QueryBuilder.php index d40da91..35bfcb3 100644 --- a/framework/db/QueryBuilder.php +++ b/framework/db/QueryBuilder.php @@ -115,6 +115,33 @@ class QueryBuilder extends \yii\base\Object } /** + * Generates a batch INSERT SQL statement. + * For example, + * + * ~~~ + * $connection->createCommand()->batchInsert('tbl_user', array('name', 'age'), array( + * array('Tom', 30), + * array('Jane', 20), + * array('Linda', 25), + * ))->execute(); + * ~~~ + * + * Not that the values in each row must match the corresponding column names. + * + * @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()) + { + throw new NotSupportedException($this->db->getDriverName() . ' does not support batch insert.'); + + } + + /** * Creates an UPDATE SQL statement. * For example, * diff --git a/framework/db/mysql/QueryBuilder.php b/framework/db/mysql/QueryBuilder.php index 73986af..6168409 100644 --- a/framework/db/mysql/QueryBuilder.php +++ b/framework/db/mysql/QueryBuilder.php @@ -129,4 +129,39 @@ class QueryBuilder extends \yii\db\QueryBuilder { return 'SET FOREIGN_KEY_CHECKS=' . ($check ? 1 : 0); } + + /** + * Generates a batch INSERT SQL statement. + * For example, + * + * ~~~ + * $connection->createCommand()->batchInsert('tbl_user', array('name', 'age'), array( + * array('Tom', 30), + * array('Jane', 20), + * array('Linda', 25), + * ))->execute(); + * ~~~ + * + * Not that the values in each row must match the corresponding column names. + * + * @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 + * @return string the batch INSERT SQL statement + */ + public function batchInsert($table, $columns, $rows) + { + $values = array(); + foreach ($rows as $row) { + $vs = array(); + foreach ($row as $value) { + $vs[] = is_string($value) ? $this->db->quoteValue($value) : $value; + } + $values[] = $vs; + } + + return 'INSERT INTO ' . $this->db->quoteTableName($table) + . ' (' . implode(', ', $columns) . ') VALUES (' + . implode(', ', $values) . ')'; + } } diff --git a/framework/logging/DbTarget.php b/framework/logging/DbTarget.php index 004bf21..129e4d4 100644 --- a/framework/logging/DbTarget.php +++ b/framework/logging/DbTarget.php @@ -89,16 +89,17 @@ class DbTarget extends Target } /** - * Stores log [[messages]] to DB. - * @param boolean $final whether this method is called at the end of the current application + * Stores log messages to DB. + * @param array $messages the messages to be exported. See [[Logger::messages]] for the structure + * of each message. */ - public function exportMessages($final) + public function export($messages) { $db = $this->getDb(); $tableName = $db->quoteTableName($this->tableName); $sql = "INSERT INTO $tableName (level, category, log_time, message) VALUES (:level, :category, :log_time, :message)"; $command = $db->createCommand($sql); - foreach ($this->messages as $message) { + foreach ($messages as $message) { $command->bindValues(array( ':level' => $message[1], ':category' => $message[2], diff --git a/framework/logging/EmailTarget.php b/framework/logging/EmailTarget.php index 73fd3bb..e02e4da 100644 --- a/framework/logging/EmailTarget.php +++ b/framework/logging/EmailTarget.php @@ -39,13 +39,14 @@ class EmailTarget extends Target public $headers = array(); /** - * Sends log [[messages]] to specified email addresses. - * @param boolean $final whether this method is called at the end of the current application + * Sends log messages to specified email addresses. + * @param array $messages the messages to be exported. See [[Logger::messages]] for the structure + * of each message. */ - public function exportMessages($final) + public function export($messages) { $body = ''; - foreach ($this->messages as $message) { + foreach ($messages as $message) { $body .= $this->formatMessage($message); } $body = wordwrap($body, 70); diff --git a/framework/logging/FileTarget.php b/framework/logging/FileTarget.php index f4ddf44..0eb897e 100644 --- a/framework/logging/FileTarget.php +++ b/framework/logging/FileTarget.php @@ -65,19 +65,28 @@ class FileTarget extends Target } /** - * Sends log [[messages]] to specified email addresses. - * @param boolean $final whether this method is called at the end of the current application + * Sends log messages to specified email addresses. + * @param array $messages the messages to be exported. See [[Logger::messages]] for the structure + * of each message. */ - public function exportMessages($final) + public function export($messages) { + $text = ''; + foreach ($messages as $message) { + $text .= $this->formatMessage($message); + } + $fp = @fopen($this->logFile, 'a'); + @flock($fp, LOCK_EX); if (@filesize($this->logFile) > $this->maxFileSize * 1024) { $this->rotateFiles(); + @flock($fp,LOCK_UN); + @fclose($fp); + @file_put_contents($this->logFile, $text, FILE_APPEND | LOCK_EX); + } else { + @fwrite($fp, $text); + @flock($fp,LOCK_UN); + @fclose($fp); } - $messages = array(); - foreach ($this->messages as $message) { - $messages[] = $this->formatMessage($message); - } - @file_put_contents($this->logFile, implode('', $messages), FILE_APPEND | LOCK_EX); } /** diff --git a/framework/logging/Logger.php b/framework/logging/Logger.php index c139193..a8ffb5e 100644 --- a/framework/logging/Logger.php +++ b/framework/logging/Logger.php @@ -8,16 +8,13 @@ */ namespace yii\logging; - -use yii\base\Event; -use yii\base\Exception; +use yii\base\InvalidConfigException; /** * Logger records logged messages in memory. * - * When [[flushInterval()]] is reached or when application terminates, it will - * call [[flush()]] to send logged messages to different log targets, such as - * file, email, Web. + * When the application ends or [[flushInterval]] is reached, Logger will call [[flush()]] + * to send logged messages to different log targets, such as file, email, Web. * * @author Qiang Xue * @since 2.0 @@ -25,15 +22,6 @@ use yii\base\Exception; class Logger extends \yii\base\Component { /** - * @event Event an event that is triggered when [[flush()]] is called. - */ - const EVENT_FLUSH = 'flush'; - /** - * @event Event an event that is triggered when [[flush()]] is called at the end of application. - */ - const EVENT_FINAL_FLUSH = 'finalFlush'; - - /** * Error message level. An error message is one that indicates the abnormal termination of the * application and may require developer's handling. */ @@ -82,7 +70,7 @@ class Logger extends \yii\base\Component * * ~~~ * array( - * [0] => message (mixed) + * [0] => message (mixed, can be a string or some complex data, such as an exception object) * [1] => level (integer) * [2] => category (string) * [3] => timestamp (float, obtained by microtime(true)) @@ -90,6 +78,10 @@ class Logger extends \yii\base\Component * ~~~ */ public $messages = array(); + /** + * @var Router the log target router registered with this logger. + */ + public $router; /** * Initializes the logger by registering [[flush()]] as a shutdown function. @@ -138,7 +130,9 @@ class Logger extends \yii\base\Component */ public function flush($final = false) { - $this->trigger($final ? self::EVENT_FINAL_FLUSH : self::EVENT_FLUSH); + if ($this->router) { + $this->router->dispatch($this->messages, $final); + } $this->messages = array(); } @@ -149,7 +143,7 @@ class Logger extends \yii\base\Component * of [[YiiBase]] class file. * @return float the total elapsed time in seconds for current request. */ - public function getExecutionTime() + public function getElapsedTime() { return microtime(true) - YII_BEGIN_TIME; } @@ -218,7 +212,7 @@ class Logger extends \yii\base\Component if (($last = array_pop($stack)) !== null && $last[0] === $token) { $timings[] = array($token, $category, $timestamp - $last[3]); } else { - throw new Exception("Unmatched profiling block: $token"); + throw new InvalidConfigException("Unmatched profiling block: $token"); } } } @@ -231,5 +225,4 @@ class Logger extends \yii\base\Component return $timings; } - } diff --git a/framework/logging/Router.php b/framework/logging/Router.php index 75fbbc0..2e6a8dd 100644 --- a/framework/logging/Router.php +++ b/framework/logging/Router.php @@ -81,26 +81,21 @@ class Router extends Component $this->targets[$name] = Yii::createObject($target); } } - - Yii::getLogger()->on(Logger::EVENT_FLUSH, array($this, 'processMessages')); - Yii::getLogger()->on(Logger::EVENT_FINAL_FLUSH, array($this, 'processMessages')); + Yii::getLogger()->router = $this; } /** - * Retrieves and processes log messages from the system logger. - * This method mainly serves the event handler to the [[Logger::EVENT_FLUSH]] event - * and the [[Logger::EVENT_FINAL_FLUSH]] event. - * It will retrieve the available log messages from the [[Yii::getLogger()|system logger]] - * and invoke the registered [[targets|log targets]] to do the actual processing. - * @param \yii\base\Event $event event parameter + * Dispatches log messages to [[targets]]. + * This method is called by [[Logger]] when its [[Logger::flush()]] method is called. + * It will forward the messages to each log target registered in [[targets]]. + * @param array $messages the messages to be processed + * @param boolean $final whether this is the final call during a request cycle */ - public function processMessages($event) + public function dispatch($messages, $final = false) { - $messages = Yii::getLogger()->messages; - $final = $event->name === Logger::EVENT_FINAL_FLUSH; foreach ($this->targets as $target) { if ($target->enabled) { - $target->processMessages($messages, $final); + $target->collect($messages, $final); } } } diff --git a/framework/logging/Target.php b/framework/logging/Target.php index a4e7714..c9e175a 100644 --- a/framework/logging/Target.php +++ b/framework/logging/Target.php @@ -50,15 +50,6 @@ abstract class Target extends \yii\base\Component */ public $except = array(); /** - * @var boolean whether to prefix each log message with the current session ID. Defaults to false. - */ - public $prefixSession = false; - /** - * @var boolean whether to prefix each log message with the current user name and ID. Defaults to false. - * @see \yii\web\User - */ - public $prefixUser = false; - /** * @var boolean whether to log a message containing the current user name and ID. Defaults to false. * @see \yii\web\User */ @@ -77,19 +68,18 @@ abstract class Target extends \yii\base\Component public $exportInterval = 1000; /** * @var array the messages that are retrieved from the logger so far by this log target. - * @see autoExport */ - public $messages = array(); + private $_messages = array(); private $_levels = 0; /** * Exports log messages to a specific destination. - * Child classes must implement this method. Note that you may need - * to clean up [[messages]] in this method to avoid re-exporting messages. - * @param boolean $final whether this method is called at the end of the current application + * Child classes must implement this method. + * @param array $messages the messages to be exported. See [[Logger::messages]] for the structure + * of each message. */ - abstract public function exportMessages($final); + abstract public function export($messages); /** * Processes the given log messages. @@ -99,45 +89,16 @@ abstract class Target extends \yii\base\Component * of each message. * @param boolean $final whether this method is called at the end of the current application */ - public function processMessages($messages, $final) + public function collect($messages, $final) { - $messages = $this->filterMessages($messages); - $this->messages = array_merge($this->messages, $messages); - - $count = count($this->messages); + $this->_messages = array($this->_messages, $this->filterMessages($messages)); + $count = count($this->_messages); if ($count > 0 && ($final || $this->exportInterval > 0 && $count >= $this->exportInterval)) { - $this->prepareExport($final); - $this->exportMessages($final); - $this->messages = array(); - } - } - - /** - * Prepares the [[messages]] for exporting. - * This method will modify each message by prepending extra information - * if [[prefixSession]] and/or [[prefixUser]] are set true. - * It will also add an additional message showing context information if - * [[logUser]] and/or [[logVars]] are set. - * @param boolean $final whether this method is called at the end of the current application - */ - protected function prepareExport($final) - { - $prefix = array(); - if ($this->prefixSession && ($id = session_id()) !== '') { - $prefix[] = "[$id]"; - } - if ($this->prefixUser && ($user = \Yii::$application->getComponent('user', false)) !== null) { - $prefix[] = '[' . $user->getName() . ']'; - $prefix[] = '[' . $user->getId() . ']'; - } - if ($prefix !== array()) { - $prefix = implode(' ', $prefix); - foreach ($this->messages as $i => $message) { - $this->messages[$i][0] = $prefix . ' ' . $this->messages[$i][0]; + if (($context = $this->getContextMessage()) !== '') { + $this->_messages[] = array($context, Logger::LEVEL_INFO, 'application', YII_BEGIN_TIME); } - } - if ($final && ($context = $this->getContextMessage()) !== '') { - $this->messages[] = array($context, Logger::LEVEL_INFO, 'application', YII_BEGIN_TIME); + $this->export($this->_messages); + $this->_messages = array(); } } @@ -164,7 +125,7 @@ abstract class Target extends \yii\base\Component /** * @return integer the message levels that this target is interested in. This is a bitmap of - * level values. Defaults to 0, meaning all available levels. + * level values. Defaults to 0, meaning all available levels. */ public function getLevels() { diff --git a/framework/util/FileHelper.php b/framework/util/FileHelper.php index b0b0611..c65e4f0 100644 --- a/framework/util/FileHelper.php +++ b/framework/util/FileHelper.php @@ -10,6 +10,7 @@ namespace yii\util; use yii\base\Exception; +use yii\base\InvalidConfigException; /** * Filesystem helper @@ -37,7 +38,7 @@ class FileHelper * If the given path does not refer to an existing directory, an exception will be thrown. * @param string $path the given path. This can also be a path alias. * @return string the normalized path - * @throws Exception if the path does not refer to an existing directory. + * @throws InvalidConfigException if the path does not refer to an existing directory. */ public static function ensureDirectory($path) { @@ -45,11 +46,25 @@ class FileHelper if ($p !== false && ($p = realpath($p)) !== false && is_dir($p)) { return $p; } else { - throw new Exception('Directory does not exist: ' . $path); + throw new InvalidConfigException('Directory does not exist: ' . $path); } } /** + * Normalizes a file/directory path. + * After normalization, the directory separators in the path will be `DIRECTORY_SEPARATOR`, + * and any trailing directory separators will be removed. For example, '/home\demo/' on Linux + * will be normalized as '/home/demo'. + * @param string $path the file/directory path to be normalized + * @param string $ds the directory separator to be used in the normalized result. Defaults to `DIRECTORY_SEPARATOR`. + * @return string the normalized file/directory path + */ + public static function normalizePath($path, $ds = DIRECTORY_SEPARATOR) + { + return rtrim(strtr($path, array('/' => $ds, '\\' => $ds)), $ds); + } + + /** * Returns the localized version of a specified file. * * The searching is based on the specified language code. In particular, diff --git a/framework/util/ReflectionHelper.php b/framework/util/ReflectionHelper.php deleted file mode 100644 index cc13f94..0000000 --- a/framework/util/ReflectionHelper.php +++ /dev/null @@ -1,103 +0,0 @@ - - * @since 2.0 - */ -class ReflectionHelper -{ - /** - * Prepares parameters so that they can be bound to the specified method. - * This method converts the input parameters into an array that can later be - * passed to `call_user_func_array()` when calling the specified method. - * The conversion is based on the matching of method parameter names - * and the input array keys. For example, - * - * ~~~ - * class Foo { - * function bar($a, $b) { ... } - * } - * $object = new Foo; - * $params = array('b' => 2, 'c' => 3, 'a' => 1); - * var_export(ReflectionHelper::extractMethodParams($object, 'bar', $params)); - * // output: array('a' => 1, 'b' => 2); - * ~~~ - * - * @param object|string $object the object or class name that owns the specified method - * @param string $method the method name - * @param array $params the parameters in terms of name-value pairs - * @return array parameters that are needed by the method only and - * can be passed to the method via `call_user_func_array()`. - * @throws Exception if any required method parameter is not found in the given parameters - */ - public static function extractMethodParams($object, $method, $params) - { - $m = new \ReflectionMethod($object, $method); - $ps = array(); - foreach ($m->getParameters() as $param) { - $name = $param->getName(); - if (array_key_exists($name, $params)) { - $ps[$name] = $params[$name]; - } elseif ($param->isDefaultValueAvailable()) { - $ps[$name] = $param->getDefaultValue(); - } else { - throw new Exception(\Yii::t('yii', 'Missing required parameter "{name}".', array('{name}' => $name))); - } - } - return $ps; - } - - /** - * Initializes an object with the given parameters. - * Only the public non-static properties of the object will be initialized, and their names must - * match the given parameter names. For example, - * - * ~~~ - * class Foo { - * public $a; - * protected $b; - * } - * $object = new Foo; - * $params = array('b' => 2, 'c' => 3, 'a' => 1); - * $remaining = ReflectionHelper::bindObjectParams($object, $params); - * var_export($object); // output: $object->a = 1; $object->b = null; - * var_export($remaining); // output: array('b' => 2, 'c' => 3); - * ~~~ - * - * @param object $object the object whose properties are to be initialized - * @param array $params the input parameters to be used to initialize the object - * @return array the remaining unused input parameters - */ - public static function initObjectWithParams($object, $params) - { - if (empty($params)) { - return array(); - } - - $class = new \ReflectionClass(get_class($object)); - foreach ($params as $name => $value) { - if ($class->hasProperty($name)) { - $property = $class->getProperty($name); - if ($property->isPublic() && !$property->isStatic()) { - $object->$name = $value; - unset($params[$name]); - } - } - } - - return $params; - } -} diff --git a/framework/yiic.php b/framework/yiic.php index 0f05183..55b9e60 100644 --- a/framework/yiic.php +++ b/framework/yiic.php @@ -1,5 +1,4 @@ '@yii/console/controllers', -); $id = 'yiic'; $basePath = __DIR__ . '/console'; -$application = new yii\console\Application($id, $basePath, $config); +$application = new yii\console\Application($id, $basePath, array( + 'controllerPath' => '@yii/console/controllers', +)); $application->run(); diff --git a/tests/unit/bootstrap.php b/tests/unit/bootstrap.php index 4dc7f33..f60eee0 100644 --- a/tests/unit/bootstrap.php +++ b/tests/unit/bootstrap.php @@ -1,6 +1,5 @@