资讯专栏INFORMATION COLUMN

PHP IOC/DI 容器 - 依赖自动注入/依赖单例注入/依赖契约注入/参数关联传值

Paul_King / 3033人阅读

摘要:标量参数关联传值依赖是自动解析注入的,剩余的标量参数则可以通过关联传值,这样比较灵活,没必要把默认值的参数放在函数参数最尾部。

更新:github(给个小星星呀

-- 2018-4-11:优化服务绑定方法 ::bind 的类型检查模式

借助 PHP 反射机制实现的一套 依赖自动解析注入 的 IOC/DI 容器,可以作为 Web MVC 框架 的应用容器

1、依赖的自动注入:你只需要在需要的位置注入你需要的依赖即可,运行时容器会自动解析依赖(存在子依赖也可以自动解析)将对应的实例注入到你需要的位置。

2、依赖的单例注入:某些情况下我们需要保持依赖的全局单例特性,比如 Web 框架中的 Request 依赖,我们需要将整个请求响应周期中的所有注入 Request 依赖的位置同步为在路由阶段解析完请求体的 Request 实例,这样我们在任何位置都可以访问全局的请求体对象。

3、依赖的契约注入:比如我们依赖某 Storage,目前使用 FileStorage 来实现,后期发现性能瓶颈,要改用 RedisStorage 来实现,如果代码中大量使用 FileStorage 作为依赖注入,这时候就需要花费精力去改代码了。我们可以使用接口 Storage 作为契约,将具体的实现类 FileStorage / RedisStorage 通过容器的绑定机制关联到 Storage 上,依赖注入 Storage,后期切换存储引擎只需要修改绑定即可。

4、标量参数关联传值:依赖是自动解析注入的,剩余的标量参数则可以通过关联传值,这样比较灵活,没必要把默认值的参数放在函数参数最尾部。这点我还是蛮喜欢 python 的函数传值风格的。

function foo($name, $age = 27, $sex)
{
    // php 没办法 foo($name = "big cat", $sex = "male") 这样传值
    // 只能 foo("big cat", 27, "male") 传值...
    // python 可以 foo(name = "big cat", sex = "male") 很舒服
}

但这也使得我的容器不支持位序传值,必须保证运行参数的键名与运行方法的参数名准确的关联映(有默认值的参数可以省略),我想着并没有什么不方便的地方吧,我不喜欢给 $bar 参数传递个 $foo 变量。

容器源码
 $provider,
            "singleton" => $singleton,
        ];
    }

    /**
     * 获取类实例
     * 通过反射获取构造参数
     * 返回对应的类实例
     * @param  [type] $class_name [description]
     * @return [type]             [description]
     */
    private static function getInstance($class_name)
    {
        //方法参数分为 params 和 default_values
        //如果一个开放构造类作为依赖注入传入它类,我们应该将此类注册为全局单例服务
        $params = static::getParams($class_name);
        return (new ReflectionClass($class_name))->newInstanceArgs($params["params"]);
    }

    /**
     * 反射方法参数类型
     * 对象参数:构造对应的实例 同时检查是否为单例模式的实例
     * 标量参数:返回参数名 索引路由参数取值
     * 默认值参数:检查路由参数中是否存在本参数 无则取默认值
     * @param  [type] $class_name [description]
     * @param  string $method     [description]
     * @return [type]             [description]
     */
    private static function getParams($class_name, $method = "__construct")
    {
        $params_set["params"] = array();
        $params_set["default_values"] = array();

        //反射检测类是否显示声明或继承父类的构造方法
        //若无则说明构造参数为空
        if ($method == "__construct") {
            $classRf = new ReflectionClass($class_name);
            if (! $classRf->hasMethod("__construct")) {
                return $params_set;
            }
        }

        //反射方法 获取参数
        $methodRf = new ReflectionMethod($class_name, $method);
        $params = $methodRf->getParameters();

        if (! empty($params)) {
            foreach ($params as $key => $param) {
                if ($paramClass = $param->getClass()) {// 对象参数 获取对象实例
                    $param_class_name = $paramClass->getName();
                    if (array_key_exists($param_class_name, static::$dependencyServices)) {// 是否为注册的服务
                        if (static::$dependencyServices[$param_class_name]["singleton"]) {// 单例模式直接返回已注册的实例
                            $params_set["params"][] = static::$dependencyServices[$param_class_name]["provider"];
                        } else {// 非单例则返回提供者的新的实例
                            $params_set["params"][] = static::getInstance(static::$dependencyServices[$param_class_name]["provider"]);
                        }
                    } else {// 没有做绑定注册的类
                        $params_set["params"][] = static::getInstance($param_class_name);
                    }
                } else {// 标量参数 获取变量名作为路由映射 包含默认值的记录默认值
                    $param_name = $param->getName();

                    if ($param->isDefaultValueAvailable()) {// 是否包含默认值
                        $param_default_value = $param->getDefaultValue();
                        $params_set["default_values"][$param_name] = $param_default_value;
                    }

                    $params_set["params"][] = $param_name;
                }
            }
        }

        return $params_set;
    }

    /**
     * 容器的运行入口 主要负责加载类方法,并将运行所需的标量参数做映射和默认值处理
     * @param  [type] $class_name 运行类
     * @param  [type] $method     运行方法
     * @param  array  $params     运行参数
     * @return [type]             输出
     */
    public static function run($class_name, $method, array $params = array())
    {
        if (! class_exists($class_name)) {
            throw new Exception($class_name . "not found!", 4040);
        }

        if (! method_exists($class_name, $method)) {
            throw new Exception($class_name . "::" . $method . " not found!", 4041);
        }

        // 获取要运行的类
        $classInstance = static::getInstance($class_name);
        // 获取要运行的方法的参数
        $method_params = static::getParams($class_name, $method);
        
        // 关联传入的运行参数
        $method_params = array_map(function ($param) use ($params, $method_params) {
            if (is_object($param)) {// 对象参数 以完成依赖解析的具体实例
                return $param;
            }

            // 以下为关联传值 可通过参数名映射的方式关联传值 可省略含有默认值的参数
            if (array_key_exists($param, $params)) {// 映射传递路由参数
                return $params[$param];
            }

            if (array_key_exists($param, $method_params["default_values"])) {// 默认值
                return $method_params["default_values"][$param];
            }

            throw new Exception($param . " is necessary parameters", 4042); // 路由中没有的则包含默认值
        }, $method_params["params"]);

        // 运行
        return call_user_func_array([$classInstance, $method], $method_params);
    }
}
演示所需的依赖类
// 它将被以单例模式注入 全局的所有注入点都使用的同一实例
class Foo
{
    public $msg = "foo nothing to say!";

