Laravel 服务容器实现原理

前言

通过实现laravel 框架功能,以便深入理解laravel框架的先进思想。

什么是服务容器

服务容器是用来管理类依赖与运行依赖注入的工具。Laravel框架中就是使用服务容器来实现 控制反转 依赖注入

什么是控制反转(IoC)和依赖注入(DI)

控制反转(IoC) 就是说把创建对象的 控制权 进行转移,以前创建对象的主动权和创建时机是由自己把控的,而现在这种权力转移到第三方,也就是 Laravel 中的容器。

依赖注入(DI)则是帮助容器实现在运行中动态的为对象提供提依赖的资源。

概念容易不太容易让人理解,举个栗子:

//我们构建一个人的类和一个狗的类
 class People
{
    public $dog = null;

    public function __construct()
    {
        $this->dog = new Dog();
    }

    public function putDog(){
        return $this->dog->dogCall();
    }

}

class Dog{
    public function dogCall(){
        return '汪汪汪';
    }
}

这个人在遛狗,突然遇到了死对头,他于是放狗咬人

$people = new People();
$people->putDog();

在这个操作中,people类要执行putDog()这个方法,需要依赖Dog类,一般我们像上面一样,在people中利用构造函数来添加这个Dog依赖。如果使用控制反转 依赖注入则是这个样子

class People
{
    public $dog = null;

    public function __construct(Dog $dog)
    {
        $this->dog = $dog;
    }

    public function putDog(){
        return $this->dog->dogCall();
    }

}

People类通过构造参数声明自己需要的 依赖类,由容器自动注入。这样就实现了程序的有效解耦,好处在这就不多说了。

Laravel容器依赖注入的实现

实现原理需要了解的知识点:

闭包(匿名函数):
匿名函数(Anonymous functions),也叫闭包函数(closures),允许 临时创建一个没有指定名称的函数

反射:PHP 5 以上版本具有完整的反射 API,添加了对类、接口、函数、方法和扩展进行反向工程的能力。 此外,反射 API 提供了方法来取出函数、类和方法中的文档注释

理解了闭包和反射的基本用法我们来看Laravel中是怎么实现容器的,下面代码是我对laravel框架容器部分代码的简化核心版:
class Container
{
    /**
     *  容器绑定,用来装提供的实例或者 提供实例的回调函数
     * @var array
     */
    public $building = [];

    /**
     * 注册一个绑定到容器
     */
    public function bind($abstract, $concrete = null, $shared = false)
    {
        if(is_null($concrete)){
            $concrete = $abstract;
        }

        if(!$concrete instanceof Closure){
            $concrete = $this->getClosure($abstract, $concrete);
        }

        $this->building[$abstract] =  compact("concrete", "shared");
    }

    //注册一个共享的绑定 单例
    public function singleton($abstract, $concrete, $shared = true){
        $this->bind($abstract, $concrete, $shared);
    }

    /**
     * 默认生成实例的回调闭包
     *
     * @param $abstract
     * @param $concrete
     * @return Closure
     */
    public function getClosure($abstract, $concrete)
    {
        return function($c) use($abstract, $concrete){
            $method = ($abstract == $concrete)? 'build' : 'make';

            return $c->$method($concrete);
        };
    }

    /**
     * 生成实例 
     */
    public function make($abstract)
    {
        $concrete = $this->getConcrete($abstract);

        if($this->isBuildable($concrete, $abstract)){
            $object = $this->build($concrete);
        }else{
            $object = $this->make($concrete);
        }

        return $object;
    }

    /**
     * 获取绑定的回调函数
     */
    public function getConcrete($abstract)
    {
        if(! isset($this->building[$abstract])){
            return $abstract;
        }

        return $this->building[$abstract]['concrete'];
    }

    /**
     * 判断 是否 可以创建服务实体
     */
    public function isBuildable($concrete, $abstract)
    {
        return $concrete === $abstract || $concrete instanceof Closure;
    }

    /**
     * 根据实例具体名称实例具体对象
     */
    public function build($concrete)
    {
        if($concrete instanceof Closure){
            return $concrete($this);
        }

        //创建反射对象
        $reflector = new ReflectionClass($concrete);

        if( ! $reflector->isInstantiable()){
            //抛出异常
            throw new \Exception('无法实例化');
        }

        $constructor = $reflector->getConstructor();
        if(is_null($constructor)){
            return new $concrete;
        }

        $dependencies = $constructor->getParameters();
        $instance = $this->getDependencies($dependencies);

        return $reflector->newInstanceArgs($instance);

    }

