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

前言

首先歡迎關注個人博客: www.leoyang90.cnphp

在前面幾個博客中,我詳細講了 Ioc 容器各個功能的使用、綁定的源碼、解析的源碼,今天這篇博客會詳細介紹 Ioc 容器的一些細節,一些特性,以便更好地掌握容器的功能。laravel

注:本文使用的測試類與測試對象都取自 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 的回調函數是不會調用的:app

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 綁定:ide

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 不只能夠針對接口執行回調函數,還能夠針對接口實現的類型進行回調函數。this

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('Illuminate\Tests\Container\ContainerLazyExtendStub', function ($obj, $container) {
        $obj->init();
        return $obj;   
    });    
    $container->bind('Illuminate\Tests\Container\ContainerLazyExtendStub'); 

    $this->assertFalse(ContainerLazyExtendStub::$initialized);   
    $container->make('Illuminate\Tests\Container\ContainerLazyExtendStub');   
    $this->assertTrue(ContainerLazyExtendStub::$initialized);
}

在綁定後解析前擴展

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

    $this->assertFalse(ContainerLazyExtendStub::$initialized);   
    $container->make('Illuminate\Tests\Container\ContainerLazyExtendStub');   
    $this->assertTrue(ContainerLazyExtendStub::$initialized);
}

在解析後擴展

public function testExtendIsLazyInitialized()
{
    ContainerLazyExtendStub::$initialized = false;
    
    $container = new Container;   
    $container->bind('Illuminate\Tests\Container\ContainerLazyExtendStub');         
    
    $container->make('Illuminate\Tests\Container\ContainerLazyExtendStub'); 
    $this->assertFalse(ContainerLazyExtendStub::$initialized);
    
    $container->extend('Illuminate\Tests\Container\ContainerLazyExtendStub', function ($obj, $container) {
        $obj->init();
        return $obj;   
    });
    $this->assertFalse(ContainerLazyExtendStub::$initialized);  
      
    $container->make('Illuminate\Tests\Container\ContainerLazyExtendStub'); 
    $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('Illuminate\Tests\Container\IContainerContractStub', new ContainerImplementationStub);

    $container->when('Illuminate\Tests\Container\ContainerTestContextInjectOne')->needs('Illuminate\Tests\Container\IContainerContractStub')->give('Illuminate\Tests\Container\ContainerImplementationStubTwo');

    $this->assertInstanceOf(
             'Illuminate\Tests\Container\ContainerImplementationStubTwo',
             $container->make('Illuminate\Tests\Container\ContainerTestContextInjectOne')->impl
     );
}

contextual 在綁定後

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

    $container->when('Illuminate\Tests\Container\ContainerTestContextInjectOne')->needs('Illuminate\Tests\Container\IContainerContractStub')->give('Illuminate\Tests\Container\ContainerImplementationStubTwo');

    $container->instance('Illuminate\Tests\Container\IContainerContractStub', new ContainerImplementationStub);

    $this->assertInstanceOf(
            'Illuminate\Tests\Container\ContainerImplementationStubTwo',
        $container->make('Illuminate\Tests\Container\ContainerTestContextInjectOne')->impl
    );
}

contextual 綁定與別名

contextual 綁定也能夠在別名上進行,不管賦予別名的位置是 contextual 的前面仍是後面:

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

    $container->instance('stub', new ContainerImplementationStub);
    $container->alias('stub', 'Illuminate\Tests\Container\IContainerContractStub');

    $container->when('Illuminate\Tests\Container\ContainerTestContextInjectTwo')->needs('Illuminate\Tests\Container\IContainerContractStub')->give('Illuminate\Tests\Container\ContainerImplementationStubTwo');

    $this->assertInstanceOf(
            'Illuminate\Tests\Container\ContainerImplementationStubTwo',
            $container->make('Illuminate\Tests\Container\ContainerTestContextInjectTwo')->impl
        );

    $this->assertInstanceOf(
            'Illuminate\Tests\Container\ContainerImplementationStub',
            $container->make('Illuminate\Tests\Container\ContainerTestContextInjectOne')->impl
    );
}

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

    $container->when('Illuminate\Tests\Container\ContainerTestContextInjectOne')->needs('Illuminate\Tests\Container\IContainerContractStub')->give('Illuminate\Tests\Container\ContainerImplementationStubTwo');

    $container->bind('stub', ContainerImplementationStub::class);
    $container->alias('stub', 'Illuminate\Tests\Container\IContainerContractStub');

    $this->assertInstanceOf(
          'Illuminate\Tests\Container\ContainerImplementationStubTwo',
          $container->make('Illuminate\Tests\Container\ContainerTestContextInjectOne')->impl
    );
}

爭議

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

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

    $container->alias('Illuminate\Tests\Container\IContainerContractStub', 'stub');
    $container->instance('stub', new ContainerImplementationStub);

    $container->when('Illuminate\Tests\Container\ContainerTestContextInjectOne')->needs('stub')->give('Illuminate\Tests\Container\ContainerImplementationStubTwo');

    $this->assertInstanceOf(
        'Illuminate\Tests\Container\ContainerImplementationStubTwo',
        $container->make('Illuminate\Tests\Container\ContainerTestContextInjectOne')->impl
    ); 
}

因爲instance的特性,當別名被綁定到其餘對象上時,別名 stub 已經失去了與 IlluminateTestsContainerIContainerContractStub 之間的關係,所以不能使用 stub 代替做上下文綁定。
可是另外一方面:

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

    $container->alias('Illuminate\Tests\Container\IContainerContractStub', 'stub');
    $container->bind('stub', ContainerImplementationStub::class);

    $container->when('Illuminate\Tests\Container\ContainerTestContextInjectOne')->needs('stub')->give('Illuminate\Tests\Container\ContainerImplementationStubTwo');

    $this->assertInstanceOf(
        'Illuminate\Tests\Container\ContainerImplementationStubTwo',
        $container->make('Illuminate\Tests\Container\ContainerTestContextInjectOne')->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('Illuminate\Tests\Container\ContainerTestCallStub@unresolvable', function ($stub,$container) {
        $container['foo'] = 'foo';
        return $stub->unresolvable('foo', 'bar');
    });
    $result = $container->call('Illuminate\Tests\Container\ContainerTestCallStub@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']);
}
相關文章
相關標籤/搜索