Browse Source

Merge branch 'master' of git.yiisoft.com:yii2

* 'master' of git.yiisoft.com:yii2:
  Added Command::batchInsert()
  exception cleanup.
  error handling cleanup.
  console app cleanup.
  MVC cleanup
  MVC WIP
  MVC WIP
  MVC WIP
  cleanup.
  MVC WIP
  refactored logging. MVC WIP
  MVC WIP
  patched controller and action creation
  todo updates.

Conflicts:
	framework/console/View.php
tags/2.0.0-beta
Carsten Brandt 12 years ago
parent
commit
c5816d233c
  1. 32
      framework/YiiBase.php
  2. 54
      framework/base/Action.php
  3. 7
      framework/base/ActionEvent.php
  4. 90
      framework/base/ActionFilter.php
  5. 366
      framework/base/Application.php
  6. 260
      framework/base/Controller.php
  7. 106
      framework/base/ErrorHandler.php
  8. 12
      framework/base/Exception.php
  9. 4
      framework/base/HttpException.php
  10. 37
      framework/base/InlineAction.php
  11. 7
      framework/base/InvalidCallException.php
  12. 7
      framework/base/InvalidConfigException.php
  13. 12
      framework/base/InvalidRequestException.php
  14. 33
      framework/base/InvalidRouteException.php
  15. 180
      framework/base/Module.php
  16. 7
      framework/base/NotSupportedException.php
  17. 105
      framework/base/Theme.php
  18. 7
      framework/base/UnknownMethodException.php
  19. 7
      framework/base/UnknownPropertyException.php
  20. 558
      framework/base/View.php
  21. 16
      framework/base/Widget.php
  22. 43
      framework/console/Application.php
  23. 118
      framework/console/Controller.php
  24. 4
      framework/console/controllers/CreateController.php
  25. 35
      framework/console/controllers/HelpController.php
  26. 432
      framework/console/controllers/MigrateController.php
  27. 149
      framework/console/controllers/ShellController.php
  28. 29
      framework/db/Command.php
  29. 8
      framework/db/Exception.php
  30. 27
      framework/db/QueryBuilder.php
  31. 35
      framework/db/mysql/QueryBuilder.php
  32. 9
      framework/logging/DbTarget.php
  33. 9
      framework/logging/EmailTarget.php
  34. 25
      framework/logging/FileTarget.php
  35. 33
      framework/logging/Logger.php
  36. 21
      framework/logging/Router.php
  37. 65
      framework/logging/Target.php
  38. 19
      framework/util/FileHelper.php
  39. 103
      framework/util/ReflectionHelper.php
  40. 10
      framework/yiic.php
  41. 1
      tests/unit/bootstrap.php
  42. 4
      todo.md

32
framework/YiiBase.php

@ -8,9 +8,8 @@
*/ */
use yii\base\Exception; use yii\base\Exception;
use yii\logging\Logger;
use yii\base\InvalidCallException;
use yii\base\InvalidConfigException; use yii\base\InvalidConfigException;
use yii\logging\Logger;
/** /**
* Gets the application start timestamp. * 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. * This constant defines the framework installation directory.
*/ */
defined('YII_PATH') or define('YII_PATH', __DIR__); 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. * 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): * 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; * - `@application/components/GoogleMap`: importing the `GoogleMap` class with a path alias;
* - `@app/components/*`: importing the whole `components` directory 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 * - `GoogleMap`: importing the `GoogleMap` class with a class name. [[autoload()]] will be used
* when this class is used for the first time. * 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. * Note, this method does not ensure the existence of the resulting path.
* @param string $alias alias * @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. * @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 * @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]; return self::$aliases[$alias];
} elseif ($alias === '' || $alias[0] !== '@') { // not an alias } elseif ($alias === '' || $alias[0] !== '@') { // not an alias
return $alias; return $alias;
@ -206,11 +210,7 @@ class YiiBase
return self::$aliases[$alias] = self::$aliases[$rootAlias] . substr($alias, $pos); return self::$aliases[$alias] = self::$aliases[$rootAlias] . substr($alias, $pos);
} }
} }
if ($throwException) { return false;
throw new Exception("Invalid path alias: $alias");
} else {
return false;
}
} }
/** /**
@ -322,12 +322,12 @@ class YiiBase
* the class. For example, * the class. For example,
* *
* - `\app\components\GoogleMap`: fully-qualified namespaced class. * - `\app\components\GoogleMap`: fully-qualified namespaced class.
* - `@app/components/GoogleMap`: an alias * - `@application/components/GoogleMap`: an alias
* *
* Below are some usage examples: * Below are some usage examples:
* *
* ~~~ * ~~~
* $object = \Yii::createObject('@app/components/GoogleMap'); * $object = \Yii::createObject('@application/components/GoogleMap');
* $object = \Yii::createObject(array( * $object = \Yii::createObject(array(
* 'class' => '\app\components\GoogleMap', * 'class' => '\app\components\GoogleMap',
* 'apiKey' => 'xyz', * 'apiKey' => 'xyz',
@ -361,7 +361,7 @@ class YiiBase
$class = $config['class']; $class = $config['class'];
unset($config['class']); unset($config['class']);
} else { } 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)) { if (!class_exists($class, false)) {

54
framework/base/Action.php

@ -9,8 +9,6 @@
namespace yii\base; namespace yii\base;
use yii\util\ReflectionHelper;
/** /**
* Action is the base class for all controller action classes. * 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. * will be invoked by the controller when the action is requested.
* The `run()` method can have parameters which will be filled up * The `run()` method can have parameters which will be filled up
* with user input values automatically according to their names. * 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 <qiang.xue@gmail.com> * @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0 * @since 2.0
@ -37,6 +43,7 @@ class Action extends Component
public $controller; public $controller;
/** /**
* Constructor.
* @param string $id the ID of this action * @param string $id the ID of this action
* @param Controller $controller the controller that owns 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 * @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. * Runs this action with the specified parameters.
* This method is mainly invoked by the controller. * 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). * @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) public function runWithParams($params)
{ {
try { if (!method_exists($this, 'run')) {
$ps = ReflectionHelper::extractMethodParams($this, 'run', $params); throw new InvalidConfigException(get_class($this) . ' must define a "run()" method.');
} catch (Exception $e) {
$this->controller->invalidActionParams($this, $e);
return 1;
} }
if ($params !== $ps) { $method = new \ReflectionMethod($this, 'run');
$this->controller->extraActionParams($this, $ps, $params); $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;
} }
} }

7
framework/base/ActionEvent.php

@ -12,8 +12,7 @@ namespace yii\base;
/** /**
* ActionEvent represents the event parameter used for an action event. * 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 * By setting the [[isValid]] property, one may control whether to continue running the action.
* the action currently being executed.
* *
* @author Qiang Xue <qiang.xue@gmail.com> * @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0 * @since 2.0
@ -25,7 +24,7 @@ class ActionEvent extends Event
*/ */
public $action; 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; public $isValid = true;
@ -34,7 +33,7 @@ class ActionEvent extends Event
* @param Action $action the action associated with this action 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 * @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; $this->action = $action;
parent::__construct($config); parent::__construct($config);

90
framework/base/ActionFilter.php

