依赖注入模块

从零开始编写一个PHP框架 系列的《依赖注入模块》

项目地址:terse

前言

关于依赖注入,相信小伙伴们都知道它的作用,因为我们要实现控制反转,使代码松耦合,易维护。

stackoverflow 上有个问题:如何向一个五岁小孩解释依赖注入,里面最高分的回答,很有道理。

When you go and get things out of the refrigerator for yourself, you can cause problems. You might leave the door open, you might get something Mommy or Daddy doesn't want you to have. You might even be looking for something we don't even have or which has expired.

What you should be doing is stating a need, "I need something to drink with lunch," and then we will make sure you have something when you sit down to eat.


当你自己从冰箱里取出东西时,你可能会引发问题。 你可能会把门打开,你可能会得到妈妈或爸爸不希望你拥有的东西。 您甚至可能正在寻找我们甚至没有或已经过期的东西。

你应该做的是说明需要,“我需要在午餐时喝点东西”,然后当你坐下来吃饭时我们会确保你有东西。

简而言之,就是我们创造了一个叮当猫,一旦我们想要得到某样东西或者使用某个功能时,只需要向叮当猫索取就行。

需求分析

  • 需要创建一个类(叮当猫),去管理我们需要注入的类。
  • 需要有注册功能
    注册动作(创建一个又一个服务)
    服务管理
  • 需要有获取功能
    获取已经初始化的类(配置类等)
    获取新初始化的类(模型类等)

创建一个依赖注入类

这里使用了 final,因为我们确定这个类是不会被继承和重写的。

<?php
/**
* Injectable
* 依赖注入器
*/
final class Injectable
{
    # todo sth
}

注册部分

关于注册一个服务,我们会有两个场景。

第一个场景,只是单纯的注册一个服务,每次使用都是注册时的状态。

第二个场景,注册的服务进行了一系列初始化,并且在使用时的改变需要继续向下流转,比如 配置类

针对上述两种情况,我们用两个方法去区分他们。

<?php
/**
* Injectable
* 依赖注入器
*/
final class Injectable
{
    /**
     * 设置服务
     * 
     * @param string $name
     * @param mixed  $definition
     */
    public function set($name, $definition)
    {
        # todo sth
    }

    /**
     * 设置静态服务, 该服务获取时只实例化一次
     * 
     * @param string $name
     * @param mixed  $definition
     */
    public function setShared($name, $definition)
    {
        # todo sth
    }
}

注册的动作有了,可是,服务暂时没地方存储,所以我们需要一个 Service 类。

<?php
/**
* Service
* 服务
*/
final class Service
{
    # todo sth
}

根据注册时的需求,仔细分析一下需要定义的属性,如下:

/**
 * 服务别名
 * 
 * @var string
 */
protected $_name;

/**
 * 服务定义
 * 
 * @var mixed
 */
protected $_definition;

/**
 * 是否是静态的共享服务
 * 
 * @var bool
 */
protected $_shared;

/**
 * 实例化(可以理解为单例模式)
 * 
 * @var mixed
 */
protected $_instance;

现在,完善一下这个服务类:

<?php
/**
* Service
* 服务
*/
final class Service
{
    /**
     * 服务别名
     * 
     * @var string
     */
    protected $_name;

    /**
     * 服务定义
     * 
     * @var mixed
     */
    protected $_definition;

    /**
     * 是否是静态的共享服务
     * 
     * @var bool
     */
    protected $_shared;

    /**
     * 实例化(可以理解为单例模式)
     * 
     * @var mixed
     */
    protected $_instance;

    /**
     * 服务构造函数
     * 
     * @param string  $name
     * @param mixed   $definition
     * @param boolean $shared
     */
    function __construct($name, $definition, $shared)
    {
        $this->_name = (string)$name;
        $this->_definition = $definition;
        $this->_shared = (bool)$shared;
    }
}

再次完善依赖注入类:

<?php
/**
* Injectable
* 依赖注入器
*/
final class Injectable
{
    /**
     * 服务
     * 
     * @var Service
     */
    protected $_services;

