Laravel5.3之Container源碼解析

說明:本文主要學習Laravel中Container的源碼,主要學習Container的綁定和解析過程,和解析過程當中的依賴解決。分享本身的研究心得,但願對別人有所幫助。實際上Container的綁定主要有三種方式:bind(),singleton(),instance(),且singleton()只是一種'shared' = true的bind(),這些已經在Laravel5.3之IoC Container實例化源碼解析聊過,其實現方法並不複雜。當Service經過Service Provider綁定到Container中後,當須要該Service時,是須要Container幫助自動解析make()。OK,下面聊聊自動解析過程,研究下Container是如何在自動解析Service時解決該Service的依賴問題的。php

開發環境: Laravel5.3 + PHP7 + OS X 10.11api

PHPUnit測試下綁定

在聊解析過程前,先測試下\Illuminate\Container\Container中綁定的源碼,這裏測試下bind(),singleton(),instance()三個綁定方式:閉包

<?php

namespace MyRightCapital\Container\Tests;

use MyRightCapital\Container\Container;

class ContainerBindTest extends \PHPUnit_Framework_TestCase
{
    /** * @var Container $container */
    protected $container;

    public function setUp()
    {
        $this->container = new Container();
    }

    public function testBindClosure()
    {
        // Arrange
        $expected = 'Laravel is a PHP Framework.';
        $this->container->bind('PHP', function () use ($expected) {
            return $expected;
        });

        // Actual
        $actual = $this->container->make('PHP');

        // Assert
        $this->assertEquals($expected, $actual);
    }

    public function testBindInterfaceToImplement()
    {
        // Arrange
        $this->container->bind(IContainerStub::class, ContainerImplementationStub::class);

        // Actual
        $actual = $this->container->make(IContainerStub::class);

        // Assert
        $this->assertInstanceOf(IContainerStub::class, $actual);
    }

    public function testBindDependencyResolution()
    {
        // Arrange
        $this->container->bind(IContainerStub::class, ContainerImplementationStub::class);

        // Actual
        $actual = $this->container->make(ContainerNestedDependentStub::class);

        // Assert
        $this->assertInstanceOf(ContainerDependentStub::class, $actual->containerDependentStub);
        $this->assertInstanceOf(ContainerImplementationStub::class, $actual->containerDependentStub->containerStub);
    }

    public function testSingleton()
    {
        // Arrange
        $this->container->singleton(ContainerConcreteStub::class);
        $expected = $this->container->make(ContainerConcreteStub::class);

        // Actual
        $actual = $this->container->make(ContainerConcreteStub::class);

        // Assert
        $this->assertSame($expected, $actual);
    }

    public function testInstanceExistingObject()
    {
        // Arrange
        $expected = new ContainerImplementationStub();
        $this->container->instance(IContainerStub::class, $expected);

        // Actual
        $actual = $this->container->make(IContainerStub::class);

        // Assert
        $this->assertSame($expected, $actual);
    }
}

class ContainerConcreteStub
{

}

interface IContainerStub
{

}

class ContainerImplementationStub implements IContainerStub
{

}

class ContainerDependentStub
{
    /** * @var \MyRightCapital\Container\Tests\IContainerStub */
    public $containerStub;

    public function __construct(IContainerStub $containerStub)
    {
        $this->containerStub = $containerStub;
    }
}

class ContainerNestedDependentStub
{
    /** * @var \MyRightCapital\Container\Tests\ContainerDependentStub */
    public $containerDependentStub;

    public function __construct(ContainerDependentStub $containerDependentStub)
    {
        $this->containerDependentStub = $containerDependentStub;
    }
}

這裏測試了bind()綁定閉包,綁定接口和對應實現,依賴解析這三個feature,singleton()測試了是否爲單例綁定一個feature,instance()測試了已存在對象綁定這個feature,測試結果5個tests都經過:
編輯器

關於在PHPStorm中配置PHPUnit可參考這篇:Laravel5.2之基於PHPStorm編輯器的Laravel開發ide

make()源碼解析

