Laravel 容器源码研究

<?php
//error_reporting(0);

include "vendor/autoload.php";

class A
{
    public $b = null;

    public $p = 0;

    public function __construct($b, $p)
    {
        $this->b = $b;
        $this->p = $p;
    }

    public function test()
    {
        echo "A-------\n";
        var_dump($this->p);
        $this->b->test();
    }
}

class B
{
    public $c = null;

    public $o = 0;

    public function __construct(C $c, $o = 0)
    {
        $this->c = $c;
        $this->o = $o;
    }

    public function test()
    {
        echo "B-------\n";
        if ($this->o) {
            echo 'Great 0-----';
        }
        $this->c->test();
    }
}

class C
{
    public function test()
    {
        echo "C-------\n";
    }
}

$container = new \Illuminate\Container\Container();
$p = 10101000;

// A类构造函数依赖形参$p(没有默认值),需要传递: 通过when()方法指定
$b = $container->make(B::class, ['o' => 1]);
$container->when(A::class)->needs('$p')->give($p);
$container->when(A::class)->needs('$b')->give($b);
//$container->when(A::class)->needs(B::class)->give(function () use ($b) {
//    return $b;
//});
$a = $container->make(A::class);

// A类构造函数依赖形参$p(没有默认值),需要传递:通过make()方法的第二参数传递
//$a = $container->make(A::class, ['p' => $p]);
$a->test();

dd((array)$container);

一、对象构建:

    public function make($abstract, array $parameters = [])
    {
        return $this->resolve($abstract, $parameters);
    }

方法make只是resolve()方法的包装而已(注意:make方法是公共的,而resolve是受保护的):

    protected function resolve($abstract, $parameters = [])
    {
	// 如果是别名,取回原来的名称:比如aa是a的别名,传递aa进来,返回a
	// 传递a进来,直接返回a
        $abstract = $this->getAlias($abstract);

        $needsContextualBuild = ! empty($parameters) || ! is_null(
            $this->getContextualConcrete($abstract)
        );

        // 在instances中,并且没有上下文对象,直接返回
        if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
            return $this->instances[$abstract];
        }

	// 把传递进来的参数进行临时保存(入栈)
        $this->with[] = $parameters;

        $concrete = $this->getConcrete($abstract);

        // 递归构建对象
        if ($this->isBuildable($concrete, $abstract)) {
            $object = $this->build($concrete);
        } else {
            $object = $this->make($concrete);
        }

        // 应用对象装饰
        foreach ($this->getExtenders($abstract) as $extender) {
            $object = $extender($object, $this);
        }

        // 如果是share类型,并且没有上下文依赖,就存入instances:并不是设置为share就一定进入instances
        if ($this->isShared($abstract) && ! $needsContextualBuild) {
            $this->instances[$abstract] = $object;
        }

	// 回调
        $this->fireResolvingCallbacks($abstract, $object);

        // 标记已经取回
        $this->resolved[$abstract] = true;

	// 出栈
        array_pop($this->with);

        return $object;
    }

类目传递了A,这时$abstract是A, $parameters是[‘p’ => $p],$abstract经过getAlias后没有发送变化(不是别名),由于参数不是空,$needsContextualBuild为true; $this->getConcrete($abstract)后得到的$concrete是$abstract本身(都是A),然后判断是否可以构建:

    protected function isBuildable($concrete, $abstract)
    {
        return $concrete === $abstract || $concrete instanceof Closure;
    }

是否可以构建,条件是$concrete和$abstract相等,或者$concrete是一个闭包函数。所以这里会进入build()方法:

    public function build($concrete)
    {
        // 如果是闭包,调用闭包函数:第一参数是容器,第二参数是外部传递进来的参数
        if ($concrete instanceof Closure) {
            return $concrete($this, $this->getLastParameterOverride());
        }
	// 不是闭包就利用反射:$concrete这里应该是类名(或类对象)
        $reflector = new ReflectionClass($concrete);

        // 如果不可实例化,抛异常
        if (! $reflector->isInstantiable()) {
            return $this->notInstantiable($concrete);
        }

	// 由于可能会依赖其它对象,这里先把当前类目入栈
        $this->buildStack[] = $concrete;
	// 取回构造函数
        $constructor = $reflector->getConstructor();

        // 构造函数为空,直接实例化
        if (is_null($constructor)) {
            array_pop($this->buildStack);

            return new $concrete;
        }
	// 否则就是指定了构造函数,那么解析这个构造函数,取回参数
        $dependencies = $constructor->getParameters();

        // 根据构造函数的参数,实例化依赖,这里可能会产生递归
	// 另外,如果外部有传递参数进来,会优先使用
        $instances = $this->resolveDependencies(
            $dependencies
        );
	// 出栈
        array_pop($this->buildStack);

	// 把实例对象传递构造函数生成对象
        return $reflector->newInstanceArgs($instances);
    }