    /**
     * 设置服务
     * 
     * @param string $name
     * @param mixed  $definition
     */
    public function set($name, $definition)
    {
        $this->_services[$name] = new Service($name, $definition, false);
    }

    /**
     * 设置静态服务, 该服务获取时只实例化一次
     * 
     * @param string $name
     * @param mixed  $definition
     */
    public function setShared($name, $definition)
    {
        $this->_services[$name] = new Service($name, $definition, true);
    }
}

到这里,依赖注入管理器的注册部分就OK了,下面继续写使用部分。

使用部分

跟注册部分类似,使用的时候,也需要根据不同情况获取不同的服务。

比如,在注册时,注册的是一个普通服务(非静态),而在获取时有需求,这时就需要进行区分。

为了给外部判断是否存在某一个服务,这里提供了 has 方法。

<?php
/**
* Injectable
* 依赖注入器
*/
final class Injectable
{
    /**
     * 静态服务
     * 
     * @var array
     */
    protected $_instances;

    /**
     * 是否存在服务
     * 
     * @param  string  $name
     * @return boolean
     */
    public function has($name)
    {
        return isset($this->_services[$name]);
    }

    /**
     * 获取服务
     * 
     * @param  string $name
     * @param  array  $params
     * @return mixed
     */
    public function get($name, array $params = [])
    {
        if (!$this->has($name)) {
            return null;
        }
        $service = $this->_services[$name];
        return $service->resolve($params, $this);
    }


    /**
     * 获取静态服务
     * 
     * @param  string $name
     * @param  array  $params
     * @return mixed
     */
    public function getShared($name, array $params = [])
    {
        if (!$this->has($name)) {
            return null;
        }
        if (isset($this->_instances[$name])) {
            return $this->_instances[$name];
        }
        $instance = $this->get($name, $params);
        $this->_instances[$name] = $instance;
        return $instance;
    }
}

在这里,我们看到了在获取时,用到了 Serviceresolve 方法。在这里我们需要针对多种情况进行分析。

第一种,字符串类型:

在注册时,我们可能会通过如下的方式来注册某个服务。

$di->set('toolString', 'I am a tool.');
$di->set('toolClass', 'ToolClass');
$di->set('toolFoo', 'ToolFoo');

如上所述,当第二个参数的类型是 string 时,可能就是要输出一个字符串,也有可能代表的是一个类名,也有可能代表的是一个方法名。

所以在获取的时候,我们需要对其进行分析。

<?php
/**
* Service
* 服务
*/
final class Service
{
    ...

    /**
     * 服务解析
     * 
     * @param  array       $params
     * @param  Injectable  $di
     * @return mixed
     */
    public function resolve(array $params, $di)
    {
        // 若是静态,且已经实例化,则直接返回实例化的结果
        if ($this->_shared && $this->_instance) {
            return $this->_instance;
        }

        $definition = $this->_definition;

        $type = 'string';
        if (is_string($definition)) {
            $type = $this->stringParse();
        }
        ...
    }

    /**
     * 解析字符串
     * 
     * @return string
     */
    public function stringParse()
    {
        $definition = $this->_definition;
        if (class_exists($definition)) {
            return 'classString';
        }
        if (function_exists($definition)) {
            return 'function';
        }
        return 'string';
    }
}

分析完成后,针对不同的情况来进行相应的初始化和输出。

<?php
/**
* Service
* 服务
*/
final class Service
{
    ...

    /**
     * 服务解析
     * 
     * @param  array       $params
     * @param  Injectable  $di
     * @return mixed
     */
    public function resolve(array $params, $di)
    {
        // 若是静态,且已经实例化,则直接返回实例化的结果
        if ($this->_shared && $this->_instance) {
            return $this->_instance;
        }

        $definition = $this->_definition;

        $type = 'string';
        if (is_string($definition)) {
            $type = $this->stringParse();
        }

        $instance = null;
        switch ($type) {
            case 'function':
                // 先绑定 $this 的作用域
                $definition = \Closure::bind($definition, $di);
                // 调用函数
                $instance = call_user_func_array($definition, $params);
                break;

            case 'classString':
                // 利用反射,实例化类
                $class = new \ReflectionClass($definition);
                $instance = $class->newInstanceArgs($params);
                break;

            case 'class':
            case 'string':
            default:
                $instance = $definition;
                break;
        }

        // 如果是静态的,则需要保存
        if ($this->_shared) {
            $this->_instance = $instance;
        }

        return $instance;
    }