@ -1,90 +0,0 @@
<?php
/**
* ActionFilter class file.
*
* @link http://www.yiiframework.com/
* @copyright Copyright &copy; 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\base;
/**
* ActionFilter is the base class for all action filters.
*
* A filter can be applied to a controller action at different stages of its life cycle. In particular,
* it responds to the following events that are raised when an action is being executed:
*
* 1. authorize
* 2. beforeAction
* 3. beforeRender
* 4. afterRender
* 5. afterAction
*
* Derived classes may respond to these events by overriding the corresponding methods in this class.
* For example, to create an access control filter, one may override the [[authorize()]] method.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @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);
}
}

366
framework/base/Application.php

@ -9,8 +9,8 @@
namespace yii\base; namespace yii\base;
use yii\base\InvalidCallException; use Yii;
use yii\util\StringHelper; use yii\util\FileHelper;
/** /**
* Application is the base class for all application classes. * 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.</li> * Yii framework messages. This application component is dynamically loaded when needed.</li>
* </ul> * </ul>
* *
* Application will undergo the following lifecycles when processing a user request: * Application will undergo the following life cycles when processing a user request:
* <ol> * <ol>
* <li>load application configuration;</li> * <li>load application configuration;</li>
* <li>set up class autoloader and error handling;</li> * <li>set up class autoloader and error handling;</li>
@ -49,28 +49,6 @@ use yii\util\StringHelper;
* Starting from lifecycle 3, if a PHP error or an uncaught exception occurs, * 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. * 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 <qiang.xue@gmail.com> * @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0 * @since 2.0
*/ */
@ -97,11 +75,9 @@ class Application extends Module
*/ */
public $sourceLanguage = 'en_us'; public $sourceLanguage = 'en_us';
/** /**
* @var array IDs of application components that need to be loaded when the application starts. * @var array IDs of the 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.
*/ */
public $preload = array('errorHandler'); public $preload = array();
/** /**
* @var Controller the currently active controller instance * @var Controller the currently active controller instance
*/ */
@ -128,12 +104,19 @@ class Application extends Module
*/ */
public function __construct($id, $basePath, $config = array()) public function __construct($id, $basePath, $config = array())
{ {
\Yii::$application = $this; Yii::$application = $this;
$this->id = $id; $this->id = $id;
$this->setBasePath($basePath); $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->registerDefaultAliases();
$this->registerCoreComponents(); $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. * Returns the directory that stores runtime files.
* @return string the directory that stores runtime files. Defaults to 'protected/runtime'. * @return string the directory that stores runtime files. Defaults to 'protected/runtime'.
*/ */
@ -239,15 +200,15 @@ class Application extends Module
/** /**
* Sets the directory that stores runtime files. * Sets the directory that stores runtime files.
* @param string $path 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) public function setRuntimePath($path)
{ {
$p = \Yii::getAlias($path); $p = FileHelper::ensureDirectory($path);
if ($p === false || !is_dir($p) || !is_writable($path)) { if (is_writable($p)) {
throw new InvalidCallException("Application runtime path \"$path\" is invalid. Please make sure it is a directory writable by the Web server process.");
} else {
$this->_runtimePath = $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); date_default_timezone_set($value);
} }
/** // /**
* Returns the locale instance. // * Returns the security manager component.
* @param string $localeID the locale ID (e.g. en_US). If null, the {@link getLanguage application language ID} will be used. // * @return SecurityManager the security manager application component.
* @return CLocale the locale instance // */
*/ // public function getSecurityManager()
public function getLocale($localeID = null) // {
{ // return $this->getComponent('securityManager');
return CLocale::getInstance($localeID === null ? $this->getLanguage() : $localeID); // }
} //
// /**
/** // * Returns the locale instance.
* @return CNumberFormatter the locale-dependent number formatter. // * @param string $localeID the locale ID (e.g. en_US). If null, the {@link getLanguage application language ID} will be used.
* The current {@link getLocale application locale} will be used. // * @return CLocale the locale instance
*/ // */
public function getNumberFormatter() // public function getLocale($localeID = null)
{ // {
return $this->getLocale()->getNumberFormatter(); // return CLocale::getInstance($localeID === null ? $this->getLanguage() : $localeID);
} // }
//
/** // /**
* Returns the locale-dependent date formatter. // * @return CNumberFormatter the locale-dependent number formatter.
* @return CDateFormatter the locale-dependent date formatter. // * The current {@link getLocale application locale} will be used.
* The current {@link getLocale application locale} will be used. // */
*/ // public function getNumberFormatter()
public function getDateFormatter() // {
{ // return $this->getLocale()->getNumberFormatter();
return $this->getLocale()->getDateFormatter(); // }
} //
// /**
// * 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. * 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. * Returns the cache component.
* @return \yii\caching\Cache the cache application component. Null if the component is not enabled. * @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. * Returns the request component.
* @return Request the request component * @return Request the request component
*/ */
@ -402,9 +363,9 @@ class Application extends Module
*/ */
public function registerDefaultAliases() public function registerDefaultAliases()
{ {
\Yii::$aliases['@application'] = $this->getBasePath(); Yii::$aliases['@application'] = $this->getBasePath();
\Yii::$aliases['@entry'] = dirname($_SERVER['SCRIPT_FILENAME']); Yii::$aliases['@entry'] = dirname($_SERVER['SCRIPT_FILENAME']);
\Yii::$aliases['@www'] = ''; Yii::$aliases['@www'] = '';
} }
/** /**
@ -417,15 +378,6 @@ class Application extends Module
'errorHandler' => array( 'errorHandler' => array(
'class' => 'yii\base\ErrorHandler', 'class' => 'yii\base\ErrorHandler',
), ),
'request' => array(
'class' => 'yii\base\Request',
),
'response' => array(
'class' => 'yii\base\Response',
),
'format' => array(
'class' => 'yii\base\Formatter',
),
'coreMessages' => array( 'coreMessages' => array(
'class' => 'yii\i18n\PhpMessageSource', 'class' => 'yii\i18n\PhpMessageSource',
'language' => 'en_us', 'language' => 'en_us',
@ -444,124 +396,88 @@ class Application extends Module
} }
/** /**
* Performs a controller action specified by a route. * Handles PHP execution errors such as warnings, notices.
* 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 * This method is used as a PHP error handler. It will simply raise an `ErrorException`.
* with the given parameters. *
* @param string $route the route that specifies the action. * @param integer $code the level of the error raised
* @param array $params the parameters to be passed to the action * @param string $message the error message
* @param Module $module the module which serves as the context of the route * @param string $file the filename that the error was raised in
* @return integer the action * @param integer $line the line number the error was raised at
* @throws InvalidConfigException if the module's defaultRoute is empty or the controller's defaultAction is empty * @throws \ErrorException the error exception
* @throws InvalidRequestException if the requested route cannot be resolved into an action successfully
*/ */
public function runAction($route, $params = array(), $module = null) public function handleError($code, $message, $file, $line)
{ {
if ($module === null) { if (error_reporting() !== 0) {
$module = $this; throw new \ErrorException($message, 0, $code, $file, $line);
}
$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);
}
} }
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 * This method is implemented as a PHP exception handler. It requires
* create the controller based on the [[controllerMap]] of the module. If not available, * that constant YII_ENABLE_ERROR_HANDLER be defined true.
* it will look for the controller class under the [[controllerPath]] and create an
* instance of it.
* *
* @param string $id the controller ID * @param \Exception $exception exception that is not caught
* @param Module $module the module that owns the controller
* @return Controller the newly created controller instance
*/ */
public function createController($id, $module) public function handleException($exception)
{ {
if (isset($module->controllerMap[$id])) { // disable error capturing to avoid recursive errors while handling exceptions
return \Yii::createObject($module->controllerMap[$id], $id, $module); restore_error_handler();
} elseif (preg_match('/^[a-z0-9\\-_]+$/', $id)) { restore_exception_handler();
$className = StringHelper::id2camel($id) . 'Controller';
$classFile = $module->controllerPath . DIRECTORY_SEPARATOR . $className . '.php'; try {
if (is_file($classFile)) { $this->logException($exception);
$className = $module->controllerNamespace . '\\' . $className;
if (!class_exists($className, false)) { if (($handler = $this->getErrorHandler()) !== null) {
require($classFile); $handler->handle($exception);
} } else {
if (class_exists($className, false) && is_subclass_of($className, '\yii\base\Controller')) { $this->renderException($exception);
return new $className($id, $module);
}
} }
$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. * Renders an exception without using rich format.
* The action is created within the given controller. The method first attempts to * @param \Exception $exception the exception to be rendered.
* 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.
*/ */
public function createAction($id, $controller) public function renderException($exception)
{ {
if ($id === '') { if ($exception instanceof Exception && ($exception->causedByUser || !YII_DEBUG)) {
$id = $controller->defaultAction; $message = $exception->getName() . ': ' . $exception->getMessage();
if ($id == '') { } else {
throw new InvalidConfigException(get_class($controller) . '::defaultAction cannot be empty.'); $message = YII_DEBUG ? (string)$exception : 'Error: ' . $exception->getMessage();
}
} }
if (isset($controller->actionMap[$id])) { if (PHP_SAPI) {
return \Yii::createObject($controller->actionMap[$id], $id, $controller); echo $message . "\n";
} elseif (preg_match('/^[a-z0-9\\-_]+$/', $id)) { } else {
$methodName = 'action' . StringHelper::id2camel($id); echo '<pre>' . htmlspecialchars($message, ENT_QUOTES, $this->charset) . '</pre>';
if (method_exists($controller, $methodName)) { }
$method = new \ReflectionMethod($controller, $methodName); }
if ($method->getName() === $methodName) {
return new InlineAction($id, $controller); // 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);
} }
} }

260
framework/base/Controller.php

@ -9,16 +9,12 @@
namespace yii\base; namespace yii\base;
use Yii;
use yii\util\StringHelper;
/** /**
* Controller is the base class for classes containing controller logic. * 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 $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). * @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 class Controller extends Component
{ {
const EVENT_BEFORE_ACTION = 'beforeAction';
const EVENT_AFTER_ACTION = 'afterAction';
/** /**
* @var string the ID of this controller * @var string the ID of this controller
*/ */
@ -91,200 +90,184 @@ class Controller extends Component
} }
/** /**
* Runs the controller with the specified action and parameters. * Runs an action with the specified action ID and parameters.
* @param Action|string $action the action to be executed. This can be either an action object * If the action ID is empty, the method will use [[defaultAction]].
* or the ID of the action. * @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. * @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 status of the action execution. 0 means normal, other values mean abnormal.
* @return integer the exit status of the action. 0 means normal, other values mean abnormal. * @throws InvalidRouteException if the requested action ID cannot be resolved into an action successfully.
* @see missingAction
* @see createAction * @see createAction
*/ */
public function run($action, $params = null) public function runAction($id, $params = array())
{ {
if (is_string($action)) { $action = $this->createAction($id);
if (($a = $this->createAction($action)) !== null) { if ($action !== null) {
$action = $a; $oldAction = $this->action;
$this->action = $action;
if ($this->beforeAction($action)) {
$status = $action->runWithParams($params);
$this->afterAction($action);
} else { } else {
$this->missingAction($action); $status = 1;
return 1;
} }
}
$priorAction = $this->action; $this->action = $oldAction;
$this->action = $action;
if ($this->authorize($action) && $this->beforeAction($action)) { return $status;
if ($params === null) {
$params = $this->getActionParams();
}
$status = $action->runWithParams($params);
$this->afterAction($action);
} else { } 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. * Runs a request specified in terms of a route.
* The action can be either an inline action or an object. * The route can be either an ID of an action within this controller or a complete route consisting
* The latter is created by looking up the action map specified in [[actions]]. * of module IDs, controller ID and action ID. If the route starts with a slash '/', the parsing of
* @param string $actionID ID of the action. If empty, it will take the value of [[defaultAction]]. * the route will start from the application; otherwise, it will start from the parent module of this controller.
* @return Action the action instance, null if the action does not exist. * @param string $route the route to be handled, e.g., 'view', 'comment/view', '/admin/comment/view'.
* @see actions * @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 === '') { $pos = strpos($route, '/');
$actionID = $this->defaultAction; if ($pos === false) {
} return $this->runAction($route, $params);
if (isset($this->actionMap[$actionID])) { } elseif ($pos > 0) {
return \Yii::createObject($this->actionMap[$actionID], $actionID, $this); return $this->module->runAction($route, $params);
} elseif (method_exists($this, 'action' . $actionID)) {
return new InlineAction($actionID, $this);
} else { } else {
return null; return \Yii::$application->runAction(ltrim($route, '/'), $params);
} }
} }
/** /**
* Returns the request parameters that will be used for action parameter binding. * Forwards the current execution flow to handle a new request specified by a route.
* Default implementation simply returns an empty array. * The only difference between this method and [[run()]] is that after calling this method,
* Child classes may override this method to customize the parameters to be provided * the application will exit.
* for action parameter binding (e.g. `$_GET`). * @param string $route the route to be handled, e.g., 'view', 'comment/view', '/admin/comment/view'.
* @return array the request parameters (name-value pairs) to be used for action parameter binding * @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. * Creates an action based on the given action ID.
* The default implementation will throw an exception. * The method first checks if the action ID has been declared in [[actions()]]. If so,
* @param Action $action the action being executed * it will use the configuration declared there to create the action object.
* @param Exception $exception the exception about the invalid parameters * If not, it will look for a controller method whose name is in the format of `actionXyz`
* @throws Exception whenever this method is invoked * 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;
}
/** $actionMap = $this->actions();
* This method is invoked when extra parameters are provided to an action when it is executed. if (isset($actionMap[$id])) {
* The default implementation does nothing. return Yii::createObject($actionMap[$id], $id, $this);
* @param Action $action the action being executed } elseif (preg_match('/^[a-z0-9\\-_]+$/', $id)) {
* @param array $expected the expected action parameters (name => value) $methodName = 'action' . StringHelper::id2camel($id);
* @param array $actual the actual action parameters (name => value) if (method_exists($this, $methodName)) {
*/ $method = new \ReflectionMethod($this, $methodName);
public function extraActionParams($action, $expected, $actual) if ($method->getName() === $methodName) {
{ return new InlineAction($id, $this, $methodName);
}
}
}
return null;
} }
/** /**
* Handles the request whose action is not recognized. * This method is invoked right before an action is to be executed (after all possible filters.)
* This method is invoked when the controller cannot find the requested action. * You may override this method to do last-minute preparation for the action.
* The default implementation simply throws an exception. * @param Action $action the action to be executed.
* @param string $actionID the missing action name * @return boolean whether the action should continue to be executed.
* @throws InvalidRequestException whenever this method is invoked
*/ */
public function missingAction($actionID) public function beforeAction($action)
{ {
throw new InvalidRequestException(\Yii::t('yii', 'The system is unable to find the requested action "{action}".', $event = new ActionEvent($action);
array('{action}' => $actionID == '' ? $this->defaultAction : $actionID))); $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. * Returns the request parameters that will be used for action parameter binding.
* @return string the route (module ID, controller ID and action ID) of the current request. * 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. * Validates the parameter being bound to actions.
* @param string $route the route of the new controller action. This can be an action ID, or a complete route * This method is invoked when parameters are being bound to the currently requested action.
* with module ID (optional in the current module), controller ID and action ID. If the former, * Child classes may override this method to throw exceptions when there are missing and/or unknown parameters.
* the action is assumed to be located within the current controller. * @param Action $action the currently requested action
* @param array $params the parameters to be passed to the action. * @param array $missingParams the names of the missing parameters
* If null, the result of [[getActionParams()]] will be used as action parameters. * @param array $unknownParams the unknown parameters (name=>value)
* 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.
*/ */
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. * @return string the controller ID that is prefixed with the module ID (if any).
* @param Action $action the action to be executed.
* @return boolean whether the action is allowed to be executed.
*/ */
public function authorize($action) public function getUniqueId()
{ {
$event = new ActionEvent($action); return $this->module instanceof Application ? $this->id : $this->module->getUniqueId() . '/' . $this->id;
$this->trigger(__METHOD__, $event);
return $event->isValid;
} }
/** /**
* This method is invoked right before an action is to be executed (after all possible filters.) * Returns the route of the current request.
* You may override this method to do last-minute preparation for the action. * @return string the route (module ID, controller ID and action ID) of the current request.
* @param Action $action the action to be executed.
* @return boolean whether the action should continue to be executed.
*/ */
public function beforeAction($action) public function getRoute()
{ {
$event = new ActionEvent($action); return $this->action !== null ? $this->getUniqueId() . '/' . $this->action->id : $this->getUniqueId();
$this->trigger(__METHOD__, $event);
return $event->isValid;
} }
/** /**
* This method is invoked right after an action is executed. * Renders a view and applies layout if available.
* You may override this method to do some postprocessing for the action. *
* @param Action $action the action just executed. * @param $view
* @param array $params
* @return string
*/ */
public function afterAction($action)
{
$this->trigger(__METHOD__, new ActionEvent($action));
}
public function render($view, $params = array()) public function render($view, $params = array())
{ {
return $this->createView()->render($view, $params); 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()) public function renderPartial($view, $params = array())
@ -296,4 +279,15 @@ class Controller extends Component
{ {
return new View($this); 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;
}
} }

106
framework/base/ErrorHandler.php

@ -52,90 +52,46 @@ class ErrorHandler extends Component
* @var \Exception the exception that is being handled currently * @var \Exception the exception that is being handled currently
*/ */
public $exception; 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 * @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->exception = $exception;
$this->logException($exception);
if ($this->discardExistingOutput) { if ($this->discardExistingOutput) {
$this->clearOutput(); $this->clearOutput();
} }
try { $this->render($exception);
$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);
}
} }
protected function render($exception) protected function render($exception)
{ {
if ($this->errorAction !== null) { if ($this->errorAction !== null) {
\Yii::$application->runController($this->errorAction); \Yii::$application->runAction($this->errorAction);
} elseif (\Yii::$application instanceof \yii\web\Application) { } elseif (\Yii::$application instanceof \yii\web\Application) {
if (!headers_sent()) { if (!headers_sent()) {
$errorCode = $exception instanceof HttpException ? $exception->statusCode : 500; $errorCode = $exception instanceof HttpException ? $exception->statusCode : 500;
header("HTTP/1.0 $errorCode " . get_class($exception)); header("HTTP/1.0 $errorCode " . get_class($exception));
} }
if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest') { if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest') {
$this->renderAsText($exception); \Yii::$application->renderException($exception);
} else { } 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 { } else {
$this->renderAsText($exception); \Yii::$application->renderException($exception);
} }
} }
@ -286,22 +242,6 @@ class ErrorHandler extends Component
return htmlspecialchars($text, ENT_QUOTES, \Yii::$application->charset); 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() public function clearOutput()
{ {
// the following manual level counting is to deal with zlib.output_compression set to On // 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 * @param \Exception $exception
*/ */
public function renderAsText($exception) public function renderAsHtml($exception)
{ {
if (YII_DEBUG) { $view = new View($this);
echo $exception; if (!YII_DEBUG || $exception instanceof Exception && $exception->causedByUser) {
$viewName = $this->errorView;
} else { } 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; $name = !YII_DEBUG || $exception instanceof HttpException ? $this->errorView : $this->exceptionView;
echo $view->render($name, array( echo $view->render($name, array(
'exception' => $exception, 'exception' => $exception,

12
framework/base/Exception.php

@ -17,5 +17,17 @@ namespace yii\base;
*/ */
class Exception extends \Exception 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');
}
} }

4
framework/base/HttpException.php

@ -25,6 +25,10 @@ class HttpException extends Exception
* @var integer HTTP status code, such as 403, 404, 500, etc. * @var integer HTTP status code, such as 403, 404, 500, etc.
*/ */
public $statusCode; public $statusCode;
/**
* @var boolean whether this exception is caused by end user's mistake (e.g. wrong URL)
*/
public $causedByUser = true;
/** /**
* Constructor. * Constructor.

37
framework/base/InlineAction.php

@ -9,13 +9,11 @@
namespace yii\base; namespace yii\base;
use yii\util\ReflectionHelper;
/** /**
* InlineAction represents an action that is defined as a controller method. * InlineAction represents an action that is defined as a controller method.
* *
* The name of the controller method should be in the format of `actionXyz` * The name of the controller method is available via [[actionMethod]] which
* where `Xyz` stands for the action ID (e.g. `actionIndex`). * is set by the [[controller]] who creates this action.
* *
* @author Qiang Xue <qiang.xue@gmail.com> * @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0 * @since 2.0
@ -23,6 +21,23 @@ use yii\util\ReflectionHelper;
class InlineAction extends Action 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. * Runs this action with the specified parameters.
* This method is mainly invoked by the controller. * This method is mainly invoked by the controller.
* @param array $params action parameters * @param array $params action parameters
@ -30,16 +45,8 @@ class InlineAction extends Action
*/ */
public function runWithParams($params) public function runWithParams($params)
{ {
try { $method = new \ReflectionMethod($this->controller, $this->actionMethod);
$method = 'action' . $this->id; $args = $this->bindActionParams($method, $params);
$ps = ReflectionHelper::extractMethodParams($this->controller, $method, $params); return (int)$method->invokeArgs($this->controller, $args);
} 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);
} }
} }

7
framework/base/InvalidCallException.php

@ -17,5 +17,12 @@ namespace yii\base;
*/ */
class InvalidCallException extends \Exception class InvalidCallException extends \Exception
{ {
/**
* @return string the user-friendly name of this exception
*/
public function getName()
{
return \Yii::t('yii', 'Invalid Call');
}
} }

7
framework/base/InvalidConfigException.php

@ -17,5 +17,12 @@ namespace yii\base;
*/ */
class InvalidConfigException extends \Exception class InvalidConfigException extends \Exception
{ {
/**
* @return string the user-friendly name of this exception
*/
public function getName()
{
return \Yii::t('yii', 'Invalid Configuration');
}
} }

12
framework/base/InvalidRequestException.php

@ -17,5 +17,17 @@ namespace yii\base;
*/ */
class InvalidRequestException extends \Exception 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');
}
} }