这个构建的过程,利用反射,如果依赖其它对象,则递归构建依赖。 这里面用到了两个栈,with是参数堆栈,每次构建从其中取出参数,填充到构造函数,另一个是buildStack。这里可以看到A传递进来,它不是闭包,那么就开始了下面的反射构建过程,A是可以实例化的,有构造函数,那么会进入resolveDependencies()递归解决依赖:

    protected function resolveDependencies(array $dependencies)
    {
        $results = [];

        foreach ($dependencies as $dependency) {
            if ($this->hasParameterOverride($dependency)) {
                $results[] = $this->getParameterOverride($dependency);

                continue;
            }

            $results[] = is_null($dependency->getClass())
                            ? $this->resolvePrimitive($dependency)
                            : $this->resolveClass($dependency);
        }

        return $results;
    }

这里看明白如下几个函数:

    protected function hasParameterOverride($dependency)
    {
        return array_key_exists(
            $dependency->name, $this->getLastParameterOverride()
        );
    }

    protected function getParameterOverride($dependency)
    {
        return $this->getLastParameterOverride()[$dependency->name];
    }

    protected function getLastParameterOverride()
    {
        return count($this->with) ? end($this->with) : [];
    }

方法getLastParameterOverride取回with堆栈的头元素,就是传递进来的参数,方法hasParameterOverride就是判断传递进来的参数有没有对应名称的变量,方法getParameterOverride就是取到传递进来的值。在这,传递进来的参数是[‘p’ => $p], 那么在构建A时,取到形参p,p出现在了[‘p’ => $p]中,所以A构造函数的$p就等于传递进来的变量$p。如果没有对应的参数,就检查是否有指定上下文参数,有则使用,没有,如果是类就去实例类(可能递归),如果是原始变量,就使用默认值。

由于A构造函数需要B对象,而B对象又需要C对象,所以会进入递归,最终返回的对象传递到A构造函数。从这个过程可见,容器的make方法,可以自动构建对象。如果没有构造函数,或构造函数没有依赖其它对象,直接new即可。到这里,还有问题问解决,当构建A时,会去构建B,如果希望使用一个现成的B对象,这个需要依靠上下文绑定来解决。

二、上下文绑定:

//当解析A::class,如果需要B::class类型对象,那么对应的对象是$b。
$container->when(A::class)->needs(B::class)->give(function () use ($b) {
    return $b;
});
//当解析A::class,如果需要形参是$b时,那么对应的值是$b。
$container->when(A::class)->needs('$b')->give($b);

注意语法,当需要的是类类型时,需要给一个闭包函数,否则直接指定值。另外,如果构造函数依赖的是B $b,这个时候直接给值的语法可能是无作用的,因为如果有类型限定,并且类型是类类型,那么总是根据类来实例化对象,这个过程会取回闭包执行返回对象。

这个用法实际产生如下数组:

$this->contextual['A']['B'] = function () use ($b) {
    return $b;
};
$this->contextual['A']['$b'] = $b;

当A实例化是,遇到了需要实例化B,那么首先判断是否存在$this->contextual[‘A’][‘B’],如果存在直接用,否则就去构造。当遇到形参$b时,就去判断$this->contextual[‘A’][‘$b’]是否存在,存在直接用,否则利用反射API去使用默认值。

容器的when()方法不过是一个语法糖,最终会调用addContextualBinding()方法来设置上下绑定:

    public function addContextualBinding($concrete, $abstract, $implementation)
    {
        $this->contextual[$concrete][$this->getAlias($abstract)] = $implementation;
    }

可以这样用:

$container->addContextualBinding(A::class, B::class, function () use ($b) {
    return $b;
});
$container->addContextualBinding(A::class, '$b', $b);

这个上下文绑定,跟make()时传递的第二参数作用非常类似,不过make时传递的参数需要和构造函数的形参名称相同,并且总是优先使用的,比如:

$container->addContextualBinding(A::class, '$b', $b);

$container->make(A::class, ['$b' => $b]);

这里的设置了上下文,也传递了第二参数,那么make时传递的总是优先被使用。一旦设置了上下文,或make时传递了参数,那么生成的对象都是独立(不共享,不会压入$this->instances数组),哪怕设置的绑定是共享的,因为跟上下文相关的对象,是特定的。

上下文设置或构建时的参数传递严格意义上说并不是容器需要实现的内容。这里显然是为了能便利的构建依赖而引入的便利方法。

三、对象构建时的回调:
对象构建之后,可以设置回调,可以全局设置回调(每构建一个对象都调用),或者设置在构建某个类型的对象时才执行回调:

$container = new \Illuminate\Container\Container();
$callBack = function () {
    echo "Callback -------\n";
};

$globalAfterCallBack = function () {
    echo "Global After Callback -------\n";
};

$afterCallBack = function () {
    echo "After Callback -------\n";
};
$container->resolving($globalCallBack);
$container->resolving('a', $callBack);
$container->afterResolving($globalAfterCallBack);
$container->afterResolving('a', $afterCallBack);

四、对象装饰器:
对象构建后,需要执行对象的某个方法,或者运行某个逻辑来扩展一下这个对象,这个就是对象的装饰。在对象生成后,可以使用extend来装饰:

$container->extend(Service::class, function($service) {
    return new DecoratedService($service);
});

五、绑定
在理解了以上基本内容后,再来看看绑定,应该就很好理解了。

// 直接绑定一个类,可以这样做,除了类实例化会延后外,没有额外好处,一般应该避免这个用法
// 在需要是直接make即可
$container->bind(A::class);

// 设置服务,当make(‘a’)时,会实时生成一个A对象
$container->bind(‘a’, A::class);

// 绑定接口,AI实现了I的接口
$container->bind(‘I’, AI::class);

// 设置服务,指定闭包函数,多见
$container->bind(‘events’, function () {});

主要到以上用法尽管都是合法的,但是绑定闭包这种用法比较多见。另外,bing的第三参数控制这个bind是否是共享的,也就是生成实例是否是单例的,bind方法默认是不共享的,有一个快捷方法singleton()可以直接设置共享绑定。

如果绑定是共享的,生成的实例会压入instances数组,表示这些是共享对象,所以有一个方法,可以往这个数组中直接压入内容进行全局共享:instance($key, $instance)。

接下来看看具体过程:

    public function bind($abstract, $concrete = null, $shared = false)
    {
        $this->dropStaleInstances($abstract);

        if (is_null($concrete)) {
            $concrete = $abstract;
        }

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

        $this->bindings[$abstract] = compact('concrete', 'shared');

        if ($this->resolved($abstract)) {
            $this->rebound($abstract);
        }
    }

简单来说就是对一个类型设置绑定,第二参数是要绑定的内容,第三参数设置是否共享,它决定了对象是否是单例对象。首先调用的$this->dropStaleInstances($abstract),对应的方法实际就是清空该类型对应的绑定和对应的实例:

    protected function dropStaleInstances($abstract)
    {
        unset($this->instances[$abstract], $this->aliases[$abstract]);
    }

如果第二参数是null(没有传递需要绑定的内容),那么绑定的内容就是类型本身。如果绑定的内容如果不是一个闭包函数,那么就调用getClosure()把内容变成一个闭包函数:

    protected function getClosure($abstract, $concrete)
    {
        return function ($container, $parameters = []) use ($abstract, $concrete) {
            if ($abstract == $concrete) {
                return $container->build($concrete);
            }

            return $container->make($concrete, $parameters);
        };
    }

这个getClosure()返回的是一个闭包函数,返回的这个闭包函数接受两个参数($container, $parameters = []),如果类型和内容一样,就直接build,否则就是make一个对象,总之现在知道其返回的是function ($container, $parameters = [])即可。

接下来按照一个统一的格式设置绑定:

$this->bindings[$abstract] = compact('concrete', 'shared');

if ($this->resolved($abstract)) {
    $this->rebound($abstract);
}

// 举例来说
$this->bindings[‘a’] = [
    ‘concrete’ => function ($container, $parameters = []) {},
    ‘shared’ => false
];

那么在a实例被生成时,concrete对应的闭包函数会被执行,其中会根据shared来决定是否设置共享实例。resolved()方法用来判断绑定是否已经生成了实例,如果生成了实例,调用rebound()方法重新生成一次(实际是调用make方法,不过,在重写生成对象时,可以对特定类型设置回调,这个方法会执行回调)。

绑定的其它情况分析:
第二参数只要不是闭包函数,那么总会调用getClosure()方法封装为一个闭包函数:
1 当第二参数为空时,把第一参数赋值给第二参数,调用getClosure()方法

class A {}

$container->bind(A::class);
// $abstract = $concrete = A::class
$container->bind['A'] = function ($container, $parameters = []) use ($abstract, $concrete) {
     if ($abstract == $concrete) {
         return $container->build($concrete);
     }

     return $container->make($concrete, $parameters);
};
// 此时获取该服务跟直接new基本一样
// 不一样的是实例生成时,如果构造函数依赖容器对象,可以自动注入

2 当是一个对象时,调用getClosure()方法

class A {}

$container->bind('aa', new A());
// $abstract = 'aa'; $concrete = new A();
$container->bind['aa'] = function ($container, $parameters = []) use ($abstract, $concrete) {
     if ($abstract == $concrete) {
         return $container->build($concrete);
     }

     return $container->make($concrete, $parameters);
};
// 在实例化时会运行$container->make('aa', []), ‘aa’对应的闭包函数被运行,该函数内再次运行$container->make($concrete, $parameters);
// 此时的$concrete是一个对象,PHP的isset($this->aliases[$concrete])会报警告,但不会影响程序运行
// 第二次运行的make最终会到达build函数,这是的build不过是根据$concrete(此时是一个对象)并应用反射,重新生成一个对象而已

所以这两种用法都应该避免。相反,在使用bind时,应该总是使用闭包。

另外,第二参数是闭包时,这个闭包函数在定义参数时注意:

$container->bind('aa', function ($a, $b, $c) {});

如果调用make时,将会报错。因为闭包调用时总是如此格式:

return $concrete($this, $this->getLastParameterOverride());

闭包可以不定义参数,以上的用法不会发生错误。如果定义了参数,第一个参数总是代表容器实例本身,第二个参数是一个数组(可不定义)用来从外传入数据到闭包中。如果多于2个参数就发生错误。如果定义了第二参数,那么在make对象时,就可以传递数据进去(注:如果是单例实例,一旦生成后不会再改变,除非强制rebund,所以不能期望通过传递参数改变单例对象的状态)

最终提炼一下公共方法:

public function when($concrete)

// 服务是否已经设置
public function bound($abstract)

// 方法bound的别名
public function has($id)

// 服务是否已经实例化
public function resolved($abstract)

// 服务是否是共享的(单例)
public function isShared($abstract)

// 名称是否是别名
public function isAlias($name)

// 服务绑定
public function bind($abstract, $concrete = null, $shared = false)

public function hasMethodBinding($method)
public function bindMethod($method, $callback)
public function callMethodBinding($method, $instance)

// 上下文绑定
public function addContextualBinding($concrete, $abstract, $implementation)

// 条件绑定,bind方法的再封装
public function bindIf($abstract, $concrete = null, $shared = false)

// 单例绑定
public function singleton($abstract, $concrete = null)

// 服务器装饰器
public function extend($abstract, Closure $closure)

// 直接注入内容到容器
public function instance($abstract, $instance)

public function tag($abstracts, $tags)
public function tagged($tag)

// 服务别名设置
public function alias($abstract, $alias)

// 绑定回调设置
public function rebinding($abstract, Closure $callback)

public function refresh($abstract, $target, $method)
public function wrap(Closure $callback, array $parameters = [])
public function call($callback, array $parameters = [], $defaultMethod = null)

// 用一个闭包封装make
public function factory($abstract)

// 方法make的另一种形式
public function makeWith($abstract, array $parameters = [])

// 构建服务
public function make($abstract, array $parameters = [])
// 
public function get($id)

// 构建服务
public function build($concrete)

public function resolving($abstract, Closure $callback = null)
public function afterResolving($abstract, Closure $callback = null)

public function getBindings()
public function getAlias($abstract)
public function forgetExtenders($abstract)
public function forgetInstance($abstract)
public function forgetInstances()
public function flush()

实际常用的就是几个方法。