From be8dd940dba036245091db9c3e548fa026ed2e42 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Sat, 12 May 2012 10:42:44 -0400 Subject: [PATCH] ... --- framework/base/Action.php | 13 ++-- framework/base/Application.php | 43 ++++++++---- framework/base/Controller.php | 12 ++-- framework/base/InlineAction.php | 14 ++-- framework/base/Module.php | 55 ++++++---------- framework/console/Application.php | 35 ++++++++-- framework/console/Controller.php | 127 ++---------------------------------- framework/util/ReflectionHelper.php | 79 ++++++++++++++++------ 8 files changed, 163 insertions(+), 215 deletions(-) diff --git a/framework/base/Action.php b/framework/base/Action.php index 59561ee..a87d519 100644 --- a/framework/base/Action.php +++ b/framework/base/Action.php @@ -9,6 +9,8 @@ namespace yii\base; +use yii\util\ReflectionHelper; + /** * Action is the base class for all controller action classes. * @@ -52,13 +54,12 @@ class Action extends Component */ public function runWithParams($params) { - $method = new \ReflectionMethod($this, 'run'); - $params = \yii\util\ReflectionHelper::bindParams($method, $params); - if ($params === false) { - $this->controller->invalidActionParams($this); + try { + $params = ReflectionHelper::extractMethodParams($this, 'run', $params); + return (int)call_user_func_array(array($this, 'run'), $params); + } catch (Exception $e) { + $this->controller->invalidActionParams($this, $e); return 1; - } else { - return (int)$method->invokeArgs($this, $params); } } } diff --git a/framework/base/Application.php b/framework/base/Application.php index 13e4086..c93d414 100644 --- a/framework/base/Application.php +++ b/framework/base/Application.php @@ -73,7 +73,7 @@ use yii\base\Exception; * @author Qiang Xue * @since 2.0 */ -abstract class Application extends Module +class Application extends Module { /** * @var string the application name. Defaults to 'My Application'. @@ -132,13 +132,6 @@ abstract class Application extends Module } /** - * Runs the application. - * This is the main entrance of an application. Derived classes must implement this method. - * @return integer the exit status (0 means normal, non-zero values mean abnormal) - */ - abstract public function run(); - - /** * Terminates the application. * This method replaces PHP's exit() function by calling [[afterRequest()]] before exiting. * @param integer $status exit status (value 0 means normal exit while other values mean abnormal exit). @@ -157,6 +150,19 @@ abstract class Application extends Module } /** + * Runs the application. + * This is the main entrance of an application. + * @return integer the exit status (0 means normal, non-zero values mean abnormal) + */ + public function run() + { + $this->beforeRequest(); + $status = $this->processRequest(); + $this->afterRequest(); + return $status; + } + + /** * Raises the [[beforeRequest]] event right BEFORE the application processes the request. */ public function beforeRequest() @@ -174,16 +180,25 @@ abstract class Application extends Module /** * Processes the request. - * The request is represented in terms of a controller route and action parameters. - * @param string $route the route of the request. It may contain module ID, controller ID, and/or action ID. - * @param array $params parameters to be bound to the controller action. + * Child classes should override this method with actual request processing logic. * @return integer the exit status of the controller action (0 means normal, non-zero values mean abnormal) + */ + public function processRequest() + { + return 0; + } + + /** + * Runs a controller with the given route and parameters. + * @param string $route the route (e.g. `post/create`) + * @param array $params the parameters to be passed to the controller action + * @return integer the exit status (0 means normal, non-zero values mean abnormal) * @throws Exception if the route cannot be resolved into a controller */ - public function processRequest($route, $params = array()) + public function runController($route, $params = array()) { - $result = $this->parseRoute($route); - if ($result === null) { + $result = $this->createController($route); + if ($result === false) { throw new Exception(\Yii::t('yii', 'Unable to resolve the request.')); } list($controller, $action) = $result; diff --git a/framework/base/Controller.php b/framework/base/Controller.php index 61ae94c..f78cfa9 100644 --- a/framework/base/Controller.php +++ b/framework/base/Controller.php @@ -114,7 +114,10 @@ class Controller extends Component implements Initable $this->action = $action; if ($this->authorize($action) && $this->beforeAction($action)) { - $status = $action->runWithParams($params !== null ?: $this->getActionParams()); + if ($params === null) { + $params = $this->getActionParams(); + } + $status = $action->runWithParams($params); $this->afterAction($action); } else { $status = 1; @@ -163,11 +166,12 @@ class Controller extends Component implements Initable * This method is invoked when the request parameters do not satisfy the requirement of the specified action. * The default implementation will throw an exception. * @param Action $action the action being executed + * @param Exception $exception the exception about the invalid parameters * @throws Exception whenever this method is invoked */ - public function invalidActionParams($action) + public function invalidActionParams($action, $exception) { - throw new Exception(\Yii::t('yii', 'Your request is invalid.')); + throw $exception; } /** @@ -219,7 +223,7 @@ class Controller extends Component implements Initable if ($route[0] !== '/' && !$this->module instanceof Application) { $route = '/' . $this->module->getUniqueId() . '/' . $route; } - $status = \Yii::$application->processRequest($route, $params); + $status = \Yii::$application->runController($route, $params); } if ($exit) { \Yii::$application->end($status); diff --git a/framework/base/InlineAction.php b/framework/base/InlineAction.php index f162aca..2bd5582 100644 --- a/framework/base/InlineAction.php +++ b/framework/base/InlineAction.php @@ -9,6 +9,8 @@ namespace yii\base; +use yii\util\ReflectionHelper; + /** * InlineAction represents an action that is defined as a controller method. * @@ -28,13 +30,13 @@ class InlineAction extends Action */ public function runWithParams($params) { - $method = new \ReflectionMethod($this->controller, 'action' . $this->id); - $params = \yii\util\ReflectionHelper::bindParams($method, $params); - if ($params === false) { - $this->controller->invalidActionParams($this); + try { + $method = 'action' . $this->id; + $params = ReflectionHelper::extractMethodParams($this->controller, $method, $params); + return (int)call_user_func_array(array($this->controller, $method), $params); + } catch (Exception $e) { + $this->controller->invalidActionParams($this, $e); return 1; - } else { - return (int)$method->invokeArgs($this, $params); } } } diff --git a/framework/base/Module.php b/framework/base/Module.php index eaa5adb..bc13c62 100644 --- a/framework/base/Module.php +++ b/framework/base/Module.php @@ -481,8 +481,8 @@ abstract class Module extends Component implements Initable } /** - * Parses a given route into controller and action ID. - * The parsing process follows the following algorithm: + * 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 @@ -490,12 +490,12 @@ abstract class Module extends Component implements Initable * and return the controller with the rest part of the route; * - a controller class under [[controllerPath]], create the controller instance, and return it * with the rest part of the route; - * - an ID in [[modules]], let the corresponding module to parse the rest part of the route. + * - an ID in [[modules]], call the [[createController()]] method of the corresponding module. * * @param string $route the route which may consist module ID, controller ID and/or action ID (e.g. `post/create`) - * @return array|null the controller instance and action ID. Null if the route cannot be resolved. + * @return array|boolean the array of controller instance and action ID. False if the route cannot be resolved. */ - public function parseRoute($route) + public function createController($route) { if (($route = trim($route, '/')) === '') { $route = $this->defaultRoute; @@ -509,40 +509,16 @@ abstract class Module extends Component implements Initable $route = ''; } - $controller = $this->createController($id); - if ($controller !== null) { - return array($controller, $route); - } - - if (($module = $this->getModule($id)) !== null) { - return $module->parseRoute($route); - } - - return null; - } - - /** - * Creates a controller instance according to the specified controller ID. - * For security reasons, the controller ID must start with a lower-case letter - * and consist of word characters only. - * - * This method will first match the controller ID in [[controllers]]. If found, - * it will create an instance of controller using the corresponding configuration. - * If not found, it will look for a controller class named `XyzController` under - * the [[controllerPath]] directory, where `xyz` is the controller ID with the first - * letter in upper-case. - * @param string $id the controller ID - * @return Controller|null the created controller instance. Null if the controller ID is invalid. - */ - public function createController($id) - { // 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 null; + return false; } if (isset($this->controllers[$id])) { - return \Yii::createObject($this->controllers[$id], $id, $this); + return array( + \Yii::createObject($this->controllers[$id], $id, $this), + $route, + ); } $className = ucfirst($id) . 'Controller'; @@ -552,10 +528,17 @@ abstract class Module extends Component implements Initable require($classFile); } if (class_exists($className, false) && is_subclass_of($className, '\yii\base\Controller')) { - return $className::newInstance(array(), $id, $this); + return array( + $className::newInstance(array(), $id, $this), + $route, + ); } } - return null; + if (($module = $this->getModule($id)) !== null) { + return $module->createController($route); + } + + return false; } } diff --git a/framework/console/Application.php b/framework/console/Application.php index 345a5be..448acf4 100644 --- a/framework/console/Application.php +++ b/framework/console/Application.php @@ -10,6 +10,7 @@ namespace yii\console; use yii\base\Exception; +use yii\util\ReflectionHelper; /** * Application represents a console application. @@ -71,19 +72,39 @@ class Application extends \yii\base\Application } /** - * Runs the application. - * This is the main entrance of an application. - * @return integer the exit status (0 means normal, non-zero values mean abnormal) + * Processes the request. + * The request is represented in terms of a controller route and action parameters. + * @return integer the exit status of the controller action (0 means normal, non-zero values mean abnormal) + * @throws Exception if the route cannot be resolved into a controller */ - public function run() + public function processRequest() { if (!isset($_SERVER['argv'])) { die('This script must be run from the command line.'); } list($route, $params) = $this->resolveRequest($_SERVER['argv']); - $this->beforeRequest(); - $status = $this->processRequest($route, $params); - $this->afterRequest(); + return $this->runController($route, $params); + } + + /** + * Runs a controller with the given route and parameters. + * @param string $route the route (e.g. `post/create`) + * @param array $params the parameters to be passed to the controller action + * @return integer the exit status (0 means normal, non-zero values mean abnormal) + * @throws Exception if the route cannot be resolved into a controller + */ + public function runController($route, $params = array()) + { + $result = $this->createController($route); + if ($result === false) { + throw new Exception(\Yii::t('yii', 'Unable to resolve the request.')); + } + 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; } diff --git a/framework/console/Controller.php b/framework/console/Controller.php index e488181..1cca900 100644 --- a/framework/console/Controller.php +++ b/framework/console/Controller.php @@ -1,6 +1,6 @@ resolveRequest($args); - $methodName = 'action' . $action; - if (!preg_match('/^\w+$/', $action) || !method_exists($this, $methodName)) - { - $this->usageError("Unknown action: " . $action); - } - - $method = new \ReflectionMethod($this, $methodName); - $params = array(); - // named and unnamed options - foreach ($method->getParameters() as $param) { - $name = $param->getName(); - if (isset($options[$name])) { - if ($param->isArray()) - $params[] = is_array($options[$name]) ? $options[$name] : array($options[$name]); - else if (!is_array($options[$name])) - $params[] = $options[$name]; - else - $this->usageError("Option --$name requires a scalar. Array is given."); - } else if ($name === 'args') - $params[] = $args; - else if ($param->isDefaultValueAvailable()) - $params[] = $param->getDefaultValue(); - else - $this->usageError("Missing required option --$name."); - unset($options[$name]); - } - - // try global options - if (!empty($options)) { - $class = new \ReflectionClass(get_class($this)); - foreach ($options as $name => $value) { - if ($class->hasProperty($name)) { - $property = $class->getProperty($name); - if ($property->isPublic() && !$property->isStatic()) { - $this->$name = $value; - unset($options[$name]); - } - } - } - } - - if (!empty($options)) - $this->usageError("Unknown options: " . implode(', ', array_keys($options))); - - if ($this->beforeAction($action, $params)) { - $method->invokeArgs($this, $params); - $this->afterAction($action, $params); - } - } - - /** - * Parses the command line arguments and determines which action to perform. - * @param array $args command line arguments - * @return array the action name, named options (name=>value), and unnamed options - */ - protected function resolveRequest($args) - { - $options = array(); // named parameters - $params = array(); // unnamed parameters - foreach ($args as $arg) { - if (preg_match('/^--(\w+)(=(.*))?$/', $arg, $matches)) // an option - { - $name = $matches[1]; - $value = isset($matches[3]) ? $matches[3] : true; - if (isset($options[$name])) { - if (!is_array($options[$name])) - $options[$name] = array($options[$name]); - $options[$name][] = $value; - } else - $options[$name] = $value; - } else if (isset($action)) - $params[] = $arg; - else - $action = $arg; - } - if (!isset($action)) - $action = $this->defaultAction; - - return array($action, $options, $params); - } - - /** - * @return string the command name. - */ - public function getName() - { - return $this->_name; - } - - /** * Provides the command description. * This method may be overridden to return the actual command description. * @return string the command description. Defaults to 'Usage: php entry-script.php command-name'. @@ -209,28 +114,6 @@ class Controller extends \yii\base\Controller } /** - * Renders a view file. - * @param string $_viewFile_ view file path - * @param array $_data_ optional data to be extracted as local view variables - * @param boolean $_return_ whether to return the rendering result instead of displaying it - * @return mixed the rendering result if required. Null otherwise. - */ - public function renderFile($_viewFile_, $_data_ = null, $_return_ = false) - { - if (is_array($_data_)) - extract($_data_, EXTR_PREFIX_SAME, 'data'); - else - $data = $_data_; - if ($_return_) { - ob_start(); - ob_implicit_flush(false); - require($_viewFile_); - return ob_get_clean(); - } else - require($_viewFile_); - } - - /** * Reads input via the readline PHP extension if that's available, or fgets() if readline is not installed. * * @param string $message to echo out before waiting for user input diff --git a/framework/util/ReflectionHelper.php b/framework/util/ReflectionHelper.php index b0d0231..8920af6 100644 --- a/framework/util/ReflectionHelper.php +++ b/framework/util/ReflectionHelper.php @@ -9,6 +9,8 @@ namespace yii\util; +use yii\base\Exception; + /** * ReflectionHelper * @@ -19,46 +21,83 @@ class ReflectionHelper { /** * Prepares parameters so that they can be bound to the specified method. - * This method mainly helps method parameter binding. It converts `$params` - * into an array which can be passed to `call_user_func_array()` when calling - * the specified method. The conversion is based on the matching of method parameter names + * 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) { ... } * } - * - * $method = new \ReflectionMethod('Foo', 'bar'); + * $object = new Foo; * $params = array('b' => 2, 'c' => 3, 'a' => 1); - * var_export(ReflectionHelper::bindMethodParams($method, $params)); - * // would output: array('a' => 1, 'b' => 2) + * var_export(ReflectionHelper::extractMethodParams($object, 'bar', $params)); + * // output: array('a' => 1, 'b' => 2); * ~~~ * - * @param \ReflectionMethod $method the method reflection + * @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|boolean the parameters that can be passed to the method via `call_user_func_array()`. - * False is returned if the input parameters do not follow the method declaration. + * @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 bindParams($method, $params) + public static function extractMethodParams($object, $method, $params) { + $m = new \ReflectionMethod($object, $method); $ps = array(); - foreach ($method->getParameters() as $param) { + foreach ($m->getParameters() as $param) { $name = $param->getName(); if (array_key_exists($name, $params)) { - if ($param->isArray()) { - $ps[$name] = is_array($params[$name]) ? $params[$name] : array($params[$name]); - } elseif (!is_array($params[$name])) { - $ps[$name] = $params[$name]; - } else { - return false; - } + $ps[$name] = $params[$name]; } elseif ($param->isDefaultValueAvailable()) { $ps[$name] = $param->getDefaultValue(); } else { - return false; + 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; + } }