    public function index()
    {
        $this->msg = "foo hello, modified by index method!";
    }
}

// 它将以普通依赖模式注入 各注入点会分别获取一个实例
class Bar
{
    public $msg = "bar nothing to say!";

    public function index()
    {
        $this->msg = "bar hello, modified by index method!";
    }
}

// 契约注入
interface StorageEngine
{
    public function info();
}

// 契约实现
class FileStorageEngine implements StorageEngine
{
    public $msg = "file storage engine!" . PHP_EOL;

    public function info()
    {
        $this->msg =  "file storage engine!" . PHP_EOL;
    }
}

// 契约实现
class RedisStorageEngine implements StorageEngine
{
    public $msg = "redis storage engine!" . PHP_EOL;

    public function info()
    {
        $this->msg =  "redis storage engine!" . PHP_EOL;
    }
}
演示所需的运行类
// 具体的运行类
class BigCatController
{
    public $foo;
    public $bar;

    // 这里自动注入一次 Foo 和 Bar 的实例
    public function __construct(Foo $foo, Bar $bar)
    {
        $this->foo = $foo;
        $this->bar = $bar;
    }

    // 这里的参数你完全可以乱序的定义(我故意写的很乱序),你只需保证 route 参数中存在对应的必要参数即可
    // 默认值参数可以直接省略
    public function index($name = "big cat", Foo $foo, $sex = "male", $age, Bar $bar, StorageEngine $se)
    {
        // Foo 为单例模式注入 $this->foo $foo 是同一实例
        $this->foo->index();
        echo $this->foo->msg . PHP_EOL;
        echo $foo->msg . PHP_EOL;
        echo "------------------------------" . PHP_EOL;

        // Bar 为普通模式注入 $this->bar $bar 为两个不同的 Bar 的实例
        $this->bar->index();
        echo $this->bar->msg . PHP_EOL;
        echo $bar->msg . PHP_EOL;
        echo "------------------------------" . PHP_EOL;

        // 契约注入 具体看你为契约者绑定了哪个具体的实现类
        // 我们绑定的 RedisStorageEngine 所以这里注入的是 RedisStorageEngine 的实例
        $se->info();
        echo $se->msg;
        echo "------------------------------" . PHP_EOL;

        // 返回个值
        return "name " . $name . ", age " . $age . ", sex " . $sex . PHP_EOL;
    }
}
运行
// 路由信息很 MVC 吧
$route = [
    "controller" => BigCatController::class, // 运行的类
    "action"     => "index", // 运行的方法
    "params"     => [ // 运行的参数
        "name" => "big cat",
        "age"  => 27 // sex 有默认值 不传
    ]
];