從以上testcase知道,make()是負責從Container中解析出service的,並且在testBindDependencyResolution()這個test中,還能發現當ContainerNestedDependentStub::class有構造依賴時,Container也會自動去解析這個依賴並注入ContainerNestedDependentStub::class的構造函數中,這個依賴是ContainerDependentStub::class,而這個依賴又有本身的依賴IContainerStub::class,從斷言語句$this->assertInstanceOf(ContainerImplementationStub::class, $actual->containerDependentStub->containerStub);知道,Container又自動解析了這個依賴,全部這一切都不須要咱們手動去解析,全都是Container自動化解析的。函數

這一切Container是怎麼作到的?實際上並不複雜,解決依賴只是用了PHP的Reflector反射機制來實現的。先看下make()源碼:學習

/** * Resolve the given type from the container. * * @param string $abstract * @param array $parameters * @return mixed */
    public function make($abstract, array $parameters = [])
    {
        $abstract = $this->getAlias($this->normalize($abstract));

        // 若是是instance()綁定的方式,就直接解析返回綁定的service
        if (isset($this->instances[$abstract])) {
            return $this->instances[$abstract];
        }

        // 獲取$abstract對應綁定的$concrete
        $concrete = $this->getConcrete($abstract);

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

        foreach ($this->getExtenders($abstract) as $extender) {
            $object = $extender($object, $this);
        }

        if ($this->isShared($abstract)) {
            $this->instances[$abstract] = $object;
        }

        $this->fireResolvingCallbacks($abstract, $object);

        $this->resolved[$abstract] = true;

        return $object;
    }
    
    protected function getConcrete($abstract)
    {
        if (! is_null($concrete = $this->getContextualConcrete($abstract))) {
            return $concrete;
        }

        // 若是是$this->container->singleton(ContainerConcreteStub::class);這種方式綁定,即$concrete = null
        // 則 $abstract = $concrete,可看以上PHPUnit的testSingleton()這個test
        // 這種方式稱爲'自動補全'綁定
        if (! isset($this->bindings[$abstract])) {
            return $abstract;
        }

        return $this->bindings[$abstract]['concrete'];
    }
    
    protected function isBuildable($concrete, $abstract)
    {
        return $concrete === $abstract || $concrete instanceof Closure;
    }

從以上源碼可知道若是綁定的是閉包或者'自動補全'綁定($concrete = null),則須要build()這個閉包或類名,轉換成對應的實例。若是是'接口實現'這種方式綁定,則須要再一次調用make()並通過getConcrete後$abstract = $concrete,而後符合isBuildable()的條件,進入build()函數內。因此以上的PHPUnit的測試用例中無論什麼方式的綁定,都要進入build()函數內編譯出相應對象實例。當編譯出對象後,檢查是不是共享的,以及是否要觸發回調,以及標記該對象已經被解析。OK,看下build()的源碼:測試

/** * Instantiate a concrete instance of the given type. * * @param string $concrete * @param array $parameters * @return mixed * * @throws \Illuminate\Contracts\Container\BindingResolutionException */
    public function build($concrete, array $parameters = [])
    {
        // 若是是閉包直接執行閉包並返回,e.g. PHPUnit的這個test:testBindClosure()
        if ($concrete instanceof Closure) {
            return $concrete($this, $parameters);
        }
        
        // 如這個test:testBindInterfaceToImplement(),這裏的$concrete = ContainerImplementationStub::class類名稱,
        // 則使用反射ReflectionClass來探測ContainerImplementationStub這個類的構造函數和構造函數的依賴
        $reflector = new ReflectionClass($concrete);

        // 若是ContainerImplementationStub不能實例化,這應該是接口或抽象類,再或者就是ContainerImplementationStub的構造函數是private的
        if (! $reflector->isInstantiable()) {
            if (! empty($this->buildStack)) {
                $previous = implode(', ', $this->buildStack);

                $message = "Target [$concrete] is not instantiable while building [$previous].";
            } else {
                $message = "Target [$concrete] is not instantiable.";
            }

            throw new BindingResolutionException($message);
        }

        $this->buildStack[] = $concrete;

        // 獲取構造函數的反射
        $constructor = $reflector->getConstructor();

        // 若是構造函數是空,說明沒有任何依賴,直接new返回
        if (is_null($constructor)) {
            array_pop($this->buildStack);

            return new $concrete;
        }
        
        // 獲取構造函數的依賴,返回ReflectionParameter[]
        $dependencies = $constructor->getParameters();

        $parameters = $this->keyParametersByArgument(
            $dependencies, $parameters
        );

        // 而後就是獲取相關依賴,如testBindDependencyResolution()這個test中,
        // ContainerNestedDependentStub::class是依賴於ContainerDependentStub::class的
        $instances = $this->getDependencies(
            $dependencies, $parameters
        );

        array_pop($this->buildStack);

        return $reflector->newInstanceArgs($instances);
    }

