Laravel核心——服務容器的細節特性

leoyang90發表於2019-02-16

前言

首先歡迎關注我的部落格: www.leoyang90.cn

在前面幾個部落格中,我詳細講了 Ioc 容器各個功能的使用、繫結的原始碼、解析的原始碼,今天這篇部落格會詳細介紹 Ioc 容器的一些細節,一些特性,以便更好地掌握容器的功能。

注:本文使用的測試類與測試物件都取自 laravel 的單元測試檔案src/illuminate/tests/Container/ContainerTest.php

rebind繫結特性

rebind 在繫結之前

instance 和 普通 bind 繫結一樣,當重新繫結的時候都會呼叫 rebind 回撥函式,但是有趣的是,對於普通 bind 繫結來說,rebind 回撥函式被呼叫的條件是當前介面被解析過:

public function testReboundListeners()
{
    unset($_SERVER[`__test.rebind`]);

    $container = new Container;
    $container->rebinding(`foo`, function () {
        $_SERVER[`__test.rebind`] = true;
    });
    $container->bind(`foo`, function () {
    });
    $container->make(`foo`);
    $container->bind(`foo`, function () {
    });

    $this->assertTrue($_SERVER[`__test.rebind`]);
}

所以遇到下面這樣的情況,rebinding 的回撥函式是不會呼叫的:

public function testReboundListeners()
{
    unset($_SERVER[`__test.rebind`]);

    $container = new Container;
    $container->rebinding(`foo`, function () {
        $_SERVER[`__test.rebind`] = true;
    });
    $container->bind(`foo`, function () {
    });
    $container->bind(`foo`, function () {
    });

    $this->assertFalse(isset($_SERVER[`__test.rebind`]));
}

有趣的是對於 instance 繫結:

public function testReboundListeners()
{
    unset($_SERVER[`__test.rebind`]);

    $container = new Container;
    $container->rebinding(`foo`, function () {
        $_SERVER[`__test.rebind`] = true;
    });
    $container->bind(`foo`, function () {
    });
    $container->instance(`foo`, function () {
    });

    $this->assertTrue(isset($_SERVER[`__test.rebind`]));
}

rebinding 回撥函式卻是可以被呼叫的。其實原因就是 instance 原始碼中 rebinding 回撥函式呼叫的條件是 rebound 為真,而普通 bind 函式呼叫 rebinding 回撥函式的條件是 resolved 為真. 目前筆者不是很清楚為什麼要對 instance 和 bind 區別對待,希望有大牛指導。

rebind 在繫結之後

為了使得 rebind 回撥函式在下一次的繫結中被啟用,在 rebind 函式的原始碼中,如果判斷當前物件已經繫結過,那麼將會立即解析:

public function rebinding($abstract, Closure $callback)
{
    $this->reboundCallbacks[$abstract = $this->getAlias($abstract)][] = $callback;
    
    if ($this->bound($abstract)) {
        return $this->make($abstract);
    }
}

單元測試程式碼:

public function testReboundListeners1()
{
    unset($_SERVER[`__test.rebind`]);

    $container = new Container;
    $container->bind(`foo`, function () {
        return `foo`;
    });

    $container->resolving(`foo`, function () {
        $_SERVER[`__test.rebind`] = true;
    });

    $container->rebinding(`foo`, function ($container,$object) {//會立即解析
        $container[`foobar`] = $object.`bar`;
    });

    $this->assertTrue($_SERVER[`__test.rebind`]);

    $container->bind(`foo`, function () {
    });

    $this->assertEquals(`bar`, $container[`foobar`]);
}

resolving 特性

resolving 回撥的型別

resolving 不僅可以針對介面執行回撥函式,還可以針對介面實現的型別進行回撥函式。

public function testResolvingCallbacksAreCalledForType()
{
    $container = new Container;
    $container->resolving(`StdClass`, function ($object) {
        return $object->name = `taylor`;
    });
    $container->bind(`foo`, function () {
          return new StdClass;
    });
    $instance = $container->make(`foo`);

    $this->assertEquals(`taylor`, $instance->name);
}
public function testResolvingCallbacksShouldBeFiredWhenCalledWithAliases()
{
    $container = new Container;
    $container->alias(`StdClass`, `std`);
    $container->resolving(`std`, function ($object) {
        return $object->name = `taylor`;
    });
    $container->bind(`foo`, function () {
        return new StdClass;
    });
    $instance = $container->make(`foo`);

    $this->assertEquals(`taylor`, $instance->name);
}

