/ yii2  

Yii2 源码阅读 07 - Controller

|--- yii\base\BaseObject
|--- Component
|--- Action
|--- InlineAction

|--- Controller <-- 本节
|--- ErrorHandler
|--- Model
|--- DynamicModel

|--- Request
|--- Response
|--- Security
|--- Theme
|--- View
|--- Widget
|--- yii\di\ServiceLocator
|--- Module
|--- Application

|--- Behavior
|--- ActionFilter

|--- Event
|--- ActionEvent
|--- ModelEvent
|--- ViewEvent
|--- WidgetEvent
|--- yii\base\Controller
|--- yii\web\Controller
|--- yii\rest\Controller
|--- ActiveController

|--- yii\console\Controller
|--- AssetController
|--- BaseMigrateController
|--- MigrateController

|--- CacheController
|--- FixtureController
|--- HelpController
|--- ServeController

yii\base\Controller

参见:控制器(Controllers)

属性:

  • @property-read Module[] $modules 此控制器所在的所有祖先模块。
  • @property-read string $route 当前请求的路由(module ID, controller ID and action ID)。
  • @property-read string $uniqueId 以模块 ID(如果有)作为前缀的控制器 ID。可以理解为路由路径。
  • @property View|\yii\web\View $view 可以用来呈现视图或视图文件的视图对象。
  • @property string $viewPath 包含此控制器的视图文件的目录。
class Controller extends Component implements ViewContextInterface

ViewContextInterface 是那些想要 support relative view names 的类应该实现的接口。

  • public function getViewPath(); 返回以相对视图名作为前缀的视图路径。

属性:

  • string $id controller ID。
  • Module $module controller 所属 module。
  • string $defaultAction 当请求中没有指定 action ID 时使用的 action ID。默认 ‘index’。
  • null|string|false $layout 要应用于此控制器视图的布局的名称。此属性主要影响 render() 的行为。默认为 null,意味着实际的布局值应该从 module 的布局值继承。如果为 false,将不应用布局。
  • Action|null $action 当前正在执行的 action。当 Application 调用该属性来运行一个 action 时,run() 将设置该属性。
  • Request|array|string $request
  • Response|array|string $response
/**
* 为控制器声明外部 actions。
* 这个方法应该被覆盖以声明控制器的外部操作。
* 它应该返回一个数组,其中数组键是操作id,数组值是对应的操作类名或操作配置数组。
*
* Yii::createObject() 稍后将使用这里提供的配置来创建请求的操作。
*
* @return array
*/
public function actions()
{
return [];
}

