前言
本文 GitBook 地址: https://www.gitbook.com/book/leoyang90/laravel-source-analysis
首先歡迎關注我的部落格: 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('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 已經失去了與 Illuminate\Tests\Container\IContainerContractStub 之間的關係,因此不能使用 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']);
}