try {
    // 依赖的单例注册
    IOCContainer::singleton(Foo::class, new Foo());

    // 依赖的契约注册 StorageEngine 相当于契约者 注册关联具体的实现类
    // IOCContainer::bind(StorageEngine::class, FileStorageEngine::class);
    IOCContainer::bind(StorageEngine::class, RedisStorageEngine::class);
    
    // 运行
    $result = IOCContainer::run($route["controller"], $route["action"], $route["params"]);
    
    echo $result;
} catch (Exception $e) {
    echo $e->getMessage();
}
运行结果
foo hello, modified by index method!
foo hello, modified by index method!
------------------------------
bar hello, modified by index method!
bar nothing to say!
------------------------------
redis storage engine!
------------------------------
name big cat, age 27, sex male

简单的实现了像 laraval 的 IOC 容器的特性,但比它多一项(可能也比较鸡肋)标量参数的关联传值,不过我这功能也限定死了你传入的参数必须与函数定义的参数名相关联,可我还是觉得能充分的填补默认参数不放在参数尾就无法跳过的强迫症问题.....

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/23216.html

相关文章

  • PHP程序员如何理解依赖注入容器(dependency injection container)

    摘要:代码这就是控制反转模式。是变量有默认值则设置默认值是一个类,递归解析有默认值则返回默认值从容器中取得以上代码的原理参考官方文档反射,具有完整的反射,添加了对类接口函数方法和扩展进行反向工程的能力。 PHP程序员如何理解依赖注入容器(dependency injection container) 背景知识 传统的思路是应用程序用到一个Foo类,就会创建Foo类并调用Foo类的方法,假如这...

    Coding01 评论0 收藏0
  • 聊一聊PHP依赖注入(DI) 和 控制反转(IoC)

    摘要:前言最近在使用框架,看了下他的源码,发现有很多地方也用到了依赖注入控制反转,觉得有必要和大家简单聊一聊什么是依赖注入以及怎么使用它。概念依赖注入和控制反转是对同一件事情的不同描述,从某个方面讲,就是它们描述的角度不同。 前言 最近在使用ThinkPHP5框架,看了下他的源码,发现有很多地方也用到了依赖注入(控制反转),觉得有必要和大家简单聊一聊什么是依赖注入以及怎么使用它。 简介 I...

    sixgo 评论0 收藏0
  • PHP程序员如何理解IoC/DI

    摘要:依赖注入容器管理应用程序中的全局对象包括实例化处理依赖关系。为了解决这样的问题,我们再次回到全局注册表创建组件。参考文章程序员如何理解依赖注入容器补充很多代码背后,都是某种哲学思想的体现。 思想 思想是解决问题的根本思想必须转换成习惯构建一套完整的思想体系是开发能力成熟的标志——《简单之美》(前言) . 成功的软件项目就是那些提交产物达到或超出客户的预期的项目,而且开发过程符合时间和费...

    DataPipeline 评论0 收藏0
  • Spring笔记01_下载_概述_监听器

    摘要:简单来说,是一个轻量级的控制反转和面向切面的容器框架。变成的支持提供面向切面编程,可以方便的实现对程序进行权限拦截,运行监控等功能。用于反射创建对象,默认情况下调用无参构造函数。指定对象的作用范围。 1.Spring介绍 1.1 Spring概述 Spring是一个开源框架,Spring是于2003 年兴起的一个轻量级的Java 开发框架,由Rod Johnson 在其著作Expert...

    reclay 评论0 收藏0
  • DIP、IoCDI、JS

    摘要:维基百科该原则规定高层次的模块不应该依赖与低层次的模块,两者都应该依赖于抽象接口。依赖反转原则则颠倒这种依赖关系,并以上面提到的两个规定作为指导思想。维基百科这些话的意思就是将依赖对象的创建和绑定转移到被依赖对象类的外部来实现。 在这个标题中,除了 JS 是乱入之外,其它的几个词汇都是存在一个共同点的,那就是依赖。 那么,依赖是什么呢? 比如,现在我正在写这篇博客文,但是我得在电脑上编...

    ssshooter 评论0 收藏0

发表评论

0条评论

Paul_King

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<