// 例子
public function actions()
{
return [
'action1' => 'app\components\Action1',
'action2' => [
'class' => 'app\components\Action2',
'property1' => 'value1',
'property2' => 'value2',
],
];
}
/**
* 使用指定的 action ID 和参数在该控制器中运行一个 action。
* 如果 action ID 为空,该方法将使用 defaultAction。
* @param string $id 需要执行的 action ID。
* @param array $params 要传递给 action 的参数(名称-值对 name-value pairs)。
* @return mixed the result of the action.
* @throws InvalidRouteException 如果请求的 action ID 不能成功解析为 action。
* @see createAction()
*/
public function runAction($id, $params = [])
{
// 根据 ID 得到一个 Action 对象
$action = $this->createAction($id);
if ($action === null) {
throw new InvalidRouteException('Unable to resolve the request: ' . $this->getUniqueId() . '/' . $id);
}

Yii::debug('Route to run: ' . $action->getUniqueId(), __METHOD__);

// 记录当前 Action
if (Yii::$app->requestedAction === null) {
Yii::$app->requestedAction = $action;
}

// 获取上一个 Action
$oldAction = $this->action;
$this->action = $action;

$modules = [];
$runAction = true;

// call beforeAction on modules
foreach ($this->getModules() as $module) {
//
// $event = new ActionEvent($action);
// $this->trigger(self::EVENT_BEFORE_ACTION, $event);
// return $event->isValid;
//
if ($module->beforeAction($action)) {
// 在数组开头插入
array_unshift($modules, $module);
} else {
$runAction = false;
break;
}
}

$result = null;

// 和 $module->beforeAction($action) 实现一样
if ($runAction && $this->beforeAction($action)) {
// run the action
// 这里有点疑问 为什么不直接调用 $action->run 呢?
// 现在理解可能是为了对 run 在进行封装操作,提交 action 的 beforeRun & afterRun
$result = $action->runWithParams($params);

//
// $event = new ActionEvent($action);
// $event->result = $result;
// $this->trigger(self::EVENT_AFTER_ACTION, $event);
// return $event->result;
//
$result = $this->afterAction($action, $result);

// call afterAction on modules
foreach ($modules as $module) {
/* @var $module Module */
$result = $module->afterAction($action, $result);
}
}

if ($oldAction !== null) {
$this->action = $oldAction;
}

return $result;
}
/**
* 根据给定的 action ID 创建 action。
*
* 该方法首先检查 action ID 是否在 actions() 中声明。
* 如果是,它将使用声明的配置来创建 action 对象。
* 如果不是,它将寻找一个名称为 actionXyz 格式的控制器方法,其中 xyz 是 action ID。
* 如果找到,表示该方法的 InlineAction 将被创建并返回,否则返回 null。
*
* @param string $id the action ID.
* @return Action|null 新创建的操作实例。如果 ID 没有分解为任何操作,则为空。
*/
public function createAction($id)
{
if ($id === '') {
// 设置默认 action id index
$id = $this->defaultAction;
}

$actionMap = $this->actions();
if (isset($actionMap[$id])) {
// 存在于 actions()
return Yii::createObject($actionMap[$id], [$id, $this]);
}

if (preg_match('/^(?:[a-z0-9_]+-)*[a-z0-9_]+$/', $id)) {
// xyz-abc => actionXyzAbc
// 中划线转大驼峰
$methodName = 'action' . str_replace(' ', '', ucwords(str_replace('-', ' ', $id)));
// 验证方法是否存在
if (method_exists($this, $methodName)) {
$method = new \ReflectionMethod($this, $methodName);
if ($method->isPublic() && $method->getName() === $methodName) {
// method 需要 public && method name 相等
// 内联 Action
// 将 ctrl 中的方法封装成了一个 InlineAction 对象
// InlineAction runWithParams 就是调用这个 ctrl 的这个方法
return new InlineAction($id, $this, $methodName);
}
}
}

return null;
}
/**
* 运行根据路由指定的请求。
* 路由可以是该控制器内某个 action ID,也可以是由模块ID、控制器ID和动作ID组成的完整路由。
* 如果路由以斜线 '/' 开头,则解析该路由将从应用程序开始;
* 否则,它将从该控制器的父模块开始。
*
* @param string $route the route to be handled, e.g., 'view', 'comment/view', '/admin/comment/view'.
* @param array $params the parameters to be passed to the action.
* @return mixed the result of the action.
* @see runAction()
*/
public function run($route, $params = [])
{
$pos = strpos($route, '/');
if ($pos === false) {
// 没有 /,从 ctrl 下开始
// 'view'
return $this->runAction($route, $params);
} elseif ($pos > 0) {
// 包含 /,从模块下开始
// 'comment/view'
return $this->module->runAction($route, $params);
}

// 如果路由以斜线 '/' 开头,则解析该路由将从应用程序开始
// '/admin/comment/view'
return Yii::$app->runAction(ltrim($route, '/'), $params);
}
/**
* 根据动作方法签名中的类型和名称填充参数。
* Fills parameters based on types and names in action method signature.
* @param \ReflectionType $type 动作参数的反射类型。
* @param string $name The name of the parameter.
* @param array &$args 动作的参数数组,此函数可以将项目附加到它。
* @param array &$requestedParams 具有请求参数的数组,此函数可能会向其写入特定键。
* @throws ErrorException when we cannot load a required service.
* @throws InvalidConfigException Thrown when there is an error in the DI configuration.
* @throws NotInstantiableException 在容器中没有正确定义的情况下无法将定义解析为具体类(例如接口类型提示)时抛出。
* @since 2.0.36
*/
final protected function bindInjectedParams(\ReflectionType $type, $name, &$args, &$requestedParams)
{
// Since it is not a builtin type it must be DI injection.
// 由于它不是内置类型,因此必须是 DI 注入。
$typeName = $type->getName();
if (($component = $this->module->get($name, false)) instanceof $typeName) {
$args[] = $component;
$requestedParams[$name] = "Component: " . get_class($component) . " \$$name";
} elseif ($this->module->has($typeName) && ($service = $this->module->get($typeName)) instanceof $typeName) {
$args[] = $service;
$requestedParams[$name] = 'Module ' . get_class($this->module) . " DI: $typeName \$$name";
} elseif (\Yii::$container->has($typeName) && ($service = \Yii::$container->get($typeName)) instanceof $typeName) {
$args[] = $service;
$requestedParams[$name] = "Container DI: $typeName \$$name";
} elseif ($type->allowsNull()) {
$args[] = null;
$requestedParams[$name] = "Unavailable service: $name";
} else {
throw new Exception('Could not load required service: ' . $name);
}
}