33
framework/base/InvalidRouteException.php

@ -0,0 +1,33 @@
<?php
/**
* InvalidRouteException class file.
*
* @link http://www.yiiframework.com/
* @copyright Copyright &copy; 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\base;
/**
* InvalidRouteException represents an exception caused by an invalid route.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @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');
}
}

180
framework/base/Module.php

@ -9,18 +9,29 @@
namespace yii\base; namespace yii\base;
use Yii;
use yii\util\StringHelper;
use yii\util\FileHelper; use yii\util\FileHelper;
/** /**
* Module is the base class for module and application classes. * 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 * @property string $uniqueId An ID that uniquely identifies this module among all modules within
* the current application. * the current application.
* @property string $basePath The root directory of the module. Defaults to the directory containing the module class. * @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 $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 $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. * @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(); 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(); public $preload = array();
/** /**
@ -86,27 +97,27 @@ abstract class Module extends Component
/** /**
* @var string the root directory of the module. * @var string the root directory of the module.
*/ */
protected $_basePath; private $_basePath;
/** /**
* @var string the root directory that contains view files for this module * @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. * @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. * @var string the directory containing controller classes in the module.
*/ */
protected $_controllerPath; private $_controllerPath;
/** /**
* @var array child modules of this module * @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. * Constructor.
@ -123,9 +134,9 @@ abstract class Module extends Component
/** /**
* Getter magic method. * Getter magic method.
* This method is overridden to support accessing application components * This method is overridden to support accessing components
* like reading module properties. * 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 * @return mixed the named property value
*/ */
public function __get($name) public function __get($name)
@ -140,7 +151,7 @@ abstract class Module extends Component
/** /**
* Checks if a property value is null. * Checks if a property value is null.
* This method overrides the parent implementation by checking * 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 * @param string $name the property name or the event name
* @return boolean whether the property value is null * @return boolean whether the property value is null
*/ */
@ -161,18 +172,21 @@ abstract class Module extends Component
*/ */
public function init() public function init()
{ {
\Yii::setAlias('@' . $this->id, $this->getBasePath()); Yii::setAlias('@' . $this->id, $this->getBasePath());
$this->preloadComponents(); $this->preloadComponents();
} }
/** /**
* Returns an ID that uniquely identifies this module among all modules within the current application. * 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. * @return string the unique ID of the module.
*/ */
public function getUniqueId() public function getUniqueId()
{ {
if ($this->module && !$this->module instanceof Application) { if ($this instanceof Application) {
return $this->module->getUniqueId() . "/{$this->id}"; return '';
} elseif ($this->module) {
return $this->module->getUniqueId() . '/' . $this->id;
} else { } else {
return $this->id; 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 * Returns the directory that contains the view files for this module.
* moduleDir is the directory containing the module class. * @return string the root directory of view files. Defaults to "[[basePath]]/view".
*/ */
public function getViewPath() 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 * Returns the directory that contains layout view files for this module.
* moduleDir is the directory containing the module class. * @return string the root directory of layout files. Defaults to "[[viewPath]]/layouts".
*/ */
public function getLayoutPath() public function getLayoutPath()
{ {
@ -277,19 +291,19 @@ abstract class Module extends Component
/** /**
* Imports the specified path aliases. * Imports the specified path aliases.
* This method is provided so that you can import a set of path aliases when configuring a module. * 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 * @param array $aliases list of path aliases to be imported
*/ */
public function setImport($aliases) public function setImport($aliases)
{ {
foreach ($aliases as $alias) { foreach ($aliases as $alias) {
\Yii::import($alias); Yii::import($alias);
} }
} }
/** /**
* Defines path aliases. * 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. * 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 * @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. * (must start with '@') and the array values are the corresponding paths or aliases.
@ -297,7 +311,7 @@ abstract class Module extends Component
* *
* ~~~ * ~~~
* array( * array(
* '@models' => '@app/models', // an existing alias * '@models' => '@application/models', // an existing alias
* '@backend' => __DIR__ . '/../backend', // a directory * '@backend' => __DIR__ . '/../backend', // a directory
* ) * )
* ~~~ * ~~~
@ -305,7 +319,7 @@ abstract class Module extends Component
public function setAliases($aliases) public function setAliases($aliases)
{ {
foreach ($aliases as $name => $alias) { 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) { if ($this->_modules[$id] instanceof Module) {
return $this->_modules[$id]; return $this->_modules[$id];
} elseif ($load) { } elseif ($load) {
\Yii::trace("Loading \"$id\" module", __CLASS__); Yii::trace("Loading module: $id", __CLASS__);
return $this->_modules[$id] = \Yii::createObject($this->_modules[$id], $id, $this); return $this->_modules[$id] = Yii::createObject($this->_modules[$id], $id, $this);
} }
} }
return null; return null;
@ -388,7 +402,7 @@ abstract class Module extends Component
* *
* Each sub-module should be specified as a name-value pair, where * 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 * 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. * 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. * 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. * Checks whether the named component exists.
* @param string $id application component ID * @param string $id component ID
* @return boolean whether the named application component exists. Both loaded and unloaded components * @return boolean whether the named component exists. Both loaded and unloaded components
* are considered. * are considered.
*/ */
public function hasComponent($id) public function hasComponent($id)
@ -428,11 +442,10 @@ abstract class Module extends Component
} }
/** /**
* Retrieves the named application component. * Retrieves the named component.
* @param string $id application component ID (case-sensitive) * @param string $id component ID (case-sensitive)
* @param boolean $load whether to load the component if it is not yet loaded. * @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 * @return Component|null the component instance, null if the component does not exist.
* does not exist.
* @see hasComponent() * @see hasComponent()
*/ */
public function getComponent($id, $load = true) public function getComponent($id, $load = true)
@ -441,22 +454,22 @@ abstract class Module extends Component
if ($this->_components[$id] instanceof Component) { if ($this->_components[$id] instanceof Component) {
return $this->_components[$id]; return $this->_components[$id];
} elseif ($load) { } elseif ($load) {
\Yii::trace("Loading \"$id\" application component", __CLASS__); Yii::trace("Loading component: $id", __CLASS__);
return $this->_components[$id] = \Yii::createObject($this->_components[$id]); return $this->_components[$id] = Yii::createObject($this->_components[$id]);
} }
} }
return null; return null;
} }
/** /**
* Registers an application component in this module. * Registers a component with this module.
* @param string $id component ID * @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: * be one of the followings:
* *
* - a [[Component]] object * - a [[Component]] object
* - a configuration array: when [[getComponent()]] is called initially for this component, the array * - 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 * - null: the named component will be removed from the module
*/ */
public function setComponent($id, $component) 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, * @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. * 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. * 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) 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 * 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. * 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. * 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) 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() public function preloadComponents()
{ {
@ -533,4 +546,79 @@ abstract class Module extends Component
$this->getComponent($id); $this->getComponent($id);
} }
} }
/**
* Runs a controller action specified by a route.
* This method parses the specified route and creates the corresponding child module(s), controller and action
* instances. It then calls [[Controller::runAction()]] to run the action with the given parameters.
* If the route is empty, the method will use [[defaultRoute]].
* @param string $route the route that specifies the action.
* @param array $params the parameters to be passed to the action
* @return integer the status code returned by the action execution. 0 means normal, and other values mean abnormal.
* @throws InvalidRouteException if the requested route cannot be resolved into an action successfully
*/
public function runAction($route, $params = array())
{
$result = $this->createController($route);
if (is_array($result)) {
/** @var $controller Controller */
list($controller, $actionID) = $result;
$oldController = Yii::$application->controller;
Yii::$application->controller = $controller;
$status = $controller->runAction($actionID, $params);
Yii::$application->controller = $oldController;
return $status;
} else {
throw new InvalidRouteException('Unable to resolve the request: ' . trim($this->getUniqueId() . '/' . $route, '/'));
}
}
/**
* Creates a controller instance based on the controller ID.
*
* The controller is created within this module. The method first attempts to
* create the controller based on the [[controllerMap]] of the module. If not available,
* it will look for the controller class under the [[controllerPath]] and create an
* instance of it.
*
* @param string $route the route consisting of module, controller and action IDs.
* @return array|boolean if the controller is created successfully, it will be returned together
* with the remainder of the route which represents the action ID. Otherwise false will be returned.
*/
public function createController($route)
{
if ($route === '') {
$route = $this->defaultRoute;
}
if (($pos = strpos($route, '/')) !== false) {
$id = substr($route, 0, $pos);
$route = substr($route, $pos + 1);
} else {
$id = $route;
$route = '';
}
$module = $this->getModule($id);
if ($module !== null) {
return $module->createController($route);
}
if (isset($this->controllerMap[$id])) {
$controller = Yii::createObject($this->controllerMap[$id], $id, $this);
} elseif (preg_match('/^[a-z0-9\\-_]+$/', $id)) {
$className = StringHelper::id2camel($id) . 'Controller';
$classFile = $this->controllerPath . DIRECTORY_SEPARATOR . $className . '.php';
if (is_file($classFile)) {
$className = $this->controllerNamespace . '\\' . $className;
if (!class_exists($className, false)) {
require($classFile);
}
if (class_exists($className, false) && is_subclass_of($className, '\yii\base\Controller')) {
$controller = new $className($id, $this);
}
}
}
return isset($controller) ? array($controller, $route) : false;
}
} }