    /**
     * 解析字符串
     * 
     * @return string
     */
    public function stringParse()
    {
        $definition = $this->_definition;
        if (class_exists($definition)) {
            return 'classString';
        }
        if (function_exists($definition)) {
            return 'function';
        }
        return 'string';
    }
}

关于反射和绑定 $this 作用域,大家可以自行查阅相关资料,这里就不多做阐述。

第二种,函数类型。

$di->set('foo', function () {
    echo $this->get('toolString');
});

这一类是属于比较好实现的一类,不过最好还是通过相关方法做一个辨别比较靠谱。

...
if (is_object($definition)) {
    $type = $this->objectParse($params);
}
...

/**
 * 解析对象
 * 
 * @return string
 */
protected function objectParse()
{
    $definition = $this->_definition;
    if ($definition instanceof \Closure) {
        return 'function';
    }
    return 'class';
}

可能有部分同学会遇到 $this 不存在 get 方法的报错。话说,还记得上面那个 \Closure::bind 不?因为我们需要改变函数内部 $this 的作用域后,才可以使用上述方法。

第三种,实例化后的类。

$config = new Config();
$di->set('config', $config);

同样的,处理方法同第二种。

三种情况都已经描述完毕。不过我还是要说一下我的建议,在可能的情况下,不要使用字符串的方式,因为不可控因素比较多。

最后,上一版完整的 Service 类。

<?php
namespace Terse\Di;
/**
* Terse\Di\Service
*
* 服务
*
* @link https://gitee.com/imjcw/terse
* @author imjcw <imjcw@imjcw.com>
*/
final class Service
{
    /**
     * 服务别名
     * 
     * @var string
     */
    protected $_name;

    /**
     * 服务定义
     * 
     * @var mixed
     */
    protected $_definition;

    /**
     * 是否是静态的共享服务
     * 
     * @var bool
     */
    protected $_shared;

    /**
     * 实例化(可以理解为单例模式)
     * 
     * @var mixed
     */
    protected $_instance;

    /**
     * 服务构造函数
     * 
     * @param string  $name
     * @param mixed   $definition
     * @param boolean $shared
     */
    function __construct($name, $definition, $shared)
    {
        $this->_name = (string)$name;
        $this->_definition = $definition;
        $this->_shared = (bool)$shared;
    }

    /**
     * 设置是否只实例化一次
     * 
     * @param string $shared
     */
    public function setShared($shared)
    {
        $this->_shared = (bool)$shared;
    }

    /**
     * 是否shared
     * 
     * @return boolean
     */
    public function isShared()
    {
        return $this->_shared;
    }

    /**
     * 服务解析
     * 
     * @param  array                 $params
     * @param  \Terse\Di\Injectable  $di
     * @return mixed
     */
    public function resolve(array $params, $di)
    {
        if ($this->_shared && $this->_instance) {
            return $this->_instance;
        }

        $definition = $this->_definition;
        $type = 'string';
        if (is_object($definition)) {
            $type = $this->objectParse($params);
        } else if (is_string($definition)) {
            $type = $this->stringParse($params);
        }

        $instance = null;
        switch ($type) {
            case 'function':
                $definition = \Closure::bind($definition, $di);
                $instance = call_user_func_array($definition, $params);
                break;

            case 'classString':
                $class = new \ReflectionClass($definition);
                $instance = $class->newInstanceArgs($params);
                break;

            case 'class':
            case 'string':
            default:
                $instance = $definition;
                break;
        }

        if ($this->_shared) {
            $this->_instance = $instance;
        }
        return $instance;
    }

