Yii2框架底层分析,服务定位器 [ 2.0 版本 ]
先讲服务定位器,
有些摘录于 http://www.digpage.com/convention.html
Service Locator目的也在于解耦他的模式非常贴合Web这种基于服务和组件的应用的运作特点
优点:
* Service Locator充当了一个运行时的链接器的角色,可以在运行时动态地修改一个类所要选用的服务, 而不必对类作任何的修改。
* 一个类可以在运行时,有针对性地增减、替换所要用到的服务,从而得到一定程度的优化。
* 实现服务提供方、服务使用方完全的解耦,便于独立测试和代码跨框架复用
Service Locator的基本功能
在Yii中Service Locator由 yii\di\ServiceLocator 来实现。 从代码组织上,Yii将Service Locator放到与DI同一层次来对待,都组织在 yii\di 命名空间下。 下面是Service Locator的源代码:
class ServiceLocator extends Component{
// 用于缓存服务、组件等的实例
private $_components = [];
// 用于保存服务和组件的定义,通常为配置数组,可以用来创建具体的实例
private $_definitions = [];
// 重载了 getter 方法,使得访问服务和组件就跟访问类的属性一样。
// 同时,也保留了原来Component的 getter所具有的功能。
// 请留意,ServiceLocator 并未重载 __set(),
// 仍然使用 yii\base\Component::__set()
public function __get($name)
{
... ...
}
// 对比Component,增加了对是否具有某个服务和组件的判断。
public function __isset($name)
{
... ...
}
// 当 $checkInstance === false 时,用于判断是否已经定义了某个服务或组件
// 当 $checkInstance === true 时,用于判断是否已经有了某人服务或组件的实例
public function has($id, $checkInstance = false)
{
return $checkInstance ? isset($this->_components[$id]) :
isset($this->_definitions[$id]);
}
// 根据 $id 获取对应的服务或组件的实例
public function get($id, $throwException = true)
{
... ...
}
// 用于注册一个组件或服务,其中 $id 用于标识服务或组件。
// $definition 可以是一个类名,一个配置数组,一个PHP callable,或者一个对象
public function set($id, $definition)
{
... ...
}
// 删除一个服务或组件
public function clear($id)
{
unset($this->_definitions[$id], $this->_components[$id]);
}
// 用于返回Service Locator的 $_components 数组或 $_definitions 数组,
// 同时也是 components 属性的getter函数
public function getComponents($returnDefinitions = true)
{
... ...
}
// 批量方式注册服务或组件,同时也是 components 属性的setter函数
public function setComponents($components)
{
... ...
}}
从代码可以看出,Service Locator继承自 yii\base\Component ,这是Yii中的一个基础类, 提供了属性、事件、行为等基本功能,关于Component的有关知识,可以看看 属性(Property) 、 事件(Event) 和 行为(Behavior)。
Service Locator 通过 get() isset() has() 等方法, 扩展了 yii\base\Component 的最基本功能,提供了对于服务和组件的属性化支持。
从功能来看,Service Locator提供了注册服务和组件的 set() setComponents() 等方法, 用于删除的 clear() 。用于读取的 get() 和 getComponents() 等方法。
细心的读者可能一看到 setComponents() 和 getComponents() 就猜到了, Service Locator还具有一个可读写的 components 属性。
Service Locator的数据结构
从上面的代码中,可以看到Service Locator维护了两个数组, $_components 和 $_definitions 。这两个数组均是以服务或组件的ID为键的数组。
其中, $_components 用于缓存存Service Locator中的组件或服务的实例。 Service Locator 为其提供了getter和setter。使其成为一个可读写的属性。 $_definitions 用于保存这些组件或服务的定义。这个定义可以是:
* 配置数组。在向Service Locator索要服务或组件时,这个数组会被用于创建服务或组件的实例。 与DI容器的要求类似,当定义是配置数组时,要求配置数组必须要有 class 元素,表示要创建的是什么类。不然你让Yii调用哪个构造函数?
* PHP callable。每当向Service Locator索要实例时,这个PHP callable都会被调用,其返回值,就是所要的对象。 对于这个PHP callable有一定的形式要求,一是它要返回一个服务或组件的实例。 二是它不接受任何的参数。 至于具体原因,后面会讲到。
* 对象。这个更直接,每当你索要某个特定实例时,直接把这个对象给你就是了。
* 类名。即,使得 is_callable($definition, true) 为真的定义。
从 yii\di\ServiceLocator::set() 的代码:
public function set($id, $definition){
// 当定义为 null 时,表示要从Service Locator中删除一个服务或组件
if ($definition === null) {
unset($this->_components[$id], $this->_definitions[$id]);
return;
}
// 确保服务或组件ID的唯一性
unset($this->_components[$id]);
// 定义如果是个对象或PHP callable,或类名,直接作为定义保存
// 留意这里 is_callable的第二个参数为true,所以,类名也可以。
if (is_object($definition) || is_callable($definition, true)) {
// 定义的过程,只是写入了 $_definitions 数组
$this->_definitions[$id] = $definition;
// 定义如果是个数组,要确保数组中具有 class 元素
} elseif (is_array($definition)) {
if (isset($definition['class'])) {
// 定义的过程,只是写入了 $_definitions 数组
$this->_definitions[$id] = $definition;
} else {
throw new InvalidConfigException("The configuration for the \"$id\" component must contain a \"class\" element.");
}
// 这也不是,那也不是,那么就抛出异常吧
} else {
throw new InvalidConfigException(
"Unexpected configuration type for the \"$id\" component: " . gettype($definition)); }}
服务或组件的ID在Service Locator中是唯一的,用于区别彼此。在任何情况下,Service Locator中同一ID只有一个实例、一个定义。也就是说,Service Locator中,所有的服务和组件,只保存一个单例。 这也是正常的逻辑,既然称为服务定位器,你只要给定一个ID,它必然返回一个确定的实例。这一点跟DI容器是一样的。
Service Locator 中ID仅起标识作用,可以是任意字符串,但通常用服务或组件名称来表示。 如,以 db 来表示数据库连接,以 cache 来表示缓存组件等。
至于批量注册的 yii\di\ServiceLocator::setCompoents() 只不过是简单地遍历数组,循环调用 set() 而已。 就算我不把代码贴出来,像你这么聪明的,一下子就可以自己写出来了。
向Service Locator注册服务或组件,其实就是向 $_definitions 数组写入信息而已。
访问Service Locator中的服务Service Locator重载了 get() 使得可以像访问类的属性一样访问已经实例化好的服务和组件。 下面是重载的get() 方法:
public function __get($name){
// has() 方法就是判断 $_definitions 数组中是否已经保存了服务或组件的定义
// 请留意,这个时候服务或组件仅是完成定义,不一定已经实例化
if ($this->has($name)) {
// get() 方法用于返回服务或组件的实例
return $this->get($name);
// 未定义的服务或组件,那么视为正常的属性、行为,
// 调用 yii\base\Component::__get()
} else {
return parent::__get($name);
}}
在注册好了服务或组件定义之后,就可以像访问属性一样访问这些服务(组件)。 前提是已经完成注册,不要求已经实例化。 访问这些服务或属性,被转换成了调用 yii\di\ServiceLocator::get() 来获取实例。 下面是使用这种形式访问服务或组件的例子:
// 创建一个Service Locator$serviceLocator = new yii\di\ServiceLocator;
// 注册一个 cache 服务$serviceLocator->set('cache', [
'class' => 'yii\cache\MemCache',
'servers' => [
... ...
],]);
// 使用访问属性的方法访问这个 cache 服务$serviceLocator->cache->flushValues();
// 上面的方法等效于下面这个$serviceLocator->get('cache')->flushValues();
在Service Locator中,并未重载 __set() 。所以,Service Locator中的服务和组件看起来就好像只读属性一样。 要向Service Locator中“写”入服务和组件,没有 setter 可以使用,需要调用 yii\di\ServiceLocator::set() 对服务和组件进行注册。
通过Service Locator获取实例与注册服务和组件的简单之极相反,Service Locator在创建获取服务或组件实例的过程要稍微复杂一点。 这一点和DI容器也是很像的。 Service Locator通过 yii\di\ServiceLocator::get() 来创建、获取服务或组件的实例:
public function get($id, $throwException = true){
// 如果已经有实例化好的组件或服务,直接使用缓存中的就OK了
if (isset($this->_components[$id])) {
return $this->_components[$id];
}
// 如果还没有实例化好,那么再看看是不是已经定义好
if (isset($this->_definitions[$id])) {
$definition = $this->_definitions[$id];
// 如果定义是个对象,且不是Closure对象,那么直接将这个对象返回
if (is_object($definition) && !$definition instanceof Closure) {
// 实例化后,保存进 $_components 数组中,以后就可以直接引用了
return $this->_components[$id] = $definition;
// 是个数组或者PHP callable,调用 Yii::createObject()来创建一个实例
} else {
// 实例化后,保存进 $_components 数组中,以后就可以直接引用了
return $this->_components[$id] = Yii::createObject($definition);
}
} elseif ($throwException) {
throw new InvalidConfigException("Unknown component ID: $id");
// 即没实例化,也没定义,万能的Yii也没办法通过一个任意的ID,
// 就给你找到想要的组件或服务呀,给你个 null 吧。
// 表示Service Locator中没有这个ID的服务或组件。
} else {
return null;
}}
Service Locator创建获取服务或组件实例的过程是:
* 看看缓存数组 $_components 中有没有已经创建好的实例。有的话,皆大欢喜,直接用缓存中的就可以了。
* 缓存中没有的话,那就要从定义开始创建了。
* 如果服务或组件的定义是个对象,那么直接把这个对象作为服务或组件的实例返回就可以了。 但有一点要注意,当使用一个PHP callable定义一个服务或组件时,这个定义是一个Closure类的对象。 这种定义虽然也对象,但是可不能把这种对象直接当成服务或组件的实例返回。
* 如果定义是一个数组或者一个PHP callable,那么把这个定义作为参数,调用 Yii::createObject() 来创建实例。
`
底层分析开始
$application = new yii\web\Application($config);//先从这入手
$application->run();//先不急,后面会提到
从上面注释的位置入口
$config为配置文件,这里我们来看看是如何加载配置文件内容的。
顺着application我们能找到:yii\web\Application.php
class Application extends \yii\base\Application
yii\web\Application.php中没有构造函数,所以我们顺理成章的找找
它的父类也就是\yii\base\Application,看看父类里面是否有构造函数
\yii\base\Application没有让我们失望,
构造方法如下:
abstract class Application extends Module{
.....
public function __construct($config = [])
{
Yii::$app = $this;
$this->setInstance($this);//将\yii\base\Application中的所有的属性和方法交给Yii::$app->loadedModules数组中
$this->state = self::STATE_BEGIN;
$this->preInit($config);//加载配置文件的框架信息 如:设置别名,设置框架路径等等 最为重要的是给加载默认组件
$this->registerErrorHandler($config);//加载配置文件中的异常组件
Component::__construct($config);//将配置文件中的所有信息赋值给Object,也就是Yii::$app->配置文件参数可以直接调用配置文件的内容 如:Yii::$app->vendorPath//输出框架路径 Yii::$app->components['redis']//输出redis配置信息
}
......
下面我们来分析下面的代码
首先是:Yii::$app = $this;
这一句指的是,将\yii\base\Application里所有的公共方法都交给了,Yii::$app
,其实Yii大部分信息都在Yii::$app
变量中
当然也包括它的父类如:\yii\base\Module \yii\di\ServiceLocator \yii\base\Component \yii\base\Object
$this->setInstance($this);
//module里会用到,为getInstance提供
这一句是指向\yii\base\Module
public static function setInstance($instance)//module模块里会用到,为getInstance提供
{
if ($instance === null) {
unset(Yii::$app->loadedModules[get_called_class()]);
} else {
Yii::$app->loadedModules[get_class($instance)] = $instance;
}
}
这句意思是:将当前类名和存储类的对象变量加入Yii::$app->loadedModules['yii\web\Application']
数组中
这样直接通过Yii::$app->loadedModules['yii\web\Application']
就可以直接调用这个类
重要的用处在于后面的使用如:
在Module里,也就是module使用的时候,可以通过self::getInstance()
获取App对象,类似于Yii::$app
。这个研究的比较浅,以后再深入,有疑问的童鞋可以深入
Yii::$app = $this;
$this->setInstance($this);
这两句做的操作是一样的,其实是有所不同的。
Yii::$app = $this;
指的是通过Yii::$app可以调用yii\web\Application及其父类所有的方法
Yii::$app->loadedModules['yii\web\Application']
//也能同样做到
loadedModules是一个数组,存放成员类的。它除了能调用当前,还能调用其它许许多多的类....
$this->preInit($config);
这一句是将配置文件中的一些变量设置别名,主要是针对路径、URL之类的
$this->registerErrorHandler($config);
加载异常处理,这块比较深就先不研究了,觉得比较浅的童鞋可以接着补充哈
Component::__construct($config);
这一句指向Object
public function __construct($config = [])
{
if (!empty($config)) {
Yii::configure($this, $config);//将配置文件里面的所有配置信息赋值给Object,由于Object是大部分类的基类,实际上也就是交给了yii\web\Application 您可以Yii::$app->配置参数来访问配置文件中的内容
}
$this->init();//下面会细分析
}
foreach ($this->coreComponents() as $id => $component) {//加载默认组件components
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'];
}
}
这个就是把当前的配置文件config变量中内容交给Object 再就是讲components默认需要加载的组件类,赋到config配置文件变量中。
Object是基础类,所以绝大部分类都能直接调用配置文件中配置内容
如:
var_dump(Yii::$app->name);
实际上config文件的数组中有name属性
return [
'id' => 'app-frontend',
'name' => '环球在线',
......
再回到Object
public function __construct($config = [])
{
if (!empty($config)) {
Yii::configure($this, $config);
}// 这以上已经执行完了
$this->init();//接下来分析这一句
}
$this->init();
这句实际上执行的是yii\base\Module.php
/*
取出控制器的命名空间,您也可以理解为路径(* 注:第一次加载它的时候。)
表面看起来没有太多的意义,实则不然,yii2的大部分组件都是以Object为基类的,
所以init函数很重要,控制器、模型、模块module,自定义组件等都可以去实现init方法。
比如说默认的控制器SiteController吧。在里面写一个init方法,当你访问site控制器下任意的$route路径,
都会先执行init方法。作用大不?其它组件同样如此。
*/
public function init()
{
if ($this->controllerNamespace === null) {
$class = get_class($this);
if (($pos = strrpos($class, '\\')) !== false) {
$this->controllerNamespace = substr($class, 0, $pos) . '\\controllers';
}
}
}
至此第一部分执行完了,再看第二部分吧
$application = new yii\web\Application($config);//分析完成
$application->run();//加载主要组件,运行默认控制器
接下拆分 $application->run
吧
用ide指向直接到了\yii\base\Application.php
public function run()
{
try {
$this->state = self::STATE_BEFORE_REQUEST;
$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;
}
}
$response = $this->handleRequest($this->getRequest());
摘出来的这句:
$this->getRequest()
//获取Request对象
这个没啥可说的,获取Request对象
$this->handleRequest($this->getRequest());
通过指向\yii\web\Application.php
public function handleRequest($request)
{
if (empty($this->catchAll)) {
list ($route, $params) = $request->resolve();//取出路由及参数
} else {
$route = $this->catchAll[0];
$params = $this->catchAll;
unset($params[0]);
}
try {
Yii::trace("Route requested: '$route'", __METHOD__);
$this->requestedRoute = $route;
$result = $this->runAction($route, $params);//运行控制器中的Acition,下面有详细介绍
if ($result instanceof Response) {
return $result;
} else {
$response = $this->getResponse();
/*这个是加载yii\base\Response类,在外部可以Yii::$app->get('response')、Yii::$app->getResponse()、Yii::$app->response 等等方式来加载response类,主要用来加载http状态,及头信息,如301,302,404,ajax头等等的获取*/
if ($result !== null) {
$response->data = $result;
}
return $response;
}
} catch (InvalidRouteException $e) {
throw new NotFoundHttpException(Yii::t('yii', 'Page not found.'), $e->getCode(), $e);
}
}
$result = $this->runAction($route, $params);//运行控制器
我们再看看这句指向:\yii\base\Module.php
public function runAction($route, $params = [])
{
$parts = $this->createController($route);//根据路由创建控制器
if (is_array($parts)) {
/* @var $controller Controller */
list($controller, $actionID) = $parts;//获得$actionId和$controller
$oldController = Yii::$app->controller;
Yii::$app->controller = $controller;
$result = $controller->runAction($actionID, $params);//运行使用控制器加载 action方法
Yii::$app->controller = $oldController;//将对象交给Yii::$app->controller 这里面起的作用应该是运行控制器,最后释放控制器的对象变量
return $result;
} else {
$id = $this->getUniqueId();
throw new InvalidRouteException('Unable to resolve the request "' . ($id === '' ? $route : $id . '/' . $route) . '".');
}
}
Module里有一段:
$controller = Yii::createObject($this->controllerMap[$id], [$id, $this]);
其实在
$this->createController($route)
这个时候创建了控制器对象
下面看看如何加载action的。
$result = $controller->runAction($actionID, $params);
//运行使用控制器加载 action方法
上面这句指向:
public function runAction($id, $params = [])
{
$action = $this->createAction($id);//创建action
if ($action === null) {
throw new InvalidRouteException('Unable to resolve the request: ' . $this->getUniqueId() . '/' . $id);
}
Yii::trace("Route to run: " . $action->getUniqueId(), __METHOD__);
if (Yii::$app->requestedAction === null) {
Yii::$app->requestedAction = $action;
}
$oldAction = $this->action;
$this->action = $action;
$modules = [];
$runAction = true;
// 加载默认模块如:Application log等。再调用模块内的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)) {//执行beforeAction
// run the action
$result = $action->runWithParams($params);//执行控制器里的action
$result = $this->afterAction($action, $result);//执行beforeAction
// call afterAction on modules
foreach ($modules as $module) {
/* @var $module Module */
$result = $module->afterAction($action, $result);
}
}
$this->action = $oldAction;
return $result;
}
$result = $action->runWithParams($params);//执行控制器里的action
这里才是真正执行action的地方
首先弄清楚$action是什么类?这里可以var_dump一下就清楚了,刚开始我也被编辑器迷糊了找半天
$action类是yii\base\InlineAction
public function runWithParams($params)
{
$args = $this->controller->bindActionParams($this, $params);//对action的参数进行分析,并且赋值给控制器
Yii::trace('Running action: ' . get_class($this->controller) . '::' . $this->actionMethod . '()', __METHOD__);
if (Yii::$app->requestedParams === null) {
Yii::$app->requestedParams = $args;
}
return call_user_func_array([$this->controller, $this->actionMethod], $args);//用控制器类去执行action方法,并且带上参数。
}
wangjie404
最后登录:2016-08-08
在线时长:4小时5分
- 粉丝4
- 金钱20
- 威望10
- 积分160
共 4 条评论
总体写的还不错,不过有一些地方有疏漏
比较重要的漏点就是:初始化的引导过程bootstrap
另外一个就是controllerMap过程和actionMap过程
这里我想说的是bootstrap过程。我顺着你的代码说:
1.
public function __construct($config = []) { Yii::$app = $this; $this->setInstance($this); $this->state = self::STATE_BEGIN; $this->preInit($config); $this->registerErrorHandler($config); Component::__construct($config); }
上面的代码:Component::construct($config); 最终会执行Object的construct($config)。
Object代码你贴上去了:
public function __construct($config = []) { if (!empty($config)) { Yii::configure($this, $config);//将配置文件里面的所有配置信息赋值给Object,由于Object是大部分类的基类,实际上也就是交给了yii\web\Application 您可以Yii::$app->配置参数来访问配置文件中的内容 } $this->init();//下面会细分析 }
需要注意的是代码: $this->init();
现在回到yii\base\Application
public function init() { $this->state = self::STATE_INIT; $this->bootstrap(); }
在这里可以看到执行了bootstrap()方法,这是系统的引导过程,这个 是非常重要的,被你疏漏了。
protected function bootstrap() { if ($this->extensions === null) { $file = Yii::getAlias('@vendor/yiisoft/extensions.php'); $this->extensions = is_file($file) ? include($file) : []; } foreach ($this->extensions as $extension) { if (!empty($extension['alias'])) { foreach ($extension['alias'] as $name => $path) { Yii::setAlias($name, $path); } } if (isset($extension['bootstrap'])) { $component = Yii::createObject($extension['bootstrap']); if ($component instanceof BootstrapInterface) { Yii::trace('Bootstrap with ' . get_class($component) . '::bootstrap()', __METHOD__); $component->bootstrap($this); } else { Yii::trace('Bootstrap with ' . get_class($component), __METHOD__); } } } foreach ($this->bootstrap as $class) { $component = null; if (is_string($class)) { if ($this->has($class)) { $component = $this->get($class); } elseif ($this->hasModule($class)) { $component = $this->getModule($class); } elseif (strpos($class, '\\') === false) { throw new InvalidConfigException("Unknown bootstrapping component ID: $class"); } } if (!isset($component)) { $component = Yii::createObject($class); } if ($component instanceof BootstrapInterface) { Yii::trace('Bootstrap with ' . get_class($component) . '::bootstrap()', __METHOD__); $component->bootstrap($this); } else { Yii::trace('Bootstrap with ' . get_class($component), __METHOD__); } } }
这里包括 yii插件,组件,模块 这三个方面的bootstrap初始化过程。
详细参看文章:http://www.fancyecommerce.com/2016/05/18/yii2-初始化的bootstrap过程-引导/
这里不细说了。
另外,index.php里面的autoload部分,也得细致说一下各个方面的加载
在另外controllerMap actionMap部分也细致说一下,基本就齐全了。
这个后面的内容不就是这篇文章的吗?http://www.yiichina.com/code/546
$_definitions是在哪被赋值的呢?