    //通过反射解决参数依赖
    public function getDependencies(array $dependencies)
    {
        $results = [];
        foreach( $dependencies as $dependency ){
            $results[] = is_null($dependency->getClass())
                ?$this->resolvedNonClass($dependency)
                :$this->resolvedClass($dependency);
        }

        return $results;
    }

    //解决一个没有类型提示依赖
    public function resolvedNonClass(ReflectionParameter $parameter)
    {
        if($parameter->isDefaultValueAvailable()){
            return $parameter->getDefaultValue();
        }
        throw new \Exception('出错');

    }

    //通过容器解决依赖
    public function resolvedClass(ReflectionParameter $parameter)
    {
        return $this->make($parameter->getClass()->name);

    }

}

容器的工作流程

接着上面遛狗的例子:

//实例化容器类
$app =  new Container();
//向容器中填充Dog
$app->bind('Dog','App\Dog');
//填充People
$app->bind('People', 'App\People');
//通过容器实现依赖注入,完成类的实例化;
$people = $app->make('People');
//调用方法
echo $people->putDog();

上面示例中我们先实例化容器类,然后使用bind()方法 绑定接口和 生成相应的实例的闭包函数。然后使用make() 函数生成实例对象,在make()中会调用 isBuildable($concrete, $abstract) 来判断 给定的服务实体($concrete参数)是否可以创建,可以创建 就会调用 build($concrete) 函数 ,build($concrete) 函数会判断传的参数是 是 闭包 还是 具体类名 ,如果是闭包则直接运行,如果是具体类名的话,则通过反射获取该类的构造函数所需的依赖,完成实例化。

重点理解 下面这几个函数中 反射的用法,应该就很好理解了

build($concrete)
getDependencies(array $dependencies)
resolvedNonClass(ReflectionParameter $parameter)
resolvedClass(ReflectionParameter $parameter) 

最后

IoC 理解起来是有点难度,可能文中描述让你感觉不是很清楚,可以将文中代码 在php中用debug观察 运行状态。
理解了容器的具体实现原理,再去看Laravel中的相关实现,就会感觉豁然开朗。

本作品采用《CC 协议》,转载必须注明作者和本文链接
Dr點燃
本帖由 Summer 于 6年前 加精
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 26
幽弥狂

放狗咬人。。。666666

6年前 评论

@xhh110 哈哈,这样比较生动形象

6年前 评论

lass Container少了个c

6年前 评论

@Dr點燃 PHP 都涉及 脑电波编程了?

6年前 评论

@CraryPrimitiveMan 非常感谢提醒,已修改

6年前 评论

@依剑听雨 你把我说晕了O__O "…

6年前 评论

@Dr點燃 通过脑电波控制狗去咬人~~

6年前 评论

@依剑听雨 哈哈,原来如此

6年前 评论

降龙十八赞~

6年前 评论
幽弥狂

file

6年前 评论

@xhh110 O(∩_∩)O谢谢提醒 ?

6年前 评论

自己实现的一套di系统,支持psr11, https://github.com/slince/di
ps: laravel container到现在没有响应PSR规范, 这点我觉得很奇怪

6年前 评论
monlone
if ($concrete instanceof Closure) {
            $concrete = $this->getClosure($abstract, $concrete);
        }

这个多一了个!号。。

6年前 评论

@monlone 因为这是在判断传入的 $concrete 是否是一个闭包,不是的话,就会使用 $this->getClosure($abstract, $concrete);来生成闭包。所以这里是需要!

6年前 评论
monlone

@Dr點燃 我有用你的代码跑过哦,你可以试下。

6年前 评论

@monlone 出现了什么问题呢?咱们可以探讨下

6年前 评论

应该还有一个问题

class People
{
    public $dog = null;

    protected $age;

    public function __construct(Dog $dog, $age)
    {
        $this->dog = $dog;
       $this->age = $age;
    }

    public function putDog(){
        return $this->dog->dogCall();
    }

}

$age这个变量你要怎么注入呢
$container->make('people');

6年前 评论

