Commit 29398f4c by Qiang Xue

MVC WIP

parent 7df4a9bd
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
namespace yii\base; namespace yii\base;
use yii\base\InvalidCallException; use yii\base\InvalidCallException;
use yii\util\StringHelper;
/** /**
* Application is the base class for all application classes. * Application is the base class for all application classes.
...@@ -206,13 +207,13 @@ class Application extends Module ...@@ -206,13 +207,13 @@ class Application extends Module
* @param string $route the route (e.g. `post/create`) * @param string $route the route (e.g. `post/create`)
* @param array $params the parameters to be passed to the controller action * @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) * @return integer the exit status (0 means normal, non-zero values mean abnormal)
* @throws BadRequestException if the route cannot be resolved into a controller * @throws InvalidRequestException if the route cannot be resolved into a controller
*/ */
public function runController($route, $params = array()) public function runController($route, $params = array())
{ {
$result = $this->createController($route); $result = $this->createController($route);
if ($result === false) { if ($result === false) {
throw new BadRequestException(\Yii::t('yii', 'Unable to resolve the request.')); throw new InvalidRequestException(\Yii::t('yii', 'Unable to resolve the request.'));
} }
/** @var $controller Controller */ /** @var $controller Controller */
list($controller, $action) = $result; list($controller, $action) = $result;
...@@ -441,4 +442,126 @@ class Application extends Module ...@@ -441,4 +442,126 @@ class Application extends Module
), ),
)); ));
} }
/**
* Performs a controller action specified by a route.
* This method parses the specified route and creates the corresponding controller and action
* instances under the context of the specified module. It then runs the created action
* with the given parameters.
* @param string $route the route that specifies the action.
* @param array $params the parameters to be passed to the action
* @param Module $module the module which serves as the context of the route
* @return integer the action
* @throws InvalidConfigException if the module's defaultRoute is empty or the controller's defaultAction is empty
* @throws InvalidRequestException if the requested route cannot be resolved into an action successfully
*/
public function runAction($route, $params = array(), $module = null)
{
if ($module === null) {
$module = $this;
}
$route = trim($route, '/');
if ($route === '') {
$route = trim($module->defaultRoute, '/');
if ($route == '') {
throw new InvalidConfigException(get_class($module) . '::defaultRoute cannot be empty.');
}
}
if (($pos = strpos($route, '/')) !== false) {
$id = substr($route, 0, $pos);
$route = substr($route, $pos + 1);
} else {
$id = $route;
$route = '';
}
$childModule = $module->getModule($id);
if ($childModule !== null) {
return $this->runAction($route, $params, $childModule);
}
/** @var $controller Controller */
if (isset($module->controllerMap[$id])) {
$controller = \Yii::createObject($module->controllerMap[$id], $id, $module);
} else {
$controller = $this->createController($id, $module);
if ($controller === null) {
throw new InvalidRequestException("Unable to resolve the request: $route");
}
}
if (isset($controller)) {
$action = $this->createAction($route, $controller);
if ($action !== null) {
return $action->runWithParams($params);
}
}
throw new InvalidRequestException("Unable to resolve the request: $route");
}
/**
* Creates a controller instance based on the controller ID.
*
* The controller is created within the given module. The method first attempts to
* create the controller based on the [[controllerMap]] of the module. If not available,
* it will look for the controller class under the [[controllerPath]] and create an
* instance of it.
*
* @param string $id the controller ID
* @param Module $module the module that owns the controller
* @return Controller the newly created controller instance
*/
public function createController($id, $module)
{
if (isset($module->controllerMap[$id])) {
return \Yii::createObject($module->controllerMap[$id], $id, $module);
} elseif (preg_match('/^[a-z0-9\\-_]+$/', $id)) {
$className = StringHelper::id2camel($id) . 'Controller';
$classFile = $module->controllerPath . DIRECTORY_SEPARATOR . $className . '.php';
if (is_file($classFile)) {
$className = $module->controllerNamespace . '\\' . $className;
if (!class_exists($className, false)) {
require($classFile);
}
if (class_exists($className, false) && is_subclass_of($className, '\yii\base\Controller')) {
return new $className($id, $module);
}
}
}
return null;
}
/**
* Creates an action based on the given action ID.
* The action is created within the given controller. The method first attempts to
* create the action based on [[Controller::actions()]]. If not available,
* it will look for the inline action method within the controller.
* @param string $id the action ID
* @param Controller $controller the controller that owns the action
* @return Action the newly created action instance
* @throws InvalidConfigException if [[Controller::defaultAction]] is empty.
*/
public function createAction($id, $controller)
{
if ($id === '') {
$id = $controller->defaultAction;
if ($id == '') {
throw new InvalidConfigException(get_class($controller) . '::defaultAction cannot be empty.');
}
}
if (isset($controller->actionMap[$id])) {
return \Yii::createObject($controller->actionMap[$id], $id, $controller);
} elseif (preg_match('/^[a-z0-9\\-_]+$/', $id)) {
$methodName = 'action' . StringHelper::id2camel($id);
if (method_exists($controller, $methodName)) {
$method = new \ReflectionMethod($controller, $methodName);
if ($method->getName() === $methodName) {
return new InlineAction($id, $controller);
}
}
}
return null;
}
} }
...@@ -28,7 +28,7 @@ namespace yii\base; ...@@ -28,7 +28,7 @@ namespace yii\base;
class Controller extends Component class Controller extends Component
{ {
/** /**
* @var string ID of this controller * @var string the ID of this controller
*/ */
public $id; public $id;
/** /**
...@@ -36,12 +36,39 @@ class Controller extends Component ...@@ -36,12 +36,39 @@ class Controller extends Component
*/ */
public $module; public $module;
/** /**
* @var string the name of the default action. Defaults to 'index'. * @var string the ID of the action that is used when the action ID is not specified
* in the request. Defaults to 'index'.
*/ */
public $defaultAction = 'index'; public $defaultAction = 'index';
/** /**
* @var array mapping from action ID to action configuration. * @var string|boolean the name of the layout to be applied to this controller's views.
* Array keys are action IDs, and array values are the corresponding * This property mainly affects the behavior of [[render()]].
* Defaults to null, meaning the actual layout value should inherit that from [[module]]'s layout value.
* If false, no layout will be applied.
*/
public $layout;
/**
* @var Action the action that is currently being executed. This property will be set
* by [[run()]] when it is called by [[Application]] to run an action.
*/
public $action;
/**
* @param string $id the ID of this controller
* @param Module $module the module that this controller belongs to.
* @param array $config name-value pairs that will be used to initialize the object properties
*/
public function __construct($id, $module, $config = array())
{
$this->id = $id;
$this->module = $module;
parent::__construct($config);
}
/**
* Declares external actions for the controller.
* This method is meant to be overwritten to declare external actions for the controller.
* It should return an array, with array keys being action IDs, and array values the corresponding
* action class names or action configuration arrays. For example, * action class names or action configuration arrays. For example,
* *
* ~~~ * ~~~
...@@ -55,36 +82,12 @@ class Controller extends Component ...@@ -55,36 +82,12 @@ class Controller extends Component
* ); * );
* ~~~ * ~~~
* *
* [[\Yii::createObject()]] will be invoked to create the requested action * [[\Yii::createObject()]] will be used later to create the requested action
* using the configuration provided here. * using the configuration provided here.
*
* Note, in order to inherit actions defined in the parent class, a child class needs to
* merge the parent actions with child actions using functions like `array_merge()`.
* @see createAction
*/
public $actions = array();
/**
* @var Action the action that is currently being executed
*/ */
public $action; public function actions()
/**
* @var string|boolean the name of the layout to be applied to this controller's views.
* This property mainly affects the behavior of [[render()]].
* Defaults to null, meaning the layout specified by the [[module]] should be used.
* If false, no layout will be applied.
*/
public $layout;
/**
* @param string $id ID of this controller
* @param Module $module the module that this controller belongs to.
* @param array $config name-value pairs that will be used to initialize the object properties
*/
public function __construct($id, $module, $config = array())
{ {
$this->id = $id; return array();
$this->module = $module;
parent::__construct($config);
} }
/** /**
...@@ -139,8 +142,8 @@ class Controller extends Component ...@@ -139,8 +142,8 @@ class Controller extends Component
if ($actionID === '') { if ($actionID === '') {
$actionID = $this->defaultAction; $actionID = $this->defaultAction;
} }
if (isset($this->actions[$actionID])) { if (isset($this->actionMap[$actionID])) {
return \Yii::createObject($this->actions[$actionID], $actionID, $this); return \Yii::createObject($this->actionMap[$actionID], $actionID, $this);
} elseif (method_exists($this, 'action' . $actionID)) { } elseif (method_exists($this, 'action' . $actionID)) {
return new InlineAction($actionID, $this); return new InlineAction($actionID, $this);
} else { } else {
...@@ -188,11 +191,11 @@ class Controller extends Component ...@@ -188,11 +191,11 @@ class Controller extends Component
* This method is invoked when the controller cannot find the requested action. * This method is invoked when the controller cannot find the requested action.
* The default implementation simply throws an exception. * The default implementation simply throws an exception.
* @param string $actionID the missing action name * @param string $actionID the missing action name
* @throws BadRequestException whenever this method is invoked * @throws InvalidRequestException whenever this method is invoked
*/ */
public function missingAction($actionID) public function missingAction($actionID)
{ {
throw new BadRequestException(\Yii::t('yii', 'The system is unable to find the requested action "{action}".', throw new InvalidRequestException(\Yii::t('yii', 'The system is unable to find the requested action "{action}".',
array('{action}' => $actionID == '' ? $this->defaultAction : $actionID))); array('{action}' => $actionID == '' ? $this->defaultAction : $actionID)));
} }
......
<?php <?php
/** /**
* BadRequestException class file. * InvalidRequestException class file.
* *
* @link http://www.yiiframework.com/ * @link http://www.yiiframework.com/
* @copyright Copyright &copy; 2008 Yii Software LLC * @copyright Copyright &copy; 2008 Yii Software LLC
...@@ -10,12 +10,12 @@ ...@@ -10,12 +10,12 @@
namespace yii\base; namespace yii\base;
/** /**
* BadRequestException represents an exception caused by incorrect end user request. * InvalidRequestException represents an exception caused by incorrect end user request.
* *
* @author Qiang Xue <qiang.xue@gmail.com> * @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0 * @since 2.0
*/ */
class BadRequestException extends \Exception class InvalidRequestException extends \Exception
{ {
} }
...@@ -70,7 +70,11 @@ abstract class Module extends Component ...@@ -70,7 +70,11 @@ abstract class Module extends Component
* ) * )
* ~~~ * ~~~
*/ */
public $controllers = array(); public $controllerMap = array();
/**
* @var string the namespace that controller classes are in. Default is to use global namespace.
*/
public $controllerNamespace;
/** /**
* @return string the default route of this module. Defaults to 'default'. * @return string the default route of this module. Defaults to 'default'.
* The route may consist of child module ID, controller ID, and/or action ID. * The route may consist of child module ID, controller ID, and/or action ID.
...@@ -529,69 +533,4 @@ abstract class Module extends Component ...@@ -529,69 +533,4 @@ abstract class Module extends Component
$this->getComponent($id); $this->getComponent($id);
} }
} }
/**
* Creates a controller instance based on the given route.
* This method tries to parse the given route (e.g. `post/create`) using the following algorithm:
*
* 1. Get the first segment in route
* 2. If the segment matches
* - an ID in [[controllers]], create a controller instance using the corresponding configuration,
* and return the controller with the rest part of the route;
* - an ID in [[modules]], call the [[createController()]] method of the corresponding module.
* - a controller class under [[controllerPath]], create the controller instance, and return it
* with the rest part of the route;
*
* @param string $route the route which may consist module ID, controller ID and/or action ID (e.g. `post/create`)
* @return array|boolean the array of controller instance and action ID. False if the route cannot be resolved.
*/
public function createController($route)
{
if (($route = trim($route, '/')) === '') {
$route = $this->defaultRoute;
}
if (($pos = strpos($route, '/')) !== false) {
$id = substr($route, 0, $pos);
$route = (string)substr($route, $pos + 1);
} else {
$id = $route;
$route = '';
}
// Controller IDs must start with a lower-case letter and consist of word characters only
if (!preg_match('/^[a-z][a-zA-Z0-9_]*$/', $id)) {
return false;
}
if (isset($this->controllers[$id])) {
return array(
\Yii::createObject($this->controllers[$id], $id, $this),
$route,
);
}
if (($module = $this->getModule($id)) !== null) {
$result = $module->createController($route);
if ($result !== false) {
return $result;
}
}
$className = ucfirst($id) . 'Controller';
$classFile = $this->getControllerPath() . DIRECTORY_SEPARATOR . $className . '.php';
if (is_file($classFile)) {
if (!class_exists($className, false)) {
require($classFile);
}
if (class_exists($className, false) && is_subclass_of($className, '\yii\base\Controller')) {
return array(
new $className($id, $this),
$route,
);
}
}
return false;
}
} }
...@@ -70,14 +70,14 @@ class Application extends \yii\base\Application ...@@ -70,14 +70,14 @@ class Application extends \yii\base\Application
parent::init(); parent::init();
if ($this->enableCoreCommands) { if ($this->enableCoreCommands) {
foreach ($this->coreCommands() as $id => $command) { foreach ($this->coreCommands() as $id => $command) {
if (!isset($this->controllers[$id])) { if (!isset($this->controllerMap[$id])) {
$this->controllers[$id] = $command; $this->controllerMap[$id] = $command;
} }
} }
} }
// ensure we have the 'help' command so that we can list the available commands // ensure we have the 'help' command so that we can list the available commands
if (!isset($this->controllers['help'])) { if (!isset($this->controllerMap['help'])) {
$this->controllers['help'] = 'yii\console\controllers\HelpController'; $this->controllerMap['help'] = 'yii\console\controllers\HelpController';
} }
} }
......
...@@ -87,7 +87,7 @@ class HelpController extends Controller ...@@ -87,7 +87,7 @@ class HelpController extends Controller
*/ */
public function getActions($controller) public function getActions($controller)
{ {
$actions = array_keys($controller->actions); $actions = array_keys($controller->actionMap);
$class = new \ReflectionClass($controller); $class = new \ReflectionClass($controller);
foreach ($class->getMethods() as $method) { foreach ($class->getMethods() as $method) {
/** @var $method \ReflectionMethod */ /** @var $method \ReflectionMethod */
...@@ -114,7 +114,7 @@ class HelpController extends Controller ...@@ -114,7 +114,7 @@ class HelpController extends Controller
} }
$commands = array(); $commands = array();
foreach (array_keys($module->controllers) as $id) { foreach (array_keys($module->controllerMap) as $id) {
$commands[] = $prefix . $id; $commands[] = $prefix . $id;
} }
......
...@@ -83,4 +83,17 @@ class StringHelper ...@@ -83,4 +83,17 @@ class StringHelper
return trim(strtolower(str_replace('_', $separator, preg_replace('/(?<![A-Z])[A-Z]/', $separator . '\0', $name))), $separator); return trim(strtolower(str_replace('_', $separator, preg_replace('/(?<![A-Z])[A-Z]/', $separator . '\0', $name))), $separator);
} }
} }
/**
* Converts an ID into a CamelCase name.
* Words in the ID separated by `$separator` (defaults to '-') will be concatenated into a CamelCase name.
* For example, 'post-tag' is converted to 'PostTag'.
* @param string $id the ID to be converted
* @param string $separator the character used to separate the words in the ID
* @return string the resulting CamelCase name
*/
public static function id2camel($id, $separator = '-')
{
return str_replace(' ', '', ucwords(implode(' ', explode($separator, $id))));
}
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment