PHP 依赖注入容器实现

0x00 前言

在看 Laravel 文档的时候发现入门指南的下一章即是核心架构,对于我这种循序渐进往下读的同窗这简直是劝退篇。各类以前没有接触过的概念砸得人头晕,容器即是其中之一。不过在拜读过几篇文章后也逐渐理解了容器的做用,因此特此总结一番。php

0x01 为什么要有容器?

这个问题能够也能够替换为「容器解决了什么问题?」。在此以前咱们须要理解依赖注入这个概念,能够看一下这篇文章:简单解释什么是 依赖注入 和 控制反转。在实践依赖注入的时候咱们会遇到一个问题,这里我将经过示例代码解释,代码以下:html

class Bread {
}

class Bacon {
}

class Hamburger {
    protected $materials;

    public function __construct(Bread $bread, Bacon $bacon) {
        $this->materials = [$bread, $bacon];
    }
}

class Cola {
}

class Meal {
    protected $food;

    protected $drink;

    public function __construct(Hamburger $hamburger, Cola $cola) {
        $this->food  = $hamburger;
        $this->drink = $cola;
    }
}
复制代码

上面是按照依赖注入实现的一段代码,咱们能够看见套餐类(Meal)依赖汉堡类(Hamburger)和可乐类(Cola),而且汉堡类又依赖于面包类(Bread)和培根类(Bacon)。经过依赖注入能达到松耦合的效果可是这也使得实例化一个有多个依赖的类会变得十分麻烦,下面这段代码是实例化一个套餐类的示例:laravel

$bread = new Bread();
$bacon = new Bacon();

$hamburger = new Hamburger($bread, $bacon);
$cola = new Cola();

$meal = new Meal($hamburger, $cola);
复制代码

能够看见为了得到一个套餐对象,咱们须要先实例化该对象的依赖,若是依赖还存在依赖,咱们还须要在实例化依赖的依赖……为了解决这个问题容器就应运而生了,容器的定位就是「管理类的依赖和执行依赖注入的工具」。经过容器咱们能够将实例化这个过程给自动化,好比咱们能够直接用一行代码获取套餐对象:git

$container->get(Meal::class);
复制代码

0x01 简单容器的实现

下面这段代码是一个简单容器的实现:github

class Container {
    /** * @var Closure[] */
    protected $binds = [];

    /** * Bind class by closure. * * @param string $class * @param Closure $closure * @return $this */
    public function bind(string $class, Closure $closure) {
        $this->binds[$class] = $closure;

        return $this;
    }

    /** * Get object by class * * @param string $class * @param array $params * @return object */
    public function make(string $class, array $params = []) {
        if (isset($this->binds[$class])) {
            return ($this->binds[$class])->call($this, $this, ...$params);
        }

        return new $class(...$params);
    }
}
复制代码

这个容器只有两个方法 bindmakebind 方法将一个类名和一个闭包进行绑定,而后 make 方法将执行指定类名对应的闭包,并返回该闭包的返回值。咱们经过容器的使用示例加深理解:数组

$container = new Container();

$container->bind(Hamburger::class, function (Container $container) {
    $bread = $container->make(Bread::class);
    $bacon = $container->make(Bacon::class);

    return new Hamburger($bread, $bacon);
});

$container->bind(Meal::class, function (Container $container) {
    $hamburger = $container->make(Hamburger::class);
    $cola      = $container->make(Cola::class);
    return new Meal($hamburger, $cola);
});

// 输出 Meal
echo get_class($container->make(Meal::class));
复制代码

经过上面这个例子咱们能够知道 bind 方法传递的是一个「返回类名对应的实例化对象」的闭包,并且该闭包还接收该容器做为参数,因此咱们还能够在该闭包内使用容器获取依赖。上面这段代码虽然看起来彷佛比使用 new 关键字还复杂,但实际上对每个类,咱们只须要 bind 一次便可。之后每次须要该对象直接用 make 方法便可,在咱们的工程中确定会节省不少代码量。闭包

0x02 经过反射强化容器

「反射」官方手册 php.net/manual/zh/b…架构

在上面的的简单容器的例子里,咱们还须要经过 bind 方法写好实例化的「脚本」,那咱们试想有没有一种方法可以直接生成咱们须要的实例呢?其实经过「反射」并在构造函数指定参数的「类型提示类」咱们就能实现自动解决依赖的功能。由于经过反射咱们能够获取指定类构造函数所须要的参数和参数类型,因此咱们的容器能够自动解决这些依赖。示例代码以下:函数

/** * Get object by class * * @param string $class * @param array $params * @return object */
public function make(string $class, array $params = []) {
    if (isset($this->binds[$class])) {
        return ($this->binds[$class])->call($this, $this, ...$params);
    }

    return $this->resolve($class);
}

/** * Get object by reflection * * @param $abstract * @return object * @throws ReflectionException */
protected function resolve($abstract) {
    // 获取反射对象
    $constructor = (new ReflectionClass($abstract))->getConstructor();
    // 构造函数未定义,直接实例化对象
    if (is_null($constructor)) {
        return new $abstract;
    }
    // 获取构造函数参数
    $parameters = $constructor->getParameters();
    $arguments  = [];
    foreach ($parameters as $parameter) {
        // 得到参数的类型提示类
        $paramClassName = $parameter->getClass()->name;
        // 参数没有类型提示类,抛出异常
        if (is_null($paramClassName)) {
            throw new Exception('Fail to get instance by reflection');
        }
        // 实例化参数
        $arguments[] = $this->make($paramClassName);
    }

    return new $abstract(...$arguments);
}
复制代码

以上代码基于只是修改了原容器类的 make 方法,binds 数组中没有找到指定类绑定的闭包后执行 resolve 方法。其中 resolve 方法只是简单的经过反射获取指定类的构造函数并将其依赖实例化,最后实例化指定类。到了这一步之后咱们实例化套餐类就真的只须要一行代码了,连配置都不用:-D。工具

$container->make(Meal::class);
复制代码

固然如今这个容器仍是至关简陋的,由于若是指定类依赖标量值(好比:字符串,数组,数值等非对象类型)会直接抛出异常,也没法指定部分依赖而且若是依赖的是接口的话还会出错/(ㄒoㄒ)/~~,但这些功能都在一些成熟的容器库都有。若是感兴趣能够去看它们的源代码,这里我推荐看 Pipmle 这个项目。

0x03 总结

本文主要介绍了容器的应用场景并实现了一个简单的容器,经过使用容器咱们可以很方便的解决依赖注入带来的问题。可是容器也并非没有缺点,由于大部分容器都应用了反射技术,这会带来较大的性能消耗并且经过容器间接生成的实例 IDE 每每不能识别它的类型,因此就不会有自动提示(能够经过写文档注释解决)。不过我的感受引入容器其实仍是利大于弊滴(纯属我的感受)!

PHP 依赖注入容器实现 - 原文地址

相关文章
相关标签/搜索