@Tao 非常感谢提出宝贵意见,因为写的这个是个简化版,所以很多东西其实都没考虑,哈哈;有兴趣可以在Laravel源码中看看\Illuminate\Foundation\helpers.php这部分

function app($abstract = null, array $parameters = [])
    {
        if (is_null($abstract)) {
            return Container::getInstance();
        }

        return empty($parameters)
            ? Container::getInstance()->make($abstract)
            : Container::getInstance()->makeWith($abstract, $parameters);
    }

可以看到里面有个makeWith($abstract, $parameters),这样当使用app($abstract = null, array $parameters = [])时候传入需要的参数。

6年前 评论

第一个instanceof中的o大写了

6年前 评论

@longlywork 感谢提醒 ^_^,已修改

6年前 评论

文章写的不错,我补充一点,阅读容器相关的代码,逻辑有点绕,还是挺吃力的。于是结合xdebug,可以看下代码的execute trace:

xdebug_start_trace();
$app->bind('Dog','Dog');
xdebug_stop_trace();

trace日志如下:

TRACE START [2017-06-23 02:03:36]
    0.0068     496096       +0     -> Illuminate\Container\Container->bind($abstract = 'Dog', $concrete = 'Dog', $shared = ???) /data/www/my_composer/vendor/shengbin/containerTest.php:32
    0.0069     496096       +0       -> Illuminate\Container\Container->dropStaleInstances($abstract = 'Dog') /data/www/my_composer/vendor/illuminate/container/Container.php:211
    0.0069     496208     +112       -> is_null('Dog') /data/www/my_composer/vendor/illuminate/container/Container.php:213
    0.0069     496208                >=> FALSE
    0.0069     496208       +0       -> Illuminate\Container\Container->getClosure($abstract = 'Dog', $concrete = 'Dog') /data/www/my_composer/vendor/illuminate/container/Container.php:221
    0.0069     496904                >=> class Closure { public $static = array ('abstract' => 'Dog', 'concrete' => 'Dog'); public $this = class Illuminate\Container\Container { protected $resolved = array (...); protected $bindings = array (...); protected $methodBindings = array (...); protected $instances = array (...); protected $aliases = array (...); protected $abstractAliases = array (...); protected $extenders = array (...); protected $tags = array (...); protected $buildStack = array (...); protected $with = array (...); public $contextual = array (...); protected $reboundCallbacks = array (...); protected $globalResolvingCallbacks = array (...); protected $globalAfterResolvingCallbacks = array (...); protected $resolvingCallbacks = array (...); protected $afterResolvingCallbacks = array (...) }; public $parameter = array ('$container' => '<required>', '$parameters' => '<optional>') }
    0.0069     497600    +1392       -> compact('concrete', 'shared') /data/www/my_composer/vendor/illuminate/container/Container.php:224
    0.0069     497976                >=> array ('concrete' => class Closure { public $static = array (...); public $this = class Illuminate\Container\Container { ... }; public $parameter = array (...) }, 'shared' => FALSE)
    0.0070     498352     +752       -> Illuminate\Container\Container->resolved($abstract = 'Dog') /data/www/my_composer/vendor/illuminate/container/Container.php:229
    0.0070     498352       +0         -> Illuminate\Container\Container->isAlias($name = 'Dog') /data/www/my_composer/vendor/illuminate/container/Container.php:166
    0.0070     498352                  >=> FALSE
    0.0070     498352                >=> FALSE
    0.0070     498352       +0     -> xdebug_stop_trace() /data/www/my_composer/vendor/shengbin/containerTest.php:33
    0.0070     498416
TRACE END   [2017-06-23 02:03:36]

注:我测试使用的laravel版本和楼主的可能不一样。
关于xdebug的用法,可以参考https://www.zhihu.com/question/20348619/answer/101893104

6年前 评论

读了container.php的代码:

1、bind('Dog') 做的工作就是在container对象中添加属性:

$this->bindings['Dog']['concrete'] = Closure; // Closure中包含了创建Dog对象的代码,但是这里不会执行闭包,其实就是一个空壳子。

2、make('Dog') 做的工作就是执行第一步bindings['Dog']中的闭包,创建Dog对象,并把resloved属性标记为true

$this->resolved['Dog'] = true;

6年前 评论

看君一席话,胜读四年文档 :joy:

2年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!