// 这个方法还不清楚作用,是在 web/Controller console/Controller 中被调用。

yii\web\Controller

属性:

  • bool $enableCsrfValidation 是否对该控制器中的 actions 启用 CSRF 验证。只有当这个 property 和 yii\web\Request::enableCsrfValidation 都为 true 时,CSRF 验证才被启用。
  • array $actionParams 绑定到当前 action 的参数。

方法:

/**
* 响应 AJAX 请求呈现视图。
*
* 这个方法类似于 renderPartial(),不同的是它会用 JS/CSS 脚本和注册在视图中的文件注入到渲染结果中。
* 出于这个原因,您应该使用此方法而不是 renderPartial() 来呈现一个视图以响应 AJAX 请求。
*
* @param string $view the view name. Please refer to [[render()]] on how to specify a view name.
* @param array $params the parameters (name-value pairs) that should be made available in the view.
* @return string the rendering result.
*/
public function renderAjax($view, $params = [])
{
return $this->getView()->renderAjax($view, $params, $this);
}

// 这个方法没有使用过 还不清楚作用
// Send data formatted as JSON.
public function asJson($data)
{
$this->response->format = Response::FORMAT_JSON;
$this->response->data = $data;
return $this->response;
}
// return $this->asJson($data);

// Send data formatted as XML.
public function asXml($data)
{
$this->response->format = Response::FORMAT_XML;
$this->response->data = $data;
return $this->response;
}
// return $this->asXml($data);

// 这两个方法异曲同工,操作 response 的 format 类型。框架已内置的方法。

非常核心的方法,在 Action 中的 runWithParams 里调用:

