wangjie404 2016-05-20 11:45:31 16831次浏览 4条评论 14 6 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方法,并且带上参数。
}
觉得很赞
  • 评论于 2016-05-20 17:01 举报

    总体写的还不错,不过有一些地方有疏漏

  • 评论于 2016-05-20 17:11 举报

    比较重要的漏点就是:初始化的引导过程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部分也细致说一下,基本就齐全了。

    觉得很赞
  • 评论于 2016-07-31 14:35 举报

    这个后面的内容不就是这篇文章的吗?http://www.yiichina.com/code/546

  • 评论于 2016-11-18 18:56 举报

    $_definitions是在哪被赋值的呢?

您需要登录后才可以评论。登录 | 立即注册