    /**
     * 解析字符串
     * 
     * @return string
     */
    protected function stringParse()
    {
        $definition = $this->_definition;
        if (class_exists($definition)) {
            return 'classString';
        }
        if (function_exists($definition)) {
            return 'function';
        }
        return 'string';
    }

    /**
     * 解析对象
     * 
     * @return string
     */
    protected function objectParse()
    {
        $definition = $this->_definition;
        if ($definition instanceof \Closure) {
            return 'function';
        }
        return 'class';
    }
}

完善

有了 注册使用 两个功能外,还需要一些其它功能,比如:初始化一些默认服务删除一个服务获取当前单例 等功能。

<?php
namespace Terse\Di;
use Terse\Di\Service;
/**
* Terse\Di\Injectable
*
* 依赖注入器
*
* @link https://gitee.com/imjcw/terse
* @author imjcw <imjcw@imjcw.com>
*/
final class Injectable
{
    /**
     * 服务
     * 
     * @var array
     */
    protected $_services = [];

    /**
     * 静态服务实例化
     * 
     * @var array
     */
    protected $_instances = [];

    /**
     * 当前实例
     * 
     * @var Terse\Di\Injectable
     */
    protected static $_default;

    function __construct()
    {
        $this->_services = [];
        if (!self::$_default) {
            self::$_default = $this;
        }
    }

    /**
     * 获取默认
     * 
     * @return Terse\Di\Injectable
     */
    public static function getDefault()
    {
        return self::$_default;
    }

    /**
     * 设置服务
     * 
     * @param string $name
     * @param mixed $definition
     */
    public function set($name, $definition)
    {
        $this->_services[$name] = new Service($name, $definition, false);
    }

    /**
     * 设置静态服务, 该服务获取时只实例化一次
     * 
     * @param string $name
     * @param mixed $definition
     */
    public function setShared($name, $definition)
    {
        $this->_services[$name] = new Service($name, $definition, true);
    }

    /**
     * 是否存在服务
     * 
     * @param  string  $name
     * @return boolean
     */
    public function has($name)
    {
        return isset($this->_services[$name]);
    }

    /**
     * 获取服务
     * 
     * @param  string $name
     * @param  array  $params
     * @return mixed
     */
    public function get($name, array $params = [])
    {
        if (!$this->has($name)) {
            return null;
        }
        $service = $this->_services[$name];
        return $service->resolve($params, $this);
    }


    /**
     * 获取静态服务
     * 
     * @param  string $name
     * @param  array  $params
     * @return mixed
     */
    public function getShared($name, array $params = [])
    {
        if (!$this->has($name)) {
            return null;
        }
        if (isset($this->_instances[$name])) {
            return $this->_instances[$name];
        }
        $instance = $this->get($name, $params);
        $this->_instances[$name] = $instance;
        return $instance;
    }

    /**
     * 移除服务
     * 
     * @param  string $name
     */
    public function remove($name)
    {
        if ($this->has($name)) {
            unset($this->_services[$name]);
        }
        if (isset($this->_instances[$name])) {
            unset($this->_instances[$name]);
        }
    }
}

总结

到这里,依赖注入管理器的模块已经完成。下一步计划,准备编写 Config 类。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,186评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,858评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,620评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,888评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,009评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,149评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,204评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,956评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,385评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,698评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,863评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,544评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,185评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,899评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,141评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,684评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,750评论 2 351

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,643评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,898评论 25 707
  • lua.exe下载地址 Sublime - Tools - BuildSystem - NewBuildSystem
    姚宏民阅读 267评论 0 0
  • 【品质精装】高层33层,中间97㎡,东边105㎡,西边145㎡,小高层115平。 【价格】 高层精装18000左右...
    DAWEI张阅读 385评论 0 1
  • 《从零到行业引领者》——友邦保险在中国(作者劉明亮先生,原 美国友邦保险 上海分公司 中国北区 副总裁,业务单位 ...
    張蕾馥阅读 1,443评论 0 0