/**
* 将参数绑定到 action。
* 当 \yii\base\Action 以给定的参数开始运行时,该方法被 \yii\base\Action 调用。
* 该方法将检查操作需要的参数名称,并根据需求返回他提供的参数。
* 如果有任何丢失的参数,就会抛出异常。
*
* @param \yii\base\Action $action 要用参数绑定的 action
* @param array $params 要绑定到 action 的参数
* @return array action 可以使用的有效参数
* @throws BadRequestHttpException if there are missing or invalid parameters.
*/
public function bindActionParams($action, $params)
{
// 获取 action 要执行的方法
if ($action instanceof InlineAction) {
$method = new \ReflectionMethod($this, $action->actionMethod);
} else {
$method = new \ReflectionMethod($action, 'run');
}

$args = [];
$missing = [];
$actionParams = [];
$requestedParams = [];

// 变量方法参数
foreach ($method->getParameters() as $param) {
// 参数名
$name = $param->getName();
// 验证参数类型
if (array_key_exists($name, $params)) {
$isValid = true;
if (PHP_VERSION_ID >= 80000) {
$isArray = ($type = $param->getType()) instanceof \ReflectionNamedType && $type->getName() === 'array';
} else {
$isArray = $param->isArray();
}
if ($isArray) {
// 参数是数组
$params[$name] = (array)$params[$name];
} elseif (is_array($params[$name])) {
$isValid = false;
} elseif (
PHP_VERSION_ID >= 70000
&& ($type = $param->getType()) !== null
&& $type->isBuiltin()
&& ($params[$name] !== null || !$type->allowsNull())
) {
$typeName = PHP_VERSION_ID >= 70100 ? $type->getName() : (string)$type;

if ($params[$name] === '' && $type->allowsNull()) {
if ($typeName !== 'string') { // for old string behavior compatibility
$params[$name] = null;
}
} else {
switch ($typeName) {
case 'int':
$params[$name] = filter_var($params[$name], FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE);
break;
case 'float':
$params[$name] = filter_var($params[$name], FILTER_VALIDATE_FLOAT, FILTER_NULL_ON_FAILURE);
break;
case 'bool':
$params[$name] = filter_var($params[$name], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
break;
}
if ($params[$name] === null) {
$isValid = false;
}
}
}
if (!$isValid) {
throw new BadRequestHttpException(
Yii::t('yii', 'Invalid data received for parameter "{param}".', ['param' => $name])
);
}
$args[] = $actionParams[$name] = $params[$name];
unset($params[$name]);
} elseif (PHP_VERSION_ID >= 70100 && ($type = $param->getType()) !== null && !$type->isBuiltin()) {
// !$type->isBuiltin() 不是内建类型
try {
$this->bindInjectedParams($type, $name, $args, $requestedParams);
} catch (HttpException $e) {
throw $e;
} catch (Exception $e) {
throw new ServerErrorHttpException($e->getMessage(), 0, $e);
}
} elseif ($param->isDefaultValueAvailable()) {
$args[] = $actionParams[$name] = $param->getDefaultValue();
} else {
$missing[] = $name;
}
}

if (!empty($missing)) {
throw new BadRequestHttpException(
Yii::t('yii', 'Missing required parameters: {params}', ['params' => implode(', ', $missing)])
);
}

$this->actionParams = $actionParams;

// We use a different array here, specifically one that doesn't contain service instances but descriptions instead.
if (Yii::$app->requestedParams === null) {
Yii::$app->requestedParams = array_merge($actionParams, $requestedParams);
}

return $args;
}

yii\console\Controller

yii <route> [--param1=value1 --param2 ...]

属性:

  • @property-read string $help
  • @property-read string $helpSummary
  • @property-read array $passedOptionValues 与传递的选项对应的属性。
  • @property-read array $passedOptions 执行期间传递的选项的名称。
  • public $interactive = true; 是否以交互方式运行命令。
  • public $silentExitOnException; 如果为真 - 脚本以 ExitCode::OK 结束,以防出现异常。Default: YII_ENV_TEST.

方法:

/**
* 使用指定的操作 ID 和参数运行操作。
* 如果动作 ID 为空,该方法将使用 [[defaultAction]]。
* @param string $id the ID of the action to be executed.
* @param array $params the parameters (name-value pairs) to be passed to the action.
* @return int 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.
* @throws Exception if there are unknown options or missing arguments
* @see createAction
*/
public function runAction($id, $params = [])
{
if (!empty($params)) {
// 在此处填充选项,以便它们在 beforeAction() 中可用。
$options = $this->options($id === '' ? $this->defaultAction : $id);
// 是否设置了别名
if (isset($params['_aliases'])) {
$optionAliases = $this->optionAliases();
foreach ($params['_aliases'] as $name => $value) {
if (array_key_exists($name, $optionAliases)) {
$params[$optionAliases[$name]] = $value;
} else {
$message = Yii::t('yii', 'Unknown alias: -{name}', ['name' => $name]);
if (!empty($optionAliases)) {
$aliasesAvailable = [];
foreach ($optionAliases as $alias => $option) {
$aliasesAvailable[] = '-' . $alias . ' (--' . $option . ')';
}

$message .= '. ' . Yii::t('yii', 'Aliases available: {aliases}', [
'aliases' => implode(', ', $aliasesAvailable)
]);
}
throw new Exception($message);
}
}
unset($params['_aliases']);
}
// 遍历参数
foreach ($params as $name => $value) {
// 允许在 kebab-case 中输入 camelCase 选项
if (!in_array($name, $options, true) && strpos($name, '-') !== false) {
$kebabName = $name;
$altName = lcfirst(Inflector::id2camel($kebabName));
if (in_array($altName, $options, true)) {
$name = $altName;
}
}

if (in_array($name, $options, true)) {
$default = $this->$name;
if (is_array($default) && is_string($value)) {
$this->$name = preg_split('/\s*,\s*(?![^()]*\))/', $value);
} elseif ($default !== null) {
settype($value, gettype($default));
$this->$name = $value;
} else {
$this->$name = $value;
}
$this->_passedOptions[] = $name;
unset($params[$name]);
if (isset($kebabName)) {
unset($params[$kebabName]);
}
} elseif (!is_int($name)) {
$message = Yii::t('yii', 'Unknown option: --{name}', ['name' => $name]);
if (!empty($options)) {
$message .= '. ' . Yii::t('yii', 'Options available: {options}', ['options' => '--' . implode(', --', $options)]);
}

throw new Exception($message);
}
}
}

if ($this->help) {
// 没有参数 & 开启帮助时执行到
// yii\console\controllers\HelpController
$route = $this->getUniqueId() . '/' . $id;
return Yii::$app->runAction('help', [$route]);
}

return parent::runAction($id, $params);
}

yii\rest\Controller

  1. Resolving response format (see ContentNegotiator);
  2. Validating request method (see verbs()).
  3. Authenticating user (see \yii\filters\auth\AuthInterface);
  4. Rate limiting (see RateLimiter);
  5. Formatting response data (see serializeData()).

– EOF –