7
framework/base/NotSupportedException.php

@ -17,5 +17,12 @@ namespace yii\base;
*/ */
class NotSupportedException extends \Exception class NotSupportedException extends \Exception
{ {
/**
* @return string the user-friendly name of this exception
*/
public function getName()
{
return \Yii::t('yii', 'Not Supported');
}
} }

105
framework/base/Theme.php

@ -9,55 +9,114 @@
namespace yii\base; namespace yii\base;
use Yii;
use yii\base\InvalidConfigException; use yii\base\InvalidConfigException;
use yii\util\FileHelper;
/** /**
* Theme represents an application theme. * 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 <qiang.xue@gmail.com> * @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0 * @since 2.0
*/ */
class Theme extends Component class Theme extends Component
{ {
/**
* @var string the root path of this theme.
* @see pathMap
*/
public $basePath; 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() public function init()
{ {
if ($this->basePath !== null) { parent::init();
$this->basePath = \Yii::getAlias($this->basePath, true); if (empty($this->pathMap)) {
} else { if ($this->basePath !== null) {
throw new InvalidConfigException("Theme.basePath must be set."); $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) { $paths = array();
$this->baseUrl = \Yii::getAlias($this->baseUrl, true); foreach ($this->pathMap as $from => $to) {
} else { $paths[FileHelper::normalizePath($from) . DIRECTORY_SEPARATOR] = FileHelper::normalizePath($to) . DIRECTORY_SEPARATOR;
throw new InvalidConfigException("Theme.baseUrl must be set.");
} }
$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 * Converts a file to a themed file if possible.
* @return string * 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'; $path = FileHelper::normalizePath($path);
if ($context === null || $context instanceof Application) { foreach ($this->pathMap as $from => $to) {
return $viewPath; if (strpos($path, $from) === 0) {
} elseif ($context instanceof Controller || $context instanceof Module) { $n = strlen($from);
return $viewPath . DIRECTORY_SEPARATOR . $context->getUniqueId(); $file = $to . substr($path, $n);
} else { if (is_file($file)) {
return $viewPath . DIRECTORY_SEPARATOR . str_replace('\\', '_', get_class($context)); return $file;
}
}
} }
return $path;
} }
/** /**
* @param Module $module * Converts a relative URL into an absolute URL using [[basePath]].
* @return string * @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, '/');
} }
} }

7
framework/base/UnknownMethodException.php

@ -17,5 +17,12 @@ namespace yii\base;
*/ */
class UnknownMethodException extends \Exception class UnknownMethodException extends \Exception
{ {
/**
* @return string the user-friendly name of this exception
*/
public function getName()
{
return \Yii::t('yii', 'Unknown Method');
}
} }

7
framework/base/UnknownPropertyException.php

@ -17,5 +17,12 @@ namespace yii\base;
*/ */
class UnknownPropertyException extends \Exception class UnknownPropertyException extends \Exception
{ {
/**
* @return string the user-friendly name of this exception
*/
public function getName()
{
return \Yii::t('yii', 'Unknown Property');
}
} }

558
framework/base/View.php

@ -9,27 +9,29 @@
namespace yii\base; namespace yii\base;
use Yii;
use yii\util\FileHelper; use yii\util\FileHelper;
use yii\base\Application; 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 <qiang.xue@gmail.com> * @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0 * @since 2.0
*/ */
class View extends Component 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. * @var string the layout to be applied when [[render()]] or [[renderContent()]] is called.
* This can be either a string representing a single directory, or an array representing multiple directories. * If not set, it will use the [[Module::layout]] of the currently active module.
* 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
*/ */
public $basePath; public $layout;
/** /**
* @var string the language that the view should be rendered in. If not set, it will use * @var string the language that the view should be rendered in. If not set, it will use
* the value of [[Application::language]]. * 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. * Note that when this is true, if a localized view cannot be found, the original view will be rendered.
* No error will be reported. * No error will be reported.
*/ */
public $localizeView = true; public $enableI18N = true;
/** /**
* @var boolean whether to theme the view when possible. Defaults to 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 * @var mixed custom parameters that are available in the view template
*/ */
public $params; public $params;
/** /**
* @var Widget[] the widgets that are currently not ended * @var Widget[] the widgets that are currently not ended
*/ */
protected $widgetStack = array(); private $_widgetStack = array();
/** /**
* Constructor. * 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 * @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); 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()) public function render($view, $params = array())
{ {
$content = $this->renderPartial($view, $params); $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(); $layoutFile = $this->findLayoutFile();
if ($layoutFile !== false) { if ($layoutFile !== false) {
return $this->renderFile($layoutFile, array('content' => $text)); return $this->renderFile($layoutFile, array('content' => $content));
} else { } 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 * 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. * 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 * @param string $view the view to be rendered. Please refer to [[findViewFile()]] on possible formats of the view name.
* will search in the directories as specified by [[basePath]].
*
* View name can be a path alias representing an absolute file path (e.g. `@app/views/layout/index`),
* or a path relative to [[basePath]]. The file suffix is optional and defaults to `.php` if not given
* in the view name.
*
* @param string $view the view to be rendered. This can be either a path alias or a path relative to [[basePath]].
* @param array $params the parameters that should be made available in the view. The PHP function `extract()` * @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. * will be called on this variable to extract the variables from this parameter.
* @return string the rendering result * @return string the rendering result
* @throws InvalidCallException if the view file cannot be found * @throws InvalidCallException if the view file cannot be found
* @see findViewFile()
*/ */
public function renderPartial($view, $params = array()) public function renderPartial($view, $params = array())
{ {
@ -119,21 +137,44 @@ class View extends Component
/** /**
* Renders a view file. * Renders a view file.
* @param string $file the view file path * This method will extract the given parameters and include the view file.
* @param array $params the parameters to be extracted and made available in 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 * @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()) public function createWidget($class, $properties = array())
{ {
$properties['class'] = $class; $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) public function widget($class, $properties = array(), $captureOutput = false)
{ {
if ($captureOutput) { if ($captureOutput) {
@ -151,14 +192,16 @@ class View extends Component
/** /**
* Begins a widget. * Begins a widget.
* @param string $class the widget class * This method is similar to [[createWidget()]] except that it will expect a matching
* @param array $properties the initial property values of the widget * [[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 * @return Widget the widget instance
*/ */
public function beginWidget($class, $properties = array()) public function beginWidget($class, $properties = array())
{ {
$widget = $this->createWidget($class, $properties); $widget = $this->createWidget($class, $properties);
$this->widgetStack[] = $widget; $this->_widgetStack[] = $widget;
return $widget; return $widget;
} }
@ -172,293 +215,256 @@ class View extends Component
*/ */
public function endWidget() public function endWidget()
{ {
/** @var $widget Widget */ $widget = array_pop($this->_widgetStack);
if (($widget = array_pop($this->widgetStack)) !== null) { if ($widget instanceof Widget) {
$widget->run(); $widget->run();
return $widget; return $widget;
} else { } else {
throw new Exception("Unmatched beginWidget() and endWidget() calls."); 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. * Finds the view file based on the given view name.
* 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,
* *
* ~~~ * A view name can be specified in one of the following formats:
* if($this->beginCache($id)) { *
* // ...generate content here * - path alias (e.g. "@application/views/site/index");
* $this->endCache(); * - 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 * @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. * 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) public function findViewFile($view)
{ {
if (($extension = FileHelper::getExtension($view)) === '') { if (FileHelper::getExtension($view) === '') {
$view .= '.php'; $view .= '.php';
} }
if (strncmp($view, '@', 1) === 0) { 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) { } elseif (strncmp($view, '/', 1) !== 0) {
$file = $this->findRelativeViewFile($view); // e.g. "index"
} else { if ($this->owner instanceof Controller || $this->owner instanceof Widget) {
$file = $this->findAbsoluteViewFile($view); $file = $this->owner->getViewPath() . DIRECTORY_SEPARATOR . $view;
} } elseif ($this->owner !== null) {
$class = new \ReflectionClass($this->owner);
if ($file === false || !is_file($file)) { $file = dirname($class->getFileName()) . DIRECTORY_SEPARATOR . 'views' . DIRECTORY_SEPARATOR . $view;
return false; } else {
} elseif ($this->localizeView) { $file = Yii::$application->getViewPath() . DIRECTORY_SEPARATOR . $view;
return FileHelper::localize($file, $this->language, $this->sourceLanguage); }
} elseif (strncmp($view, '//', 2) !== 0 && Yii::$application->controller !== null) {
// e.g. "/site/index"
$file = Yii::$application->controller->module->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/');
} else { } else {
return $file; // e.g. "//layouts/main"
$file = Yii::$application->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/');
} }
}
/** if (is_file($file)) {
* Finds the view file corresponding to the given relative view name. if ($this->enableTheme && ($theme = Yii::$application->getTheme()) !== null) {
* The method will look for the view file under a set of directories returned by [[resolveBasePath()]]. $file = $theme->apply($file);
* 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;
} }
} return $this->enableI18N ? FileHelper::localize($file, $this->language, $this->sourceLanguage) : $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;
} else { } 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. * Finds the layout file that can be applied to the view.
* The method resolves the base path using the following algorithm:
* *
* - If [[basePath]] is not empty, it is returned; * The applicable layout is resolved according to the following rules:
* - If [[context]] is a controller, it will return the subdirectory named as *
* [[Controller::uniqueId]] under the controller's module view path; * - If [[layout]] is specified as a string, use it as the layout name and search for the layout file
* - If [[context]] is an object, it will return the `views` subdirectory under * under the layout path of the currently active module;
* the directory containing the object class file. * - If [[layout]] is null and [[owner]] is a controller:
* - Otherwise, it will return false. * * If the controller's [[Controller::layout|layout]] is a string, use it as the layout name
* @return array the base paths * 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
protected function resolveBasePath() * and find the first one whose [[Module::layout|layout]] is not null. Use the layout specified
{ * by that module;
if (!empty($this->basePath)) { * - Returns false for all other cases.
return (array)$this->basePath; *
} elseif ($this->context instanceof Controller) { * Like view names, a layout name can take several formats:
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:
* *
* - If `context.layout` is false, it will return false; * - path alias (e.g. "@application/views/layouts/main");
* - If `context.layout` is a string, it will look for the layout file under the [[Module::layoutPath|layout path]] * - absolute path (e.g. "/main"): the layout name starts with a slash. The actual layout file will be
* of the controller's parent module; * looked for under the [[Application::layoutPath|layout path]] of the application;
* - If `context.layout` is null, the following steps are taken to resolve the actual layout to be returned: * - relative path (e.g. "main"): the actual layout layout file will be looked for under the
* * Check the `layout` property of the parent module. If it is null, check the grand parent module and so on * [[Module::viewPath|view path]] of the context module determined by the above layout resolution process.
* 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.
* *
* 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. * If [[enableTheme]] is true and there is an active application them, the method will also
* @throws InvalidCallException if the layout file cannot be found * 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() public function findLayoutFile()
{ {
if (!$this->context instanceof Controller || $this->context->layout === false) { /** @var $module Module */
return false; if (is_string($this->layout)) {
} if (Yii::$application->controller) {
$module = $this->context->module; $module = Yii::$application->controller->module;
while ($module !== null && $module->layout === null) { } else {
$module = $module->module; $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; return false;
} }
$view = $module->layout; if (FileHelper::getExtension($view) === '') {
if (($extension = FileHelper::getExtension($view)) === '') {
$view .= '.php'; $view .= '.php';
} }
if (strncmp($view, '@', 1) === 0) { 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) { } elseif (strncmp($view, '/', 1) === 0) {
$file = $this->findAbsoluteViewFile($view); $file = Yii::$application->getLayoutPath() . DIRECTORY_SEPARATOR . $view;
} else { } else {
if ($this->themeView && ($theme = \Yii::$application->getTheme()) !== null) { $file = $module->getLayoutPath() . DIRECTORY_SEPARATOR . $view;
$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;
}
}
} }
if ($file === false || !is_file($file)) {
throw new InvalidCallException("Unable to find the layout file for layout '{$module->layout}' (specified by " . get_class($module) . ")"); if (is_file($file)) {
} elseif ($this->localizeView) { if ($this->enableTheme && ($theme = Yii::$application->getTheme()) !== null) {
return FileHelper::localize($file, $this->language, $this->sourceLanguage); $file = $theme->apply($file);
}
return $this->enableI18N ? FileHelper::localize($file, $this->language, $this->sourceLanguage) : $file;
} else { } else {
return $file; throw new InvalidConfigException("Layout file for layout '$view' does not exist: $file");
} }
} }
} }

16
framework/base/Widget.php

@ -49,7 +49,7 @@ class Widget extends Component
public function getId($autoGenerate = true) public function getId($autoGenerate = true)
{ {
if ($autoGenerate && $this->_id === null) { if ($autoGenerate && $this->_id === null) {
$this->_id = 'yw' . self::$_counter++; $this->_id = 'w' . self::$_counter++;
} }
return $this->_id; return $this->_id;
} }
@ -80,7 +80,7 @@ class Widget extends Component
* To determine which view file should be rendered, the method calls [[findViewFile()]] which * To determine which view file should be rendered, the method calls [[findViewFile()]] which
* will search in the directories as specified by [[basePath]]. * 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 * or a path relative to [[basePath]]. The file suffix is optional and defaults to `.php` if not given
* in the view name. * in the view name.
* *
@ -102,4 +102,16 @@ class Widget extends Component
{ {
return new View($this); 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';
}
} }

43
framework/console/Application.php

@ -10,7 +10,7 @@
namespace yii\console; namespace yii\console;
use yii\base\Exception; use yii\base\Exception;
use yii\util\ReflectionHelper; use yii\base\InvalidRouteException;
/** /**
* Application represents a console application. * Application represents a console application.
@ -85,40 +85,36 @@ class Application extends \yii\base\Application
* Processes the request. * Processes the request.
* The request is represented in terms of a controller route and action parameters. * 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) * @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() public function processRequest()
{ {
/** @var $request Request */ /** @var $request Request */
$request = $this->getRequest(); $request = $this->getRequest();
if ($request->getIsConsoleRequest()) { if ($request->getIsConsoleRequest()) {
return $this->runController($request->route, $request->params); return $this->runAction($request->route, $request->params);
} else { } 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. * Runs a controller action specified by a route.
* @param string $route the route (e.g. `post/create`) * This method parses the specified route and creates the corresponding child module(s), controller and action
* @param array $params the parameters to be passed to the controller action * instances. It then calls [[Controller::runAction()]] to run the action with the given parameters.
* @return integer the exit status (0 means normal, non-zero values mean abnormal) * If the route is empty, the method will use [[defaultRoute]].
* @throws Exception if the route cannot be resolved into a controller * @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); try {
if ($result === false) { return parent::runAction($route, $params);
throw new Exception(\Yii::t('yii', 'Unable to resolve the request.')); } 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)
{
}
} }

118
framework/console/Controller.php

@ -9,8 +9,10 @@
namespace yii\console; namespace yii\console;
use Yii;
use yii\base\Action; 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. * Controller is the base class of console command classes.
@ -30,72 +32,56 @@ use yii\base\Exception;
class Controller extends \yii\base\Controller class Controller extends \yii\base\Controller
{ {
/** /**
* This method is invoked when the request parameters do not satisfy the requirement of the specified action. * @var boolean whether the call of [[confirm()]] requires a user input.
* The default implementation will throw an exception. * If false, [[confirm()]] will always return true no matter what user enters or not.
* @param Action $action the action being executed
* @param Exception $exception the exception about the invalid parameters
*/ */
public function invalidActionParams($action, $exception) public $interactive = true;
{
echo \Yii::t('yii', 'Error: {message}', array(
'{message}' => $exception->getMessage(),
));
\Yii::$application->end(1);
}
/** /**
* This method is invoked when extra parameters are provided to an action while it is executed. * Runs an action with the specified action ID and parameters.
* @param Action $action the action being executed * If the action ID is empty, the method will use [[defaultAction]].
* @param array $expected the expected action parameters (name => value) * @param string $id the ID of the action to be executed.
* @param array $actual the actual action parameters (name => value) * @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']); if ($params !== array()) {
$class = new \ReflectionClass($this);
$keys = array_diff(array_keys($actual), array_keys($expected)); foreach ($params as $name => $value) {
if (!empty($keys)) { if ($class->hasProperty($name)) {
echo \Yii::t('yii', 'Error: Unknown parameter(s): {params}', array( $property = $class->getProperty($name);
'{params}' => implode(', ', $keys), if ($property->isPublic() && !$property->isStatic() && $property->getDeclaringClass()->getName() === get_class($this)) {
)) . "\n"; $this->$name = $value;
\Yii::$application->end(1); 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. * Validates the parameter being bound to actions.
* * This method is invoked when parameters are being bound to the currently requested action.
* @param string $message to echo out before waiting for user input * Child classes may override this method to throw exceptions when there are missing and/or unknown parameters.
* @param string $default the default string to be returned when user does not write anything. * @param Action $action the currently requested action
* Defaults to null, means that default string is disabled. * @param array $missingParams the names of the missing parameters
* @return mixed line read as a string, or false if input has been closed * @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) { if (!empty($missingParams)) {
$message .= " [$default] "; throw new InvalidRequestException(Yii::t('yii', 'Missing required options: {params}', array(
} '{params}' => implode(', ', $missingParams),
else { )));
$message .= ' '; } elseif (!empty($unknownParams)) {
} throw new InvalidRequestException(Yii::t('yii', 'Unknown options: {params}', array(
'{params}' => implode(', ', $unknownParams),
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;
} }
} }
@ -108,9 +94,23 @@ class Controller extends \yii\base\Controller
*/ */
public function confirm($message, $default = false) 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)); public function globalOptions()
return empty($input) ? $default : !strncasecmp($input, 'y', 1); {
return array();
} }
} }