從源碼可知道,比較麻煩的是當ContainerNestedDependentStub::class的構造函數有依賴ContainerDependentStub::class時,經過getDependencies()來解決的,看下getDependencies()的源碼:ui

// 這裏$parameters = ReflectionParameter[]
    protected function getDependencies(array $parameters, array $primitives = [])
    {
        $dependencies = [];

        foreach ($parameters as $parameter) {
            $dependency = $parameter->getClass();

            // 若是某一依賴值已給,就賦值
            if (array_key_exists($parameter->name, $primitives)) {
                $dependencies[] = $primitives[$parameter->name];
            } 
            // 若是類名爲null,說明是基本類型,如'int','string' and so on.
            elseif (is_null($dependency)) {
                $dependencies[] = $this->resolveNonClass($parameter);
            } 
            // 若是是類名,如ContainerDependentStub::class,則resolveClass去解析成對象
            else {
                $dependencies[] = $this->resolveClass($parameter);
            }
        }

        return $dependencies;
    }

經過上面註釋,看下resolveClass()的源碼:this

protected function resolveClass(ReflectionParameter $parameter)
    {
        try {
            // $parameter->getClass()->name返回的是類名,如ContainerNestedDependentStub依賴於$containerDependentStub
            // $containerDependentStub的typehint是ContainerDependentStub,因此類名是'ContainerDependentStub'
            // 而後遞歸繼續make(ContainerDependentStub::class)
            // 又和PHPUnit中這個測試$this->container->make(ContainerNestedDependentStub::class)相相似了
            // ContainerNestedDependentStub又依賴於IContainerStub::class,
            // IContainerStub::class是綁定於ContainerImplementationStub::class
            // 直到ContainerImplementationStub沒有依賴或者是構造函數是基本屬性,
            // 最後build()結束
            return $this->make($parameter->getClass()->name);
        } catch (BindingResolutionException $e) {
            if ($parameter->isOptional()) {
                return $parameter->getDefaultValue();
            }

            throw $e;
        }
    }

從以上代碼註釋直到build()是個遞歸過程,A類依賴於B類,B類依賴於C類和D類,那就從A類開始build,發現依賴於B類,再從Container中解析make()即再build()出B類,發現依賴於C類,再make() and build(),發現B類又同時依賴於D類,再make() and build(),以此類推直到沒有依賴或依賴基本屬性,解析結束。這樣一步步解析完後,發現Container的解析make()並非很神祕很複雜中的過程。

從以上源碼發現PHP的反射Reflector是個很好用的技術,這裏給出個test,看下Reflector能幹些啥:

<?php

class ConstructorParameter
{

}

class ReflectorTest
{
    private $refletorProperty1;

    protected $refletorProperty2;

    public $refletorProperty3;

    /** * @var int */
    private $request;

    public function __construct(int $request = 10, string $response, ConstructorParameter $constructorParameter, Closure $closure)
    {

        $this->request = $request;
    }

    private function reflectorMethod1()
    {
    }

    protected function reflectorMethod2()
    {
    }

    public function reflectorMethod3()
    {
    }
}

$reflector_class        = new ReflectionClass(ReflectorTest::class);
$methods                = $reflector_class->getMethods();
$properties             = $reflector_class->getProperties();
$constructor            = $reflector_class->getConstructor();
$constructor_parameters = $constructor->getParameters();

foreach ($constructor_parameters as $constructor_parameter) {
    $dependency = $constructor_parameter->getClass();
    var_dump($dependency);

    if ($constructor_parameter->isDefaultValueAvailable()) {
        var_dump($constructor_parameter->getDefaultValue());
    }
}

var_dump($methods);
var_dump($properties);
var_dump($constructor);
var_dump($constructor_parameters);

打印結果太長了,就不粘貼了。能夠看下PHP官方文檔:Reflector

總結:本文學習了下Container的核心功能:service resolve的過程,並學習了service的依賴是如何被自動解析的。遇到好的心得再分享,到時見。

相關文章
相關標籤/搜索