最近在看 yii2 源码,想在入门的基础上掌握 yii2 更多的设计思路和设计风格,以便更好的理解 yii2
以 yii 2.0.14 高级版的 frontend 为例,从 frontend/web/index.php 开始
//引用 yii2 composer 的 autoload,调用 getLoader
require __DIR__ . '/../../vendor/autoload.php';
//引用 yii.php
require __DIR__ . '/../../vendor/yiisoft/yii2/Yii.php';
//引用 bootstrap.php 定义一些别名等
require __DIR__ . '/../../common/config/bootstrap.php';
require __DIR__ . '/../config/bootstrap.php';
//合并配置文件
$config = yii\helpers\ArrayHelper::merge(
require __DIR__ . '/../../common/config/main.php',
require __DIR__ . '/../../common/config/main-local.php',
require __DIR__ . '/../config/main.php',
require __DIR__ . '/../config/main-local.php'
);
(new yii\web\Application($config))->run();
入口文件看着就这么几行,简单的很,那他是怎么通过这几行来运行应用的呢?先看 Yii.php 内的逻辑
/**
* Yii::autoload 内执行过程
* 1、先查看类是否在 Yii::$classMap 中存在,存在直接调用 getAlias 生成类文件物理地址
* 2、如果 Yii::$classMap 中不存在,将命名空间转为实际路径调用 getAlias 生成类文件物理地址
*/
spl_autoload_register(['Yii', 'autoload'], true, true);
//yii2 核心类的类名和物理文件地址映射的 hash 数组
Yii::$classMap = require __DIR__ . '/classes.php';
/**
* 实例化 依赖注入(Dependency Injection,DI)容器
* 依赖注入容器知道怎样初始化并配置对象及其依赖的所有对象
* 在Yii中使用DI解耦,有2种注入方式:构造函数注入、属性注入
* yii\di\Container 继承了
* yii\base\Component
* yii\base\BaseObject
* BaseObject 实现了 Configurable
* DI容器只支持 yii\base\Object 类
* 如果你的类想放在DI容器里,那么必须继承自 yii\base\Object 类
* 参考地址:
* http://www.digpage.com/di.html
* https://www.cnblogs.com/minirice/p/yii2_configurations.html
*/
Yii::$container = new yii\di\Container();
接下来,就是重头戏,yii\web\Application,它继承了
yii\base\Application
yii\base\Module
yii\di\ServiceLocator(服务定位器)
yii\base\Component
yii\base\BaseObject, BaseObject 实现 Configurable
PS:继承 Component 的都有 on event 和 as behavior 配置实现事件绑定
一、new yii\web\Application 时,会调用构造方法 yii\base\Application::__construct
public function __construct($config = [])
{
Yii::$app = $this;
//application 对象放到注册树中
static::setInstance($this);
$this->state = self::STATE_BEGIN;
/**
* 初始化 application 中应用属性的一些值,配置一些高优先级的应用属性
* 还会初始化 components 中,log、user、urlManager 对应的类文件
* foreach ($this->coreComponents() as $id => $component) {
* if (!isset($config['components'][$id])) {
* $config['components'][$id] = $component;
* } elseif (
* is_array($config['components'][$id])
* && !isset($config['components'][$id]['class'])
* ) {
* $config['components'][$id]['class'] = $component['class'];
* }
* }
*
* yii\web\Application 中,coreComponents 的代码
* public function coreComponents()
* {
* return array_merge(parent::coreComponents(), [
* 'request' => ['class' => 'yii\web\Request'],
* 'response' => ['class' => 'yii\web\Response'],
* 'session' => ['class' => 'yii\web\Session'],
* 'user' => ['class' => 'yii\web\User'],
* 'errorHandler' => ['class' => 'yii\web\ErrorHandler'],
* ]);
* }
*
* yii\base\Application 中,coreComponents 的代码
* public function coreComponents()
* {
* return [
* 'log' => ['class' => 'yii\log\Dispatcher'],
* 'view' => ['class' => 'yii\web\View'],
* 'formatter' => ['class' => 'yii\i18n\Formatter'],
* 'i18n' => ['class' => 'yii\i18n\I18N'],
* 'mailer' => ['class' => 'yii\swiftmailer\Mailer'],
* 'urlManager' => ['class' => 'yii\web\UrlManager'],
* 'assetManager' => ['class' => 'yii\web\AssetManager'],
* 'security' => ['class' => 'yii\base\Security'],
* ];
* }
*
* 从2.0.11 开始,配置支持使用 container 属性来配置依赖注入容器
* 'container' => [
* 'definitions' => [
* 'yii\widgets\LinkPager' => ['maxButtonCount' => 5]
* ],
* 'singletons' => [
* // 依赖注入容器单例配置
* ]
* ]
*
*
*/
$this->preInit($config);
/**
* registerErrorHandler 内代码
* 1、调用 $this->set('errorHandler', $config['components']['errorHandler'])
* 将 errorHandler 配置放到 ServiceLocator (_definitions 数组中,这时还没实例化)
* 2、调用 $this->getErrorHandler()->register()
* 调用 getErrorHandler,使用 createObject 调用 Container 依赖注入容器实例化对象
* 调用 yii\web\ErrorHandler::register,初始化错误异常显示和抛出
*/
$this->registerErrorHandler($config);
/**
* 在多层继承中,调用上级某一层的构造函数,而不是单纯的父类构造函数
* 上级某一层的构造函数中如果调用了某个方法
* 并且这个方法被下层类重写过,那么会直接执行重写之后的方法
* 所以执行 Component::__construct,__construct 中调用 init()
* 会执行 yii\base\Application 的 init
* 如果上级调用下级重写的 静态方法 时
* 要使用延时静态绑定(上级静态调用 self::a() 改为 static::a())
*/
Component::__construct($config);
}
二、yii\base\Application::init 代码
public function init()
{
$this->state = self::STATE_INIT;
$this->bootstrap();
}
三、yii\web\Application::bootstrap 代码
protected function bootstrap()
{
/**
* 通过 Application::get('request')
* 使用 createObject 实现调用 Container 依赖注入容器实例化对象
*/
$request = $this->getRequest();
//定义别名
Yii::setAlias('@webroot', dirname($request->getScriptFile()));
Yii::setAlias('@web', $request->getBaseUrl());
//调用 yii\base\Application::bootstrap 代码
parent::bootstrap();
}
四、yii\base\Application::bootstrap 代码太多,不展示源码了,大致总结为
1、是否在配置文件中配置了 extensions 参数,如果没有配置,直接加载扩展清单文件 @vendor/yiisoft/extensions.php,否则使用配置的 extensions。然后在 extensions 文件返回的数组中,可有含有 alias 和 bootstrap 参数,根据 alias 中的参数定义别名,根据 bootstrap 中的参数,使用 createObject 实例化对象(创建并运行各个扩展声明的 引导组件 )
2、根据配置文件配置的 bootstrap 参数,使用 createObject 实例化对象(创建并运行各个 应用组件 以及在应用的 bootstrap 属性中声明的各个 模块组件 )
3、注意:extensions 文件中配置的 bootstrap 和 配置文件中配置的 bootstrap,如果实现了 BootstrapInterface 接口,还会执行实例化后的 bootstrap 方法
4、注意:bootstrap 会直接将配置的类实例化,而不是在第一次使用的时候实例化,所以为了性能考虑 bootstrap 中的配置应该尽量少,而且只配置一些全局使用的类
五、yii\base\Application::run 代码
public function run()
{
try {
$this->state = self::STATE_BEFORE_REQUEST;
/**
* trigger 触发通知,将此事件通知给绑定到这个事件的观察者,绑定事件的方法:
* yii\base\Component 或者其子类::on("事件名称","方法")
*/
$this->trigger(self::EVENT_BEFORE_REQUEST);
$this->state = self::STATE_HANDLING_REQUEST;
$response = $this->handleRequest($this->getRequest());
$this->state = self::STATE_AFTER_REQUEST;
$this->trigger(self::EVENT_AFTER_REQUEST);
$this->state = self::STATE_SENDING_RESPONSE;
$response->send();
$this->state = self::STATE_END;
return $response->exitStatus;
} catch (ExitException $e) {
$this->end($e->statusCode, isset($response) ? $response : null);
return $e->statusCode;
}
}
六、yii\web\Application::handleRequest 代码
public function handleRequest($request)
{
if (empty($this->catchAll)) {
try {
//resolve 方法调用 urlManager 对 url 进行解析
list($route, $params) = $request->resolve();
} catch (UrlNormalizerRedirectException $e) {
$url = $e->url;
if (is_array($url)) {
if (isset($url[0])) {
$url[0] = '/' . ltrim($url[0], '/');
}
$url += $request->getQueryParams();
}
return $this->getResponse()->redirect(Url::to($url, $e->scheme), $e->statusCode);
}
} else {
/**
* 如果设置了 catchAll 变量, 那么所有请求都会跳转到这里
* 示例:
* 假设网站维护, 需要将网站重定向到一个设置好的页面上
* 可以在配置文件中添加
* 'catchAll' => ['offline/index']
* 这样, 所有的访问都跳转到 offline/index 页面了
*/
$route = $this->catchAll[0];
$params = $this->catchAll;
unset($params[0]);
}
try {
Yii::debug("Route requested: '$route'", __METHOD__);
$this->requestedRoute = $route;
//根据 route 访问对应的 module/controller/action
$result = $this->runAction($route, $params);
if ($result instanceof Response) {
return $result;
}
$response = $this->getResponse();
if ($result !== null) {
$response->data = $result;
}
return $response;
} catch (InvalidRouteException $e) {
throw new NotFoundHttpException(Yii::t('yii', 'Page not found.'), $e->getCode(), $e);
}
}
七、yii\base\Module::runAction 代码
public function runAction($route, $params = [])
{
/**
* yii\base\Module::createController 代码也不贴了,可以追进去看,思路是
* 1、如果 route 是空(直接通过域名访问应用 www.aaa.com)
* 使用配置中的 defaultRoute 属性
* 2、route 不为空,查看配置文件中是否有 controllerMap 的配置
* 直接使用配置创建
* controllerMap 配置如
* [
* 'controllerMap' => [
* // 用类名申明 "account" 控制器
* 'account' => 'app\controllers\UserController',
* // 用配置数组申明 "article" 控制器
* 'article' => [
* 'class' => 'app\controllers\PostController',
* 'enableCsrfValidation' => false,
* ]
* ]
* ]
*
* 3、调用 yii/base/Module::getModule 查看 route 中是否有 module 存在
* 如果直接调用yii/base/Module::createController 方法
* 否则调用 yii/base/Module::createControllerByID
* 通过 createControllerByID 实例化的 Controller 类,必须继承 yii\base\Controller
* createController 和 createControllerByID 都使用 Yii::createObject 实例化
*/
$parts = $this->createController($route);
if (is_array($parts)) {
list($controller, $actionID) = $parts;
$oldController = Yii::$app->controller;
Yii::$app->controller = $controller;
$result = $controller->runAction($actionID, $params);
if ($oldController !== null) {
Yii::$app->controller = $oldController;
}
return $result;
}
$id = $this->getUniqueId();
throw new InvalidRouteException('Unable to resolve the request "' . ($id === '' ? $route : $id . '/' . $route) . '".');
}
八、说明一下 yii/base/Module::getModule 这个很有意思
1、先看一下配置文件时 modules 配置后的赋值过程
我们使用 modules 时,需要在配置文件中配置 modules,比如
'modules' => [
'v1' => [
'class' => 'frontend\modules\v1\Module',
],
],
或者像 main-local.php 中那样,新建一个 config,$config 中配置
$config['modules']['gii'] = [
'class' => 'yii\gii\Module',
];
这个 modules 的属性,在 Application 及其父类中,都是不存在的
只有私有属性 config) (不清楚的往上看,上边有这块代码)
然后 Component::__construct(config) ,然后方法中执行
if (!empty($config)) {
Yii::configure($this, $config);
}
再调用 yii\base\Component::setter 方法 (yii\base\Module::setModules),将 $_modules 赋值
2、如果 module 套着 module,需要这么这么设置
'modules' => [
'v1' => [
'class' => 'frontend\modules\v1\Module',
'modules' => [
'v2' => 'frontend\modules\v2\Module'
],
],
],
九、yii\base\Controller::runAction 代码
public function runAction($id, $params = [])
{
/**
* yii\base\Controller::createAction 代码也不贴了,可以追进去看,思路是
* 1、如果 action id 是空(访问 www.aaa.com/controller)
* 使用 yii\base\Controller 中的 defaultAction 属性
*
* 2、id 不为空,查看 Controller::actions 方法中是否有配置
* 如果有,直接使用配置创建,actions 配置如
*
* public function actions()
* {
* return [
* 'error' => [
* 'class' => 'yii\web\ErrorAction',
* ],
* 'captcha' => [
* 'class' => 'yii\captcha\CaptchaAction',
* 'fixedVerifyCode' => YII_ENV_TEST ? 'testme' : null,
* ],
* ];
* }
*
* 3、利用反射(ReflectionMethod)查看调用方法是否存在,是否是公共方法
* 如果是,返回 yii\base\InlineAction 的实例
*/
$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__);
if (Yii::$app->requestedAction === null) {
Yii::$app->requestedAction = $action;
}
$oldAction = $this->action;
$this->action = $action;
$modules = [];
$runAction = true;
//调用所有加载模块中的 beforeAction 方法
foreach ($this->getModules() as $module) {
if ($module->beforeAction($action)) {
array_unshift($modules, $module);
} else {
$runAction = false;
break;
}
}
$result = null;
if ($runAction && $this->beforeAction($action)) {
$result = $action->runWithParams($params);
$result = $this->afterAction($action, $result);
//调用所有加载模块中的 afterAction 方法
foreach ($modules as $module) {
$result = $module->afterAction($action, $result);
}
}
if ($oldAction !== null) {
$this->action = $oldAction;
}
return $result;
}
最后,附个图,源自
http://www.yiichina.com/doc/guide/2.0/structure-applications