4
framework/console/controllers/CreateController.php

@ -165,8 +165,8 @@ class CreateController extends Controller
} }
/** /**
* @param string $path1 abosolute path * @param string $path1 absolute path
* @param string $path2 abosolute path * @param string $path2 absolute path
* *
* @return string relative path * @return string relative path
*/ */

35
framework/console/controllers/HelpController.php

@ -12,6 +12,7 @@ namespace yii\console\controllers;
use yii\base\Application; use yii\base\Application;
use yii\base\InlineAction; use yii\base\InlineAction;
use yii\console\Controller; use yii\console\Controller;
use yii\util\StringHelper;
/** /**
* This command provides help information about console commands. * This command provides help information about console commands.
@ -54,16 +55,16 @@ class HelpController extends Controller
} else { } else {
$result = \Yii::$application->createController($args[0]); $result = \Yii::$application->createController($args[0]);
if ($result === false) { if ($result === false) {
echo "Unknown command: " . $args[0] . "\n"; echo "Error: no help for unknown command \"{$args[0]}\".\n";
return 1; return 1;
} }
list($controller, $action) = $result; list($controller, $actionID) = $result;
if ($action === '') { if ($actionID === '') {
$status = $this->getControllerHelp($controller); $status = $this->getControllerHelp($controller);
} else { } else {
$status = $this->getActionHelp($controller, $action); $status = $this->getActionHelp($controller, $actionID);
} }
} }
return $status; return $status;
@ -87,13 +88,13 @@ class HelpController extends Controller
*/ */
public function getActions($controller) public function getActions($controller)
{ {
$actions = array_keys($controller->actionMap); $actions = array_keys($controller->actions());
$class = new \ReflectionClass($controller); $class = new \ReflectionClass($controller);
foreach ($class->getMethods() as $method) { foreach ($class->getMethods() as $method) {
/** @var $method \ReflectionMethod */ /** @var $method \ReflectionMethod */
$name = $method->getName(); $name = $method->getName();
if ($method->isPublic() && !$method->isStatic() && strpos($name, 'action') === 0) { if ($method->isPublic() && !$method->isStatic() && strpos($name, 'action') === 0 && $name !== 'actions') {
$actions[] = lcfirst(substr($name, 6)); $actions[] = StringHelper::camel2id(substr($name, 6));
} }
} }
sort($actions); sort($actions);
@ -107,11 +108,7 @@ class HelpController extends Controller
*/ */
protected function getModuleCommands($module) protected function getModuleCommands($module)
{ {
if ($module instanceof Application) { $prefix = $module instanceof Application ? '' : $module->getUniqueID() . '/';
$prefix = '';
} else {
$prefix = $module->getUniqueId() . '/';
}
$commands = array(); $commands = array();
foreach (array_keys($module->controllerMap) as $id) { foreach (array_keys($module->controllerMap) as $id) {
@ -145,12 +142,12 @@ class HelpController extends Controller
{ {
$commands = $this->getCommands(); $commands = $this->getCommands();
if ($commands !== array()) { if ($commands !== array()) {
echo "\n Usage: yiic <command-name> [...options...]\n\n"; echo "\nUsage: yiic <command-name> [...options...]\n\n";
echo "The following commands are available:\n"; echo "The following commands are available:\n\n";
foreach ($commands as $command) { 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 <command-name>\n"; echo "\n yiic help <command-name>\n";
} else { } else {
echo "\nNo commands are found.\n"; echo "\nNo commands are found.\n";
@ -195,7 +192,7 @@ class HelpController extends Controller
$prefix = $controller->getUniqueId(); $prefix = $controller->getUniqueId();
foreach ($actions as $action) { foreach ($actions as $action) {
if ($controller->defaultAction === $action) { if ($controller->defaultAction === $action) {
echo " * $prefix/$action (default)\n"; echo " * $prefix (default)\n";
} else { } else {
echo " * $prefix/$action\n"; echo " * $prefix/$action\n";
} }
@ -216,7 +213,7 @@ class HelpController extends Controller
{ {
$action = $controller->createAction($actionID); $action = $controller->createAction($actionID);
if ($action === null) { 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; return 1;
} }
if ($action instanceof InlineAction) { if ($action instanceof InlineAction) {
@ -312,7 +309,7 @@ class HelpController extends Controller
{ {
$options = array(); $options = array();
foreach ($class->getProperties() as $property) { 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; continue;
} }
$name = $property->getName(); $name = $property->getName();

432
framework/console/controllers/MigrateController.php

@ -10,6 +10,7 @@
namespace yii\console\controllers; namespace yii\console\controllers;
use Yii;
use yii\console\Controller; use yii\console\Controller;
/** /**
@ -60,25 +61,25 @@ use yii\console\Controller;
*/ */
class MigrateController extends 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 * @var string the directory that stores the migrations. This must be specified
* in terms of a path alias, and the corresponding directory must exist. * in terms of a path alias, and the corresponding directory must exist.
* Defaults to 'application.migrations' (meaning 'protected/migrations'). * 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. * @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'. * 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) * 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 * @var string the application component ID that specifies the database connection for
* storing migration information. Defaults to 'db'. * 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 * @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). * 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'. * @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. * @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. * Set this to false when performing migration in a cron job or background process.
*/ */
public $interactive=true; public $interactive = true;
public function beforeAction($action) public function beforeAction($action)
{ {
$path = \Yii::getAlias($this->migrationPath); if (parent::beforeAction($action)) {
if($path===false || !is_dir($path)) { $path = Yii::getAlias($this->migrationPath);
echo 'Error: The migration directory does not exist: ' . $this->migrationPath . "\n"; if ($path === false || !is_dir($path)) {
\Yii::$application->end(1); 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) 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"; echo "No new migration found. Your system is up-to-date.\n";
\Yii::$application->end(); Yii::$application->end();
} }
$total=count($migrations); $total = count($migrations);
$step=isset($args[0]) ? (int)$args[0] : 0; $step = isset($args[0]) ? (int)$args[0] : 0;
if($step>0) { if ($step > 0) {
$migrations=array_slice($migrations,0,$step); $migrations = array_slice($migrations, 0, $step);
} }
$n=count($migrations); $n = count($migrations);
if($n===$total) if ($n === $total) {
echo "Total $n new ".($n===1 ? 'migration':'migrations')." to be applied:\n"; echo "Total $n new " . ($n === 1 ? 'migration' : 'migrations') . " to be applied:\n";
else } else {
echo "Total $n out of $total new ".($total===1 ? 'migration':'migrations')." to be applied:\n"; 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 " $migration\n";
}
echo "\n"; echo "\n";
if($this->confirm('Apply the above '.($n===1 ? 'migration':'migrations')."?")) if ($this->confirm('Apply the above ' . ($n === 1 ? 'migration' : 'migrations') . "?")) {
{ foreach ($migrations as $migration) {
foreach($migrations as $migration) if ($this->migrateUp($migration) === false) {
{
if($this->migrateUp($migration)===false)
{
echo "\nMigration failed. All later migrations are canceled.\n"; echo "\nMigration failed. All later migrations are canceled.\n";
return; return;
} }
@ -153,29 +155,27 @@ class MigrateController extends Controller
public function actionDown($args) public function actionDown($args)
{ {
$step=isset($args[0]) ? (int)$args[0] : 1; $step = isset($args[0]) ? (int)$args[0] : 1;
if($step<1) if ($step < 1) {
die("Error: The step parameter must be greater than 0.\n"); 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"; echo "No migration has been done before.\n";
return; return;
} }
$migrations=array_keys($migrations); $migrations = array_keys($migrations);
$n=count($migrations); $n = count($migrations);
echo "Total $n ".($n===1 ? 'migration':'migrations')." to be reverted:\n"; echo "Total $n " . ($n === 1 ? 'migration' : 'migrations') . " to be reverted:\n";
foreach($migrations as $migration) foreach ($migrations as $migration) {
echo " $migration\n"; echo " $migration\n";
}
echo "\n"; echo "\n";
if($this->confirm('Revert the above '.($n===1 ? 'migration':'migrations')."?")) if ($this->confirm('Revert the above ' . ($n === 1 ? 'migration' : 'migrations') . "?")) {
{ foreach ($migrations as $migration) {
foreach($migrations as $migration) if ($this->migrateDown($migration) === false) {
{
if($this->migrateDown($migration)===false)
{
echo "\nMigration failed. All later migrations are canceled.\n"; echo "\nMigration failed. All later migrations are canceled.\n";
return; return;
} }
@ -186,37 +186,33 @@ class MigrateController extends Controller
public function actionRedo($args) public function actionRedo($args)
{ {
$step=isset($args[0]) ? (int)$args[0] : 1; $step = isset($args[0]) ? (int)$args[0] : 1;
if($step<1) if ($step < 1) {
die("Error: The step parameter must be greater than 0.\n"); 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"; echo "No migration has been done before.\n";
return; return;
} }
$migrations=array_keys($migrations); $migrations = array_keys($migrations);
$n=count($migrations); $n = count($migrations);
echo "Total $n ".($n===1 ? 'migration':'migrations')." to be redone:\n"; echo "Total $n " . ($n === 1 ? 'migration' : 'migrations') . " to be redone:\n";
foreach($migrations as $migration) foreach ($migrations as $migration) {
echo " $migration\n"; echo " $migration\n";
}
echo "\n"; echo "\n";
if($this->confirm('Redo the above '.($n===1 ? 'migration':'migrations')."?")) if ($this->confirm('Redo the above ' . ($n === 1 ? 'migration' : 'migrations') . "?")) {
{ foreach ($migrations as $migration) {
foreach($migrations as $migration) if ($this->migrateDown($migration) === false) {
{
if($this->migrateDown($migration)===false)
{
echo "\nMigration failed. All later migrations are canceled.\n"; echo "\nMigration failed. All later migrations are canceled.\n";
return; return;
} }
} }
foreach(array_reverse($migrations) as $migration) foreach (array_reverse($migrations) as $migration) {
{ if ($this->migrateUp($migration) === false) {
if($this->migrateUp($migration)===false)
{
echo "\nMigration failed. All later migrations are canceled.\n"; echo "\nMigration failed. All later migrations are canceled.\n";
return; return;
} }
@ -227,38 +223,37 @@ class MigrateController extends Controller
public function actionTo($args) public function actionTo($args)
{ {
if(isset($args[0])) if (isset($args[0])) {
$version=$args[0]; $version = $args[0];
else } else {
$this->usageError('Please specify which version to migrate to.'); $this->usageError('Please specify which version to migrate to.');
}
$originalVersion=$version; $originalVersion = $version;
if(preg_match('/^m?(\d{6}_\d{6})(_.*?)?$/',$version,$matches)) if (preg_match('/^m?(\d{6}_\d{6})(_.*?)?$/', $version, $matches)) {
$version='m'.$matches[1]; $version = 'm' . $matches[1];
else } 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"); 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 // try migrate up
$migrations=$this->getNewMigrations(); $migrations = $this->getNewMigrations();
foreach($migrations as $i=>$migration) foreach ($migrations as $i => $migration) {
{ if (strpos($migration, $version . '_') === 0) {
if(strpos($migration,$version.'_')===0) $this->actionUp(array($i + 1));
{
$this->actionUp(array($i+1));
return; return;
} }
} }
// try migrate down // try migrate down
$migrations=array_keys($this->getMigrationHistory(-1)); $migrations = array_keys($this->getMigrationHistory(-1));
foreach($migrations as $i=>$migration) foreach ($migrations as $i => $migration) {
{ if (strpos($migration, $version . '_') === 0) {
if(strpos($migration,$version.'_')===0) if ($i === 0) {
{
if($i===0)
echo "Already at '$originalVersion'. Nothing needs to be done.\n"; echo "Already at '$originalVersion'. Nothing needs to be done.\n";
else } else {
$this->actionDown(array($i)); $this->actionDown(array($i));
}
return; return;
} }
} }
@ -268,32 +263,30 @@ class MigrateController extends Controller
public function actionMark($args) public function actionMark($args)
{ {
if(isset($args[0])) if (isset($args[0])) {
$version=$args[0]; $version = $args[0];
else } else {
$this->usageError('Please specify which version to mark to.'); $this->usageError('Please specify which version to mark to.');
$originalVersion=$version; }
if(preg_match('/^m?(\d{6}_\d{6})(_.*?)?$/',$version,$matches)) $originalVersion = $version;
$version='m'.$matches[1]; if (preg_match('/^m?(\d{6}_\d{6})(_.*?)?$/', $version, $matches)) {
else $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"); 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 // try mark up
$migrations=$this->getNewMigrations(); $migrations = $this->getNewMigrations();
foreach($migrations as $i=>$migration) foreach ($migrations as $i => $migration) {
{ if (strpos($migration, $version . '_') === 0) {
if(strpos($migration,$version.'_')===0) if ($this->confirm("Set migration history at $originalVersion?")) {
{ $command = $db->createCommand();
if($this->confirm("Set migration history at $originalVersion?")) for ($j = 0; $j <= $i; ++$j) {
{
$command=$db->createCommand();
for($j=0;$j<=$i;++$j)
{
$command->insert($this->migrationTable, array( $command->insert($this->migrationTable, array(
'version'=>$migrations[$j], 'version' => $migrations[$j],
'apply_time'=>time(), 'apply_time' => time(),
)); ));
} }
echo "The migration history is set at $originalVersion.\nNo actual migration was performed.\n"; 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 // try mark down
$migrations=array_keys($this->getMigrationHistory(-1)); $migrations = array_keys($this->getMigrationHistory(-1));
foreach($migrations as $i=>$migration) foreach ($migrations as $i => $migration) {
{ if (strpos($migration, $version . '_') === 0) {
if(strpos($migration,$version.'_')===0) if ($i === 0) {
{
if($i===0)
echo "Already at '$originalVersion'. Nothing needs to be done.\n"; echo "Already at '$originalVersion'. Nothing needs to be done.\n";
else } else {
{ if ($this->confirm("Set migration history at $originalVersion?")) {
if($this->confirm("Set migration history at $originalVersion?")) $command = $db->createCommand();
{ for ($j = 0; $j < $i; ++$j) {
$command=$db->createCommand(); $command->delete($this->migrationTable, $db->quoteColumnName('version') . '=:version', array(':version' => $migrations[$j]));
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"; 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) public function actionHistory($args)
{ {
$limit=isset($args[0]) ? (int)$args[0] : -1; $limit = isset($args[0]) ? (int)$args[0] : -1;
$migrations=$this->getMigrationHistory($limit); $migrations = $this->getMigrationHistory($limit);
if($migrations===array()) if ($migrations === array()) {
echo "No migration has been done before.\n"; echo "No migration has been done before.\n";
else } else {
{ $n = count($migrations);
$n=count($migrations); if ($limit > 0) {
if($limit>0) echo "Showing the last $n applied " . ($n === 1 ? 'migration' : 'migrations') . ":\n";
echo "Showing the last $n applied ".($n===1 ? 'migration' : 'migrations').":\n"; } else {
else echo "Total $n " . ($n === 1 ? 'migration has' : 'migrations have') . " been applied before:\n";
echo "Total $n ".($n===1 ? 'migration has' : 'migrations have')." been applied before:\n"; }
foreach($migrations as $version=>$time) foreach ($migrations as $version => $time) {
echo " (".date('Y-m-d H:i:s',$time).') '.$version."\n"; echo " (" . date('Y-m-d H:i:s', $time) . ') ' . $version . "\n";
}
} }
} }
public function actionNew($args) public function actionNew($args)
{ {
$limit=isset($args[0]) ? (int)$args[0] : -1; $limit = isset($args[0]) ? (int)$args[0] : -1;
$migrations=$this->getNewMigrations(); $migrations = $this->getNewMigrations();
if($migrations===array()) if ($migrations === array()) {
echo "No new migrations found. Your system is up-to-date.\n"; echo "No new migrations found. Your system is up-to-date.\n";
else } else {
{ $n = count($migrations);
$n=count($migrations); if ($limit > 0 && $n > $limit) {
if($limit>0 && $n>$limit) $migrations = array_slice($migrations, 0, $limit);
{ echo "Showing $limit out of $n new " . ($n === 1 ? 'migration' : 'migrations') . ":\n";
$migrations=array_slice($migrations,0,$limit); } else {
echo "Showing $limit out of $n new ".($n===1 ? 'migration' : 'migrations').":\n"; echo "Found $n new " . ($n === 1 ? 'migration' : 'migrations') . ":\n";
} }
else
echo "Found $n new ".($n===1 ? 'migration' : 'migrations').":\n";
foreach($migrations as $migration) foreach ($migrations as $migration) {
echo " ".$migration."\n"; echo " " . $migration . "\n";
}
} }
} }
public function actionCreate($args) public function actionCreate($args)
{ {
if(isset($args[0])) if (isset($args[0])) {
$name=$args[0]; $name = $args[0];
else } else {
$this->usageError('Please provide the name of the new migration.'); $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"); die("Error: The name of the migration must contain letters, digits and/or underscore characters only.\n");
}
$name='m'.gmdate('ymd_His').'_'.$name; $name = 'm' . gmdate('ymd_His') . '_' . $name;
$content=strtr($this->getTemplate(), array('{ClassName}'=>$name)); $content = strtr($this->getTemplate(), array('{ClassName}' => $name));
$file=$this->migrationPath.DIRECTORY_SEPARATOR.$name.'.php'; $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); file_put_contents($file, $content);
echo "New migration created successfully.\n"; echo "New migration created successfully.\n";
} }
} }
public function confirm($message)
{
if(!$this->interactive)
return true;
return parent::confirm($message);
}
protected function migrateUp($class) protected function migrateUp($class)
{ {
if($class===self::BASE_MIGRATION) if ($class === self::BASE_MIGRATION) {
return; return;
}
echo "*** applying $class\n"; echo "*** applying $class\n";
$start=microtime(true); $start = microtime(true);
$migration=$this->instantiateMigration($class); $migration = $this->instantiateMigration($class);
if($migration->up()!==false) if ($migration->up() !== false) {
{
$this->getDb()->createCommand()->insert($this->migrationTable, array( $this->getDb()->createCommand()->insert($this->migrationTable, array(
'version'=>$class, 'version' => $class,
'apply_time'=>time(), 'apply_time' => time(),
)); ));
$time=microtime(true)-$start; $time = microtime(true) - $start;
echo "*** applied $class (time: ".sprintf("%.3f",$time)."s)\n\n"; echo "*** applied $class (time: " . sprintf("%.3f", $time) . "s)\n\n";
} } else {
else $time = microtime(true) - $start;
{ echo "*** failed to apply $class (time: " . sprintf("%.3f", $time) . "s)\n\n";
$time=microtime(true)-$start;
echo "*** failed to apply $class (time: ".sprintf("%.3f",$time)."s)\n\n";
return false; return false;
} }
} }
protected function migrateDown($class) protected function migrateDown($class)
{ {
if($class===self::BASE_MIGRATION) if ($class === self::BASE_MIGRATION) {
return; return;
}
echo "*** reverting $class\n"; echo "*** reverting $class\n";
$start=microtime(true); $start = microtime(true);
$migration=$this->instantiateMigration($class); $migration = $this->instantiateMigration($class);
if($migration->down()!==false) if ($migration->down() !== false) {
{ $db = $this->getDb();
$db=$this->getDb(); $db->createCommand()->delete($this->migrationTable, $db->quoteColumnName('version') . '=:version', array(':version' => $class));
$db->createCommand()->delete($this->migrationTable, $db->quoteColumnName('version').'=:version', array(':version'=>$class)); $time = microtime(true) - $start;
$time=microtime(true)-$start; echo "*** reverted $class (time: " . sprintf("%.3f", $time) . "s)\n\n";
echo "*** reverted $class (time: ".sprintf("%.3f",$time)."s)\n\n"; } else {
} $time = microtime(true) - $start;
else echo "*** failed to revert $class (time: " . sprintf("%.3f", $time) . "s)\n\n";
{
$time=microtime(true)-$start;
echo "*** failed to revert $class (time: ".sprintf("%.3f",$time)."s)\n\n";
return false; return false;
} }
} }
protected function instantiateMigration($class) protected function instantiateMigration($class)
{ {
$file=$this->migrationPath.DIRECTORY_SEPARATOR.$class.'.php'; $file = $this->migrationPath . DIRECTORY_SEPARATOR . $class . '.php';
require_once($file); require_once($file);
$migration=new $class; $migration = new $class;
$migration->setDb($this->getDb()); $migration->setDb($this->getDb());
return $migration; return $migration;
} }
@ -456,21 +436,24 @@ class MigrateController extends Controller
* @var CDbConnection * @var CDbConnection
*/ */
private $_db; private $_db;
protected function getDb() protected function getDb()
{ {
if($this->_db!==null) if ($this->_db !== null) {
return $this->_db;
else if(($this->_db=\Yii::$application->getComponent($this->connectionID)) instanceof CDbConnection)
return $this->_db; return $this->_db;
else } else {
die("Error: CMigrationCommand.connectionID '{$this->connectionID}' is invalid. Please make sure it refers to the ID of a CDbConnection application component.\n"); 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) protected function getMigrationHistory($limit)
{ {
$db=$this->getDb(); $db = $this->getDb();
if($db->schema->getTable($this->migrationTable)===null) if ($db->schema->getTable($this->migrationTable) === null) {
{
$this->createMigrationHistoryTable(); $this->createMigrationHistoryTable();
} }
return CHtml::listData($db->createCommand() return CHtml::listData($db->createCommand()
@ -483,34 +466,36 @@ class MigrateController extends Controller
protected function createMigrationHistoryTable() protected function createMigrationHistoryTable()
{ {
$db=$this->getDb(); $db = $this->getDb();
echo 'Creating migration history table "'.$this->migrationTable.'"...'; echo 'Creating migration history table "' . $this->migrationTable . '"...';
$db->createCommand()->createTable($this->migrationTable,array( $db->createCommand()->createTable($this->migrationTable, array(
'version'=>'string NOT NULL PRIMARY KEY', 'version' => 'string NOT NULL PRIMARY KEY',
'apply_time'=>'integer', 'apply_time' => 'integer',
)); ));
$db->createCommand()->insert($this->migrationTable,array( $db->createCommand()->insert($this->migrationTable, array(
'version'=>self::BASE_MIGRATION, 'version' => self::BASE_MIGRATION,
'apply_time'=>time(), 'apply_time' => time(),
)); ));
echo "done.\n"; echo "done.\n";
} }
protected function getNewMigrations() protected function getNewMigrations()
{ {
$applied=array(); $applied = array();
foreach($this->getMigrationHistory(-1) as $version=>$time) foreach ($this->getMigrationHistory(-1) as $version => $time) {
$applied[substr($version,1,13)]=true; $applied[substr($version, 1, 13)] = true;
}
$migrations=array();
$handle=opendir($this->migrationPath); $migrations = array();
while(($file=readdir($handle))!==false) $handle = opendir($this->migrationPath);
{ while (($file = readdir($handle)) !== false) {
if($file==='.' || $file==='..') if ($file === '.' || $file === '..') {
continue; 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]])) $path = $this->migrationPath . DIRECTORY_SEPARATOR . $file;
$migrations[]=$matches[1]; if (preg_match('/^(m(\d{6}_\d{6})_.*?)\.php$/', $file, $matches) && is_file($path) && !isset($applied[$matches[2]])) {
$migrations[] = $matches[1];
}
} }
closedir($handle); closedir($handle);
sort($migrations); sort($migrations);
@ -519,9 +504,9 @@ class MigrateController extends Controller
protected function getTemplate() protected function getTemplate()
{ {
if($this->templateFile!==null) if ($this->templateFile !== null) {
return file_get_contents(Yii::getPathOfAlias($this->templateFile).'.php'); return file_get_contents(Yii::getPathOfAlias($this->templateFile) . '.php');
else } else {
return <<<EOD return <<<EOD
<?php <?php
@ -549,5 +534,6 @@ class {ClassName} extends CDbMigration
*/ */
} }
EOD; EOD;
}
} }
} }

149
framework/console/controllers/ShellController.php

@ -1,149 +0,0 @@
<?php
/**
* ShellController class file.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @link http://www.yiiframework.com/
* @copyright Copyright &copy; 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 <qiang.xue@gmail.com>
* @since 2.0
*/
class ShellController extends Controller
{
/**
* @return string the help information for the shell command
*/
public function getHelp()
{
return <<<EOD
USAGE
yiic shell [entry-script | config-file]
DESCRIPTION
This command allows you to interact with a Web application
on the command line. It also provides tools to automatically
generate new controllers, views and data models.
It is recommended that you execute this command under
the directory that contains the entry script file of
the Web application.
PARAMETERS
* entry-script | config-file: optional, the path to
the entry script file or the configuration file for
the Web application. If not given, it is assumed to be
the 'index.php' file under the current directory.
EOD;
}
/**
* Execute the action.
* @param array $args command line parameters specific for this command
*/
public function run($args)
{
if(!isset($args[0]))
$args[0]='index.php';
$entryScript=isset($args[0]) ? $args[0] : 'index.php';
if(($entryScript=realpath($args[0]))===false || !is_file($entryScript))
$this->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 <<<EOD
Yii Interactive Tool v1.1 (based on Yii v{$yiiVersion})
Please type 'help' for help. Type 'exit' to quit.
EOD;
$this->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
{
}

29
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 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 $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 * @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); $sql = $this->db->getQueryBuilder()->insert($table, $columns, $params);
return $this->setSql($sql)->bindValues($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. * Creates an UPDATE command.
* For example, * For example,
* *

8
framework/db/Exception.php

@ -34,4 +34,12 @@ class Exception extends \yii\base\Exception
$this->errorInfo = $errorInfo; $this->errorInfo = $errorInfo;
parent::__construct($message, $code); parent::__construct($message, $code);
} }
/**
* @return string the user-friendly name of this exception
*/
public function getName()
{
return \Yii::t('yii', 'Database Exception');
}
} }

27
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. * Creates an UPDATE SQL statement.
* For example, * For example,
* *

35
framework/db/mysql/QueryBuilder.php

@ -129,4 +129,39 @@ class QueryBuilder extends \yii\db\QueryBuilder
{ {
return 'SET FOREIGN_KEY_CHECKS=' . ($check ? 1 : 0); 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) . ')';
}
} }

9
framework/logging/DbTarget.php

@ -89,16 +89,17 @@ class DbTarget extends Target
} }
/** /**
* Stores log [[messages]] to DB. * Stores log messages to DB.
* @param boolean $final whether this method is called at the end of the current application * @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(); $db = $this->getDb();
$tableName = $db->quoteTableName($this->tableName); $tableName = $db->quoteTableName($this->tableName);
$sql = "INSERT INTO $tableName (level, category, log_time, message) VALUES (:level, :category, :log_time, :message)"; $sql = "INSERT INTO $tableName (level, category, log_time, message) VALUES (:level, :category, :log_time, :message)";
$command = $db->createCommand($sql); $command = $db->createCommand($sql);
foreach ($this->messages as $message) { foreach ($messages as $message) {
$command->bindValues(array( $command->bindValues(array(
':level' => $message[1], ':level' => $message[1],
':category' => $message[2], ':category' => $message[2],

9
framework/logging/EmailTarget.php

@ -39,13 +39,14 @@ class EmailTarget extends Target
public $headers = array(); public $headers = array();
/** /**
* Sends log [[messages]] to specified email addresses. * Sends log messages to specified email addresses.
* @param boolean $final whether this method is called at the end of the current application * @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 = ''; $body = '';
foreach ($this->messages as $message) { foreach ($messages as $message) {
$body .= $this->formatMessage($message); $body .= $this->formatMessage($message);
} }
$body = wordwrap($body, 70); $body = wordwrap($body, 70);

25
framework/logging/FileTarget.php

@ -65,19 +65,28 @@ class FileTarget extends Target
} }
/** /**
* Sends log [[messages]] to specified email addresses. * Sends log messages to specified email addresses.
* @param boolean $final whether this method is called at the end of the current application * @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) { if (@filesize($this->logFile) > $this->maxFileSize * 1024) {
$this->rotateFiles(); $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);
} }
/** /**

33
framework/logging/Logger.php

@ -8,16 +8,13 @@
*/ */
namespace yii\logging; namespace yii\logging;
use yii\base\InvalidConfigException;
use yii\base\Event;
use yii\base\Exception;
/** /**
* Logger records logged messages in memory. * Logger records logged messages in memory.
* *
* When [[flushInterval()]] is reached or when application terminates, it will * When the application ends or [[flushInterval]] is reached, Logger will call [[flush()]]
* call [[flush()]] to send logged messages to different log targets, such as * to send logged messages to different log targets, such as file, email, Web.
* file, email, Web.
* *
* @author Qiang Xue <qiang.xue@gmail.com> * @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0 * @since 2.0
@ -25,15 +22,6 @@ use yii\base\Exception;
class Logger extends \yii\base\Component 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 * Error message level. An error message is one that indicates the abnormal termination of the
* application and may require developer's handling. * application and may require developer's handling.
*/ */
@ -82,7 +70,7 @@ class Logger extends \yii\base\Component
* *
* ~~~ * ~~~
* array( * array(
* [0] => message (mixed) * [0] => message (mixed, can be a string or some complex data, such as an exception object)
* [1] => level (integer) * [1] => level (integer)
* [2] => category (string) * [2] => category (string)
* [3] => timestamp (float, obtained by microtime(true)) * [3] => timestamp (float, obtained by microtime(true))
@ -90,6 +78,10 @@ class Logger extends \yii\base\Component
* ~~~ * ~~~
*/ */
public $messages = array(); 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. * 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) 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(); $this->messages = array();
} }
@ -149,7 +143,7 @@ class Logger extends \yii\base\Component
* of [[YiiBase]] class file. * of [[YiiBase]] class file.
* @return float the total elapsed time in seconds for current request. * @return float the total elapsed time in seconds for current request.
*/ */
public function getExecutionTime() public function getElapsedTime()
{ {
return microtime(true) - YII_BEGIN_TIME; 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) { if (($last = array_pop($stack)) !== null && $last[0] === $token) {
$timings[] = array($token, $category, $timestamp - $last[3]); $timings[] = array($token, $category, $timestamp - $last[3]);
} else { } 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; return $timings;
} }
} }

21
framework/logging/Router.php

@ -81,26 +81,21 @@ class Router extends Component
$this->targets[$name] = Yii::createObject($target); $this->targets[$name] = Yii::createObject($target);
} }
} }
Yii::getLogger()->router = $this;
Yii::getLogger()->on(Logger::EVENT_FLUSH, array($this, 'processMessages'));
Yii::getLogger()->on(Logger::EVENT_FINAL_FLUSH, array($this, 'processMessages'));
} }
/** /**
* Retrieves and processes log messages from the system logger. * Dispatches log messages to [[targets]].
* This method mainly serves the event handler to the [[Logger::EVENT_FLUSH]] event * This method is called by [[Logger]] when its [[Logger::flush()]] method is called.
* and the [[Logger::EVENT_FINAL_FLUSH]] event. * It will forward the messages to each log target registered in [[targets]].
* It will retrieve the available log messages from the [[Yii::getLogger()|system logger]] * @param array $messages the messages to be processed
* and invoke the registered [[targets|log targets]] to do the actual processing. * @param boolean $final whether this is the final call during a request cycle
* @param \yii\base\Event $event event parameter
*/ */
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) { foreach ($this->targets as $target) {
if ($target->enabled) { if ($target->enabled) {
$target->processMessages($messages, $final); $target->collect($messages, $final);
} }
} }
} }

65
framework/logging/Target.php

@ -50,15 +50,6 @@ abstract class Target extends \yii\base\Component
*/ */
public $except = array(); 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. * @var boolean whether to log a message containing the current user name and ID. Defaults to false.
* @see \yii\web\User * @see \yii\web\User
*/ */
@ -77,19 +68,18 @@ abstract class Target extends \yii\base\Component
public $exportInterval = 1000; public $exportInterval = 1000;
/** /**
* @var array the messages that are retrieved from the logger so far by this log target. * @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; private $_levels = 0;
/** /**
* Exports log messages to a specific destination. * Exports log messages to a specific destination.
* Child classes must implement this method. Note that you may need * Child classes must implement this method.
* to clean up [[messages]] in this method to avoid re-exporting messages. * @param array $messages the messages to be exported. See [[Logger::messages]] for the structure
* @param boolean $final whether this method is called at the end of the current application * of each message.
*/ */
abstract public function exportMessages($final); abstract public function export($messages);
/** /**
* Processes the given log messages. * Processes the given log messages.
@ -99,45 +89,16 @@ abstract class Target extends \yii\base\Component
* of each message. * of each message.
* @param boolean $final whether this method is called at the end of the current application * @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($this->_messages, $this->filterMessages($messages));
$this->messages = array_merge($this->messages, $messages); $count = count($this->_messages);
$count = count($this->messages);
if ($count > 0 && ($final || $this->exportInterval > 0 && $count >= $this->exportInterval)) { if ($count > 0 && ($final || $this->exportInterval > 0 && $count >= $this->exportInterval)) {
$this->prepareExport($final); if (($context = $this->getContextMessage()) !== '') {
$this->exportMessages($final); $this->_messages[] = array($context, Logger::LEVEL_INFO, 'application', YII_BEGIN_TIME);
$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];
} }
} $this->export($this->_messages);
if ($final && ($context = $this->getContextMessage()) !== '') { $this->_messages = array();
$this->messages[] = array($context, Logger::LEVEL_INFO, 'application', YII_BEGIN_TIME);
} }
} }
@ -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 * @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() public function getLevels()
{ {

19
framework/util/FileHelper.php

@ -10,6 +10,7 @@
namespace yii\util; namespace yii\util;
use yii\base\Exception; use yii\base\Exception;
use yii\base\InvalidConfigException;
/** /**
* Filesystem helper * Filesystem helper
@ -37,7 +38,7 @@ class FileHelper
* If the given path does not refer to an existing directory, an exception will be thrown. * 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. * @param string $path the given path. This can also be a path alias.
* @return string the normalized path * @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) public static function ensureDirectory($path)
{ {
@ -45,11 +46,25 @@ class FileHelper
if ($p !== false && ($p = realpath($p)) !== false && is_dir($p)) { if ($p !== false && ($p = realpath($p)) !== false && is_dir($p)) {
return $p; return $p;
} else { } 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. * Returns the localized version of a specified file.
* *
* The searching is based on the specified language code. In particular, * The searching is based on the specified language code. In particular,

103
framework/util/ReflectionHelper.php

@ -1,103 +0,0 @@
<?php
/**
* ReflectionHelper class file.
*
* @link http://www.yiiframework.com/
* @copyright Copyright &copy; 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\util;
use yii\base\Exception;
/**
* ReflectionHelper
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @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;
}
}

10
framework/yiic.php

@ -1,5 +1,4 @@
<?php <?php
define('YII_DEBUG', true);
/** /**
* Yii console bootstrap file. * Yii console bootstrap file.
* *
@ -8,16 +7,17 @@ define('YII_DEBUG', true);
* @license http://www.yiiframework.com/license/ * @license http://www.yiiframework.com/license/
*/ */
defined('YII_DEBUG') or define('YII_DEBUG', true);
// fcgi doesn't have STDIN defined by default // fcgi doesn't have STDIN defined by default
defined('STDIN') or define('STDIN', fopen('php://stdin', 'r')); defined('STDIN') or define('STDIN', fopen('php://stdin', 'r'));
require(__DIR__ . '/yii.php'); require(__DIR__ . '/yii.php');
$config = array(
'controllerPath' => '@yii/console/controllers',
);
$id = 'yiic'; $id = 'yiic';
$basePath = __DIR__ . '/console'; $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(); $application->run();

1
tests/unit/bootstrap.php

@ -1,6 +1,5 @@
<?php <?php
define('YII_ENABLE_EXCEPTION_HANDLER', false);
define('YII_ENABLE_ERROR_HANDLER', false); define('YII_ENABLE_ERROR_HANDLER', false);
define('YII_DEBUG', true); define('YII_DEBUG', true);
$_SERVER['SCRIPT_NAME'] = '/' . __DIR__; $_SERVER['SCRIPT_NAME'] = '/' . __DIR__;

4
todo.md

@ -1,6 +1,6 @@
- db - db
* pgsql, sql server, oracle, db2 drivers * pgsql, sql server, oracle, db2 drivers
* write a guide on creating own schema definitions * unit tests on different DB drivers
* document-based (should allow storage-specific methods additionally to generic ones) * document-based (should allow storage-specific methods additionally to generic ones)
* mongodb (put it under framework/db/mongodb) * mongodb (put it under framework/db/mongodb)
* key-value-based (should allow storage-specific methods additionally to generic ones) * key-value-based (should allow storage-specific methods additionally to generic ones)
@ -8,8 +8,10 @@
- logging - logging
* WebTarget (TBD after web is in place): should consider using javascript and make it into a toolbar * WebTarget (TBD after web is in place): should consider using javascript and make it into a toolbar
* ProfileTarget (TBD after web is in place): should consider using javascript and make it into a toolbar * ProfileTarget (TBD after web is in place): should consider using javascript and make it into a toolbar
* unit tests
- caching - caching
* a console command to clear cached data * a console command to clear cached data
* unit tests
- validators - validators
* FileValidator: depends on CUploadedFile * FileValidator: depends on CUploadedFile
* CaptchaValidator: depends on CaptchaAction * CaptchaValidator: depends on CaptchaAction

Loading…
Cancel
Save