resolving 回撥與 instance

前面講過,對於 singleton 繫結來說,resolving 回撥函式僅僅執行一次,只在 singleton 第一次解析的時候才會呼叫。如果我們利用 instance 直接繫結類的物件,不需要解析,那麼 resolving 回撥函式將不會被呼叫:

public function testResolvingCallbacksAreCalledForSpecificAbstracts()
{
    $container = new Container;
    $container->resolving(`foo`, function ($object) {
        return $object->name = `taylor`;
    });
    $obj = new StdClass;
    $container->instance(`foo`, $obj);
    $instance = $container->make(`foo`);

    $this->assertFalse(isset($instance->name));
}

extend 擴充套件特性

extend 用於擴充套件繫結物件的功能,對於普通繫結來說,這個函式的位置很靈活:

在繫結前擴充套件

public function testExtendIsLazyInitialized()
{
    ContainerLazyExtendStub::$initialized = false;
    
    $container = new Container;      
    $container->extend(`IlluminateTestsContainerContainerLazyExtendStub`, function ($obj, $container) {
        $obj->init();
        return $obj;   
    });    
    $container->bind(`IlluminateTestsContainerContainerLazyExtendStub`); 

    $this->assertFalse(ContainerLazyExtendStub::$initialized);   
    $container->make(`IlluminateTestsContainerContainerLazyExtendStub`);   
    $this->assertTrue(ContainerLazyExtendStub::$initialized);
}

在繫結後解析前擴充套件

public function testExtendIsLazyInitialized()
{
    ContainerLazyExtendStub::$initialized = false;
    
    $container = new Container;   
    $container->bind(`IlluminateTestsContainerContainerLazyExtendStub`);    
    $container->extend(`IlluminateTestsContainerContainerLazyExtendStub`, function ($obj, $container) {
        $obj->init();
        return $obj;   
    });    

    $this->assertFalse(ContainerLazyExtendStub::$initialized);   
    $container->make(`IlluminateTestsContainerContainerLazyExtendStub`);   
    $this->assertTrue(ContainerLazyExtendStub::$initialized);
}

在解析後擴充套件

public function testExtendIsLazyInitialized()
{
    ContainerLazyExtendStub::$initialized = false;
    
    $container = new Container;   
    $container->bind(`IlluminateTestsContainerContainerLazyExtendStub`);         
    
    $container->make(`IlluminateTestsContainerContainerLazyExtendStub`); 
    $this->assertFalse(ContainerLazyExtendStub::$initialized);
    
    $container->extend(`IlluminateTestsContainerContainerLazyExtendStub`, function ($obj, $container) {
        $obj->init();
        return $obj;   
    });
    $this->assertFalse(ContainerLazyExtendStub::$initialized);  
      
    $container->make(`IlluminateTestsContainerContainerLazyExtendStub`); 
    $this->assertTrue(ContainerLazyExtendStub::$initialized);
}

可以看出,無論在哪個位置,extend 擴充套件都有 lazy 初始化的特點,也就是使用 extend 函式並不會立即起作用,而是要等到 make 解析才會啟用。

extend 與 instance 繫結

對於 instance 繫結來說,暫時 extend 的位置需要位於 instance 之後才會起作用,並且會立即起作用,沒有 lazy 的特點:

public function testExtendInstancesArePreserved()
{
    $container = new Container;

    $obj = new StdClass;
    $obj->foo = `foo`;
    $container->instance(`foo`, $obj);
    $container->extend(`foo`, function ($obj, $container) {
        $obj->bar = `baz`;

        return $obj;
    });

    $this->assertEquals(`foo`, $container->make(`foo`)->foo);
    $this->assertEquals(`baz`, $container->make(`foo`)->bar);
}

extend 繫結與 rebind 回撥

無論擴充套件物件是 instance 繫結還是 bind 繫結,extend 都會啟動 rebind 回撥函式:

public function testExtendReBindingInstance()
{
    $_SERVER[`_test_rebind`] = false;

    $container = new Container;
    $container->rebinding(`foo`,function (){
        $_SERVER[`_test_rebind`] = true;
    });

    $obj = new StdClass;
    $container->instance(`foo`,$obj);

    $container->make(`foo`);

    $container->extend(`foo`, function ($obj, $container) {
        return $obj;
    });

    this->assertTrue($_SERVER[`_test_rebind`]);
}

public function testExtendReBinding()
{
    $_SERVER[`_test_rebind`] = false;

    $container = new Container;
    $container->rebinding(`foo`,function (){
        $_SERVER[`_test_rebind`] = true;
    });
    $container->bind(`foo`,function (){
        $obj = new StdClass;

        return $obj;
    });

    $container->make(`foo`);

    $container->extend(`foo`, function ($obj, $container) {
        return $obj;
    });

    this->assertFalse($_SERVER[`_test_rebind`]);
}

contextual 繫結特性

contextual 在繫結前

contextual 繫結不僅可以與 bind 繫結合作,相互不干擾,還可以與 instance 繫結相互合作。而且 instance 的位置也很靈活,可以在 contextual 繫結前,也可以在contextual 繫結後:

public function testContextualBindingWorksForExistingInstancedBindings()
{
    $container = new Container;

    $container->instance(`IlluminateTestsContainerIContainerContractStub`, new ContainerImplementationStub);

    $container->when(`IlluminateTestsContainerContainerTestContextInjectOne`)->needs(`IlluminateTestsContainerIContainerContractStub`)->give(`IlluminateTestsContainerContainerImplementationStubTwo`);

    $this->assertInstanceOf(
             `IlluminateTestsContainerContainerImplementationStubTwo`,
             $container->make(`IlluminateTestsContainerContainerTestContextInjectOne`)->impl
     );
}

contextual 在繫結後

public function testContextualBindingWorksForNewlyInstancedBindings()
{
    $container = new Container;

    $container->when(`IlluminateTestsContainerContainerTestContextInjectOne`)->needs(`IlluminateTestsContainerIContainerContractStub`)->give(`IlluminateTestsContainerContainerImplementationStubTwo`);

    $container->instance(`IlluminateTestsContainerIContainerContractStub`, new ContainerImplementationStub);

    $this->assertInstanceOf(
            `IlluminateTestsContainerContainerImplementationStubTwo`,
        $container->make(`IlluminateTestsContainerContainerTestContextInjectOne`)->impl
    );
}

contextual 繫結與別名

contextual 繫結也可以在別名上進行,無論賦予別名的位置是 contextual 的前面還是後面:

public function testContextualBindingDoesntOverrideNonContextualResolution()
{
    $container = new Container;

    $container->instance(`stub`, new ContainerImplementationStub);
    $container->alias(`stub`, `IlluminateTestsContainerIContainerContractStub`);

    $container->when(`IlluminateTestsContainerContainerTestContextInjectTwo`)->needs(`IlluminateTestsContainerIContainerContractStub`)->give(`IlluminateTestsContainerContainerImplementationStubTwo`);

    $this->assertInstanceOf(
            `IlluminateTestsContainerContainerImplementationStubTwo`,
            $container->make(`IlluminateTestsContainerContainerTestContextInjectTwo`)->impl
        );

    $this->assertInstanceOf(
            `IlluminateTestsContainerContainerImplementationStub`,
            $container->make(`IlluminateTestsContainerContainerTestContextInjectOne`)->impl
    );
}

public function testContextualBindingWorksOnNewAliasedBindings()
{
    $container = new Container;

    $container->when(`IlluminateTestsContainerContainerTestContextInjectOne`)->needs(`IlluminateTestsContainerIContainerContractStub`)->give(`IlluminateTestsContainerContainerImplementationStubTwo`);

    $container->bind(`stub`, ContainerImplementationStub::class);
    $container->alias(`stub`, `IlluminateTestsContainerIContainerContractStub`);

    $this->assertInstanceOf(
          `IlluminateTestsContainerContainerImplementationStubTwo`,
          $container->make(`IlluminateTestsContainerContainerTestContextInjectOne`)->impl
    );
}

爭議

目前比較有爭議的是下面的情況:

public function testContextualBindingWorksOnExistingAliasedInstances()
{
    $container = new Container;

    $container->alias(`IlluminateTestsContainerIContainerContractStub`, `stub`);
    $container->instance(`stub`, new ContainerImplementationStub);

    $container->when(`IlluminateTestsContainerContainerTestContextInjectOne`)->needs(`stub`)->give(`IlluminateTestsContainerContainerImplementationStubTwo`);

    $this->assertInstanceOf(
        `IlluminateTestsContainerContainerImplementationStubTwo`,
        $container->make(`IlluminateTestsContainerContainerTestContextInjectOne`)->impl
    ); 
}

由於instance的特性,當別名被繫結到其他物件上時,別名 stub 已經失去了與 IlluminateTestsContainerIContainerContractStub 之間的關係,因此不能使用 stub 代替作上下文繫結。
但是另一方面:

public function testContextualBindingWorksOnBoundAlias()
{
    $container = new Container;

    $container->alias(`IlluminateTestsContainerIContainerContractStub`, `stub`);
    $container->bind(`stub`, ContainerImplementationStub::class);

    $container->when(`IlluminateTestsContainerContainerTestContextInjectOne`)->needs(`stub`)->give(`IlluminateTestsContainerContainerImplementationStubTwo`);

    $this->assertInstanceOf(
        `IlluminateTestsContainerContainerImplementationStubTwo`,
        $container->make(`IlluminateTestsContainerContainerTestContextInjectOne`)->impl
    ); 
}

程式碼只是從 instance 繫結改為 bind 繫結,由於 bind 繫結只切斷了別名中的 alias 陣列的聯絡,並沒有斷絕abstractAlias陣列的聯絡,因此這段程式碼卻可以通過,很讓人難以理解。本人在給 Taylor Otwell 提出 PR 時,作者原話為“I`m not making any of these changes to the container on a patch release.”。也許,在以後(5.5或以後)版本作者會更新這裡的邏輯,我們就可以看看服務容器對別名繫結的態度了,大家也最好不要這樣用。

服務容器中的閉包函式引數

服務容器中很多函式都有閉包函式,這些閉包函式可以放入特定的引數,在繫結或者解析過程中,這些引數會被服務容器自動帶入各種類物件或者服務容器例項。

bind 閉包引數

public function testAliasesWithArrayOfParameters()
{
    $container = new Container;    
    $container->bind(`foo`, function ($app, $config) {
        return $config;    
    });    

    $container->alias(`foo`, `baz`);    
    $this->assertEquals([1, 2, 3], $container->makeWith(`baz`, [1, 2, 3]));
}

extend 閉包引數

public function testExtendedBindings()
{
    $container = new Container;    
    $container[`foo`] = `foo’;    
    $container->extend(`foo`, function ($old, $container) {
        return $old.`bar’;    
    });
   
    $this->assertEquals(`foobar`, $container->make(`foo`));
    
    $container = new Container;
    
    $container->singleton(`foo`, function () {
        return (object) [`name` => `taylor`];    
    });    
    $container->extend(`foo`, function ($old, $container) {
        $old->age = 26;
        return $old;    
    });
    
    $result = $container->make(`foo`);
    $this->assertEquals(`taylor`, $result->name);    
    $this->assertEquals(26, $result->age);   
    $this->assertSame($result, $container->make(`foo`));
}

bindmethod 閉包引數

public function testCallWithBoundMethod()
{
    $container = new Container;
    $container->bindMethod(`IlluminateTestsContainerContainerTestCallStub@unresolvable`, function ($stub,$container) {
        $container[`foo`] = `foo`;
        return $stub->unresolvable(`foo`, `bar`);
    });
    $result = $container->call(`IlluminateTestsContainerContainerTestCallStub@unresolvable`);
    $this->assertEquals([`foo`, `bar`], $result);
    $this->assertEquals(`foo`,$container[`foo`]);
}

resolve 閉包引數

public function testResolvingCallbacksAreCalledForSpecificAbstracts()
{
     $container = new Container;
     $container->resolving(`foo`, function ($object,$container) {
         return $object->name = `taylor`;
     });
 
     $container->bind(`foo`, function () {
        return new StdClass;
     });
     $instance = $container->make(`foo`);

     $this->assertEquals(`taylor`, $instance->name);
}

rebinding 閉包引數

public function testReboundListeners()
{
    $container = new Container;
    $container->bind(`foo`, function () {
        return `foo`;
    });
  
    $container->rebinding(`foo`, function ($container,$object) {
         $container[`bar`] = $object.`bar`;
    });
  
    $container->bind(`foo`, function () {
    });

    $this->assertEquals(`bar`,$container[`foobar`]);
}

相關文章