在說 Ioc 容器以前,咱們須要瞭解什麼是 Ioc 容器。php
Laravel 服務容器是一個用於管理類依賴和執行依賴注入的強大工具。laravel
在理解這句話以前,咱們須要先了解一下服務容器的前因後果: laravel神奇的服務容器。這篇博客告訴咱們,服務容器就是工廠模式的升級版,對於傳統的工廠模式來講,雖然解耦了對象和外部資源之間的關係,可是工廠和外部資源之間卻存在了耦和。而服務容器在爲對象建立了外部資源的同時,又與外部資源沒有任何關係,這個就是 Ioc 容器。
所謂的依賴注入和控制反轉: 依賴注入和控制反轉,就是segmentfault
只要不是由內部生產(好比初始化、構造函數 __construct 中經過工廠方法、自行手動 new 的),而是由外部以參數或其餘形式注入的,都屬於依賴注入(DI)api
也就是說:數組
依賴注入是從應用程序的角度在描述,能夠把依賴注入描述完整點:應用程序依賴容器建立並注入它所須要的外部資源;閉包
控制反轉是從容器的角度在描述,描述完整點:容器控制應用程序,由容器反向的嚮應用程序注入應用程序所須要的外部資源。app
Laravel服務容器主要承擔兩個做用:綁定與解析,服務容器的結構以下:
ide
所謂的綁定就是將接口與實現創建對應關係。幾乎全部的服務容器綁定都是在服務提供者中完成,也就是在服務提供者中綁定。函數
若是一個類沒有基於任何接口那麼就沒有必要將其綁定到容器。容器並不須要被告知如何構建對象,由於它會使用 PHP 的反射服務自動解析出具體的對象。工具
也就是說,若是須要依賴注入的外部資源若是沒有接口,那麼就不須要綁定,直接利用服務容器進行解析就能夠了,服務容器會根據類名利用反射對其進行自動構造。
綁定有多種方法,首先最經常使用的是bind函數的綁定:
綁定自身
$this->app->bind('App\Services\RedisEventPusher', null);
綁定閉包
$this->app->bind('name', function () { return 'Taylor'; });//閉包返回變量 $this->app->bind('HelpSpot\API', function () { return HelpSpot\API::class; });//閉包直接提供類實現方式 public function testSharedClosureResolution() { $container = new Container; $class = new stdClass; $container->bind('class', function () use ($class) { return $class; }); $this->assertSame($class, $container->make('class')); }//閉包返回類變量 $this->app->bind('HelpSpot\API', function () { return new HelpSpot\API(); });//閉包直接提供類實現方式 $this->app->bind('HelpSpot\API', function ($app) { return new HelpSpot\API($app->make('HttpClient')); });//閉包返回須要依賴注入的類
綁定接口
public function testCanBuildWithoutParameterStackWithConstructors() { $container = new Container; $container->bind('Illuminate\Tests\Container\IContainerContractStub', 'Illuminate\Tests\Container\ContainerImplementationStub'); $this->assertInstanceOf(ContainerDependentStub::class, $container->build(ContainerDependentStub::class)); } interface IContainerContractStub { } class ContainerImplementationStub implements IContainerContractStub { } class ContainerDependentStub { public $impl; public function __construct(IContainerContractStub $impl) { $this->impl = $impl; } }
這三種綁定方式中,第一種綁定自身通常用於綁定單例。
public function testBindIfDoesntRegisterIfServiceAlreadyRegistered() { $container = new Container; $container->bind('name', function () return 'Taylor'; }); $container->bindIf('name', function () { return 'Dayle'; }); $this->assertEquals('Taylor', $container->make('name')); }
singleton 方法綁定一個只須要解析一次的類或接口到容器,而後接下來對容器的調用將會返回同一個實例:
$this->app->singleton('HelpSpot\API', function ($app) { return new HelpSpot\API($app->make('HttpClient')); });
值得注意的是,singleton綁定在解析的時候若存在參數重載,那麼就自動取消單例模式。
public function testSingletonBindingsNotRespectedWithMakeParameters() { $container = new Container; $container->singleton('foo', function ($app, $config) { return $config; }); $this->assertEquals(['name' => 'taylor'], $container->makeWith('foo', ['name' => 'taylor'])); $this->assertEquals(['name' => 'abigail'], $container->makeWith('foo', ['name' => 'abigail'])); }
咱們還可使用 instance 方法綁定一個已存在的對象實例到容器,隨後調用容器將老是返回給定的實例:
$api = new HelpSpot\API(new HttpClient); $this->app->instance('HelpSpot\Api', $api);
有時侯咱們可能有兩個類使用同一個接口,但咱們但願在每一個類中注入不一樣實現,例如,兩個控制器依賴 IlluminateContractsFilesystemFilesystem 契約的不一樣實現。Laravel 爲此定義了簡單、平滑的接口:
use Illuminate\Support\Facades\Storage; use App\Http\Controllers\VideoController; use App\Http\Controllers\PhotoControllers; use Illuminate\Contracts\Filesystem\Filesystem; $this->app->when(StorageController::class) ->needs(Filesystem::class) ->give(function () { Storage::class });//提供類名 $this->app->when(PhotoController::class) ->needs(Filesystem::class) ->give(function () { return new Storage(); });//提供實現方式 $this->app->when(VideoController::class) ->needs(Filesystem::class) ->give(function () { return new Storage($app->make(Disk::class)); });//須要依賴注入
咱們可能有一個接收注入類的類,同時須要注入一個原生的數值好比整型,能夠結合上下文輕鬆注入這個類須要的任何值:
$this->app->when('App\Http\Controllers\UserController') ->needs('$variableName') ->give($value);
數組綁定通常用於綁定閉包和變量,可是不能綁定接口,不然只能返回接口的實現類名字符串,並不能返回實現類的對象。
public function testArrayAccess() { $container = new Container; $container[IContainerContractStub::class] = ContainerImplementationStub::class; $this->assertTrue(isset($container[IContainerContractStub::class])); $this->assertEquals(ContainerImplementationStub::class, $container[IContainerContractStub::class]); unset($container['something']); $this->assertFalse(isset($container['something'])); }
少數狀況下,咱們須要解析特定分類下的全部綁定,例如,你正在構建一個接收多個不一樣 Report 接口實現的報告聚合器,在註冊完 Report 實現以後,能夠經過 tag 方法給它們分配一個標籤:
$this->app->bind('SpeedReport', function () { // }); $this->app->bind('MemoryReport', function () { // }); $this->app->tag(['SpeedReport', 'MemoryReport'], 'reports');
這些服務被打上標籤後,能夠經過 tagged 方法來輕鬆解析它們:
$this->app->bind('ReportAggregator', function ($app) { return new ReportAggregator($app->tagged('reports')); });
public function testContainerTags() { $container = new Container; $container->tag('Illuminate\Tests\Container\ContainerImplementationStub', 'foo', 'bar'); $container->tag('Illuminate\Tests\Container\ContainerImplementationStubTwo', ['foo']); $this->assertCount(1, $container->tagged('bar')); $this->assertCount(2, $container->tagged('foo')); $this->assertInstanceOf('Illuminate\Tests\Container\ContainerImplementationStub', $container->tagged('foo')[0]); $this->assertInstanceOf('Illuminate\Tests\Container\ContainerImplementationStub', $container->tagged('bar')[0]); $this->assertInstanceOf('Illuminate\Tests\Container\ContainerImplementationStubTwo', $container->tagged('foo')[1]); $container = new Container; $container->tag(['Illuminate\Tests\Container\ContainerImplementationStub', 'Illuminate\Tests\Container\ContainerImplementationStubTwo'], ['foo']); $this->assertCount(2, $container->tagged('foo')); $this->assertInstanceOf('Illuminate\Tests\Container\ContainerImplementationStub', $container->tagged('foo')[0]); $this->assertInstanceOf('Illuminate\Tests\Container\ContainerImplementationStubTwo', $container->tagged('foo')[1]); $this->assertEmpty($container->tagged('this_tag_does_not_exist')); }
extend是在當原來的類被註冊或者實例化出來後,能夠對其進行擴展,並且能夠支持多重擴展:
public function testExtendInstancesArePreserved() { $container = new Container; $container->bind('foo', function () { $obj = new StdClass; $obj->foo = 'bar'; return $obj; }); $obj = new StdClass; $obj->foo = 'foo'; $container->instance('foo', $obj); $container->extend('foo', function ($obj, $container) { $obj->bar = 'baz'; return $obj; }); $container->extend('foo', function ($obj, $container) { $obj->baz = 'foo'; return $obj; }); $this->assertEquals('foo', $container->make('foo')->foo); $this->assertEquals('baz', $container->make('foo')->bar); $this->assertEquals('foo', $container->make('foo')->baz); }
綁定是針對接口的,是爲接口提供實現方式的方法。咱們能夠對接口在不一樣的時間段裏提供不一樣的實現方法,通常來講,對同一個接口提供新的實現方法後,不會對已經實例化的對象產生任何影響。可是在一些場景下,在提供新的接口實現後,咱們但願對已經實例化的對象從新作一些改變,這個就是 rebinding 函數的用途。
下面就是一個例子:
abstract class Car { public function __construct(Fuel $fuel) { $this->fuel = $fuel; } public function refuel($litres) { return $litres * $this->fuel->getPrice(); } public function setFuel(Fuel $fuel) { $this->fuel = $fuel; } } class JeepWrangler extends Car { // } interface Fuel { public function getPrice(); } class Petrol implements Fuel { public function getPrice() { return 130.7; } }
咱們在服務容器中是這樣對car接口和fuel接口綁定的:
$this->app->bind('fuel', function ($app) { return new Petrol; }); $this->app->bind('car', function ($app) { return new JeepWrangler($app['fuel']); }); $this->app->make('car');
若是car被服務容器解析實例化成對象以後,有人修改了 fuel 接口的實現,從 Petrol 改成 PremiumPetrol:
$this->app->bind('fuel', function ($app) { return new PremiumPetrol; });
因爲 car 已經被實例化,那麼這個接口實現的改變並不會影響到 car 的實現,倘若咱們想要 car 的成員變量 fuel 隨着 fuel 接口的變化而變化,咱們就須要一個回調函數,每當對 fuel 接口實現進行改變的時候,都要對 car 的 fuel 變量進行更新,這就是 rebinding 的用途:
$this->app->bindShared('car', function ($app) { return new JeepWrangler($app->rebinding('fuel', function ($app, $fuel) { $app['car']->setFuel($fuel); })); });
在說服務容器的解析以前,須要先說說服務的別名。什麼是服務別名呢?不一樣於上一個博客中提到的 Facade 門面的別名(在 config/app 中定義),這裏的別名服務綁定名稱的別名。經過服務綁定的別名,在解析服務的時候,跟不使用別名的效果一致。別名的做用也是爲了同時支持全類型的服務綁定名稱以及簡短的服務綁定名稱考慮的。
通俗的講,假如咱們想要建立 auth 服務,咱們既能夠這樣寫:
$this->app->make('auth')
又能夠寫成:
$this->app->make('\Illuminate\Auth\AuthManager::class')
還能夠寫成
$this->app->make('\Illuminate\Contracts\Auth\Factory::class')
後面兩個服務的名字都是 auth 的別名,使用別名和使用 auth 的效果是相同的。
須要注意的是別名是能夠遞歸的:
app()->alias('service', 'alias_a'); app()->alias('alias_a', 'alias_b'); app()-alias('alias_b', 'alias_c');
會獲得:
'alias_a' => 'service' 'alias_b' => 'alias_a' 'alias_c' => 'alias_b'
那麼這些別名是如何加載到服務容器裏面的呢?實際上,服務容器裏面有個 aliases 數組:
$aliases = [ 'app' => [\Illuminate\Foundation\Application::class, \Illuminate\Contracts\Container\Container::class, \Illuminate\Contracts\Foundation\Application::class], 'auth' => [\Illuminate\Auth\AuthManager::class, \Illuminate\Contracts\Auth\Factory::class], 'auth.driver' => [\Illuminate\Contracts\Auth\Guard::class], 'blade.compiler' => [\Illuminate\View\Compilers\BladeCompiler::class], 'cache' => [\Illuminate\Cache\CacheManager::class, \Illuminate\Contracts\Cache\Factory::class], ... ]
而服務容器的初始化的過程當中,會運行一個函數:
public function registerCoreContainerAliases() { foreach ($aliases as $key => $aliases) { foreach ($aliases as $alias) { $this->alias($key, $alias); } } } public function alias($abstract, $alias) { $this->aliases[$alias] = $abstract; $this->abstractAliases[$abstract][] = $alias; }
加載後,服務容器的aliases和abstractAliases數組:
$aliases = [ 'Illuminate\Foundation\Application' = "app" 'Illuminate\Contracts\Container\Container' = "app" 'Illuminate\Contracts\Foundation\Application' = "app" 'Illuminate\Auth\AuthManager' = "auth" 'Illuminate\Contracts\Auth\Factory' = "auth" 'Illuminate\Contracts\Auth\Guard' = "auth.driver" 'Illuminate\View\Compilers\BladeCompiler' = "blade.compiler" 'Illuminate\Cache\CacheManager' = "cache" 'Illuminate\Contracts\Cache\Factory' = "cache" ... ] $abstractAliases = [ app = {array} [3] 0 = "Illuminate\Foundation\Application" 1 = "Illuminate\Contracts\Container\Container" 2 = "Illuminate\Contracts\Foundation\Application" auth = {array} [2] 0 = "Illuminate\Auth\AuthManager" 1 = "Illuminate\Contracts\Auth\Factory" auth.driver = {array} [1] 0 = "Illuminate\Contracts\Auth\Guard" blade.compiler = {array} [1] 0 = "Illuminate\View\Compilers\BladeCompiler" cache = {array} [2] 0 = "Illuminate\Cache\CacheManager" 1 = "Illuminate\Contracts\Cache\Factory" ... ]
有不少方式能夠從容器中解析對象,首先,你可使用 make 方法,該方法接收你想要解析的類名或接口名做爲參數:
public function testAutoConcreteResolution() { $container = new Container; $this->assertInstanceOf('Illuminate\Tests\Container\ContainerConcreteStub', $container->make('Illuminate\Tests\Container\ContainerConcreteStub')); } //帶有依賴注入和默認值的解析 public function testResolutionOfDefaultParameters() { $container = new Container; $instance = $container->make('Illuminate\Tests\Container\ContainerDefaultValueStub'); $this->assertInstanceOf('Illuminate\Tests\Container\ContainerConcreteStub', $instance->stub); $this->assertEquals('taylor', $instance->default); } // public function testResolvingWithArrayOfParameters() { $container = new Container; $instance = $container->makeWith(ContainerDefaultValueStub::class, ['default' => 'adam']); $this->assertEquals('adam', $instance->default); $instance = $container->make(ContainerDefaultValueStub::class); $this->assertEquals('taylor', $instance->default); $container->bind('foo', function ($app, $config) { return $config; }); $this->assertEquals([1, 2, 3], $container->makeWith('foo', [1, 2, 3])); } public function testNestedDependencyResolution() { $container = new Container; $container->bind('Illuminate\Tests\Container\IContainerContractStub', 'Illuminate\Tests\Container\ContainerImplementationStub'); $class = $container->make('Illuminate\Tests\Container\ContainerNestedDependentStub'); $this->assertInstanceOf('Illuminate\Tests\Container\ContainerDependentStub', $class->inner); $this->assertInstanceOf('Illuminate\Tests\Container\ContainerImplementationStub', $class->inner->impl); } class ContainerDefaultValueStub { public $stub; public $default; public function __construct(ContainerConcreteStub $stub, $default = 'taylor') { $this->stub = $stub; $this->default = $default; } } class ContainerConcreteStub { } class ContainerImplementationStub implements IContainerContractStub { } class ContainerDependentStub { public $impl; public function __construct(IContainerContractStub $impl) { $this->impl = $impl; } } class ContainerNestedDependentStub { public $inner; public function __construct(ContainerDependentStub $inner) { $this->inner = $inner; } }
若是你所在的代碼位置訪問不了 $app 變量,可使用輔助函數resolve:
$api = resolve('HelpSpot\API');
namespace App\Http\Controllers; use App\Users\Repository as UserRepository; class UserController extends Controller{ /** * 用戶倉庫實例 */ protected $users; /** * 建立一個控制器實例 * * @param UserRepository $users 自動注入 * @return void */ public function __construct(UserRepository $users) { $this->users = $users; } }
make 解析是服務容器進行解析構建類對象時所用的方法,在實際應用中,還有另一個需求,那就是當前已經獲取了一個類對象,咱們想要調用它的一個方法函數,這時發現這個方法中參數衆多,若是一個個的 make 會比較繁瑣,這個時候就要用到 call 解析了。咱們能夠看這個例子:
class TaskRepository{ public function testContainerCall(User $user,Task $task){ $this->assertInstanceOf(User::class, $user); $this->assertInstanceOf(Task::class, $task); } public static function testContainerCallStatic(User $user,Task $task){ $this->assertInstanceOf(User::class, $user); $this->assertInstanceOf(Task::class, $task); } public function testCallback(){ echo 'call callback successfully!'; } public function testDefaultMethod(){ echo 'default Method successfully!'; } }
public function testCallWithDependencies() { $container = new Container; $result = $container->call(function (StdClass $foo, $bar = []) { return func_get_args(); }); $this->assertInstanceOf('stdClass', $result[0]); $this->assertEquals([], $result[1]); $result = $container->call(function (StdClass $foo, $bar = []) { return func_get_args(); }, ['bar' => 'taylor']); $this->assertInstanceOf('stdClass', $result[0]); $this->assertEquals('taylor', $result[1]); }
public function testCallWithGlobalMethodName() { $container = new Container; $result = $container->call('Illuminate\Tests\Container\containerTestInject'); $this->assertInstanceOf('Illuminate\Tests\Container\ContainerConcreteStub', $result[0]); $this->assertEquals('taylor', $result[1]); }
服務容器的 call 解析主要依靠 call_user_func_array() 函數,關於這個函數能夠查看 Laravel學習筆記之Callback Type - 來生作個漫畫家,這個函數對類中的靜態函數和非靜態函數有一些區別,對於靜態函數來講:
class ContainerCallTest { public function testContainerCallStatic(){ App::call(TaskRepository::class.'@testContainerCallStatic'); App::call(TaskRepository::class.'::testContainerCallStatic'); App::call([TaskRepository::class,'testContainerCallStatic']); } }
服務容器調用類的靜態方法有三種,注意第三種使用數組的形式,數組中能夠直接傳類名 TaskRepository::class;
對於類的非靜態方法:
class ContainerCallTest { public function testContainerCall(){ $taskRepo = new TaskRepository(); App::call(TaskRepository::class.'@testContainerCall'); App::call([$taskRepo,'testContainerCall']); } }
咱們能夠看到非靜態方法只有兩種調用方式,並且第二種數組傳遞的參數是類對象,緣由就是 call_user_func_array函數的限制,對於非靜態方法只能傳遞對象。
服務容器還有一個 bindmethod 的方法,能夠綁定類的一個方法到自定義的函數:
public function testContainCallMethodBind(){ App::bindMethod(TaskRepository::class.'@testContainerCallStatic',function () { $taskRepo = new TaskRepository(); $taskRepo->testCallback(); }); App::call(TaskRepository::class.'@testContainerCallStatic'); App::call(TaskRepository::class.'::testContainerCallStatic'); App::call([TaskRepository::class,'testContainerCallStatic']); App::bindMethod(TaskRepository::class.'@testContainerCall',function (TaskRepository $taskRepo) { $taskRepo->testCallback(); }); $taskRepo = new TaskRepository(); App::call(TaskRepository::class.'@testContainerCall'); App::call([$taskRepo,'testContainerCall']); }
從結果上看,bindmethod 不會對靜態的第二種解析方法( :: 解析方式)起做用,對於其餘方式都會調用綁定的函數。
public function testCallWithBoundMethod() { $container = new Container; $container->bindMethod('Illuminate\Tests\Container\ContainerTestCallStub@unresolvable', function ($stub) { return $stub->unresolvable('foo', 'bar'); }); $result = $container->call('Illuminate\Tests\Container\ContainerTestCallStub@unresolvable'); $this->assertEquals(['foo', 'bar'], $result); $container = new Container; $container->bindMethod('Illuminate\Tests\Container\ContainerTestCallStub@unresolvable', function ($stub) { return $stub->unresolvable('foo', 'bar'); }); $result = $container->call([new ContainerTestCallStub, 'unresolvable']); $this->assertEquals(['foo', 'bar'], $result); } class ContainerTestCallStub { public function unresolvable($foo, $bar) { return func_get_args(); } }
public function testContainCallDefultMethod(){ App::call(TaskRepository::class,[],'testContainerCall'); App::call(TaskRepository::class,[],'testContainerCallStatic'); App::bindMethod(TaskRepository::class.'@testContainerCallStatic',function () { $taskRepo = new TaskRepository(); $taskRepo->testCallback(); }); App::bindMethod(TaskRepository::class.'@testContainerCall',function (TaskRepository $taskRepo) { $taskRepo->testCallback(); }); App::call(TaskRepository::class,[],'testContainerCall'); App::call(TaskRepository::class,[],'testContainerCallStatic'); }
值得注意的是,這種默認函數注入的方法使得非靜態的方法也能夠利用類名去調用,並不須要對象。默認函數注入也回受到 bindmethod 函數的影響。
app()['service'];
app('service');
每當服務容器解析一個對象時就會觸發一個事件。你可使用 resolving 方法監聽這個事件:
$this->app->resolving(function ($object, $app) { // 解析任何類型的對象時都會調用該方法... }); $this->app->resolving(HelpSpot\API::class, function ($api, $app) { // 解析「HelpSpot\API」類型的對象時調用... }); $this->app->afterResolving(function ($object, $app) { // 解析任何類型的對象後都會調用該方法... }); $this->app->afterResolving(HelpSpot\API::class, function ($api, $app) { // 解析「HelpSpot\API」類型的對象後調用... });
服務容器每次解析對象的時候,都會調用這些經過 resolving 和 afterResolving 函數傳入的閉包函數,也就是觸發這些事件。
注意:若是是單例,則只在解析時會觸發一次
public function testResolvingCallbacksAreCalled() { $container = new Container; $container->resolving(function ($object) { return $object->name = 'taylor'; }); $container->bind('foo', function () { return new StdClass; }); $instance = $container->make('foo'); $this->assertEquals('taylor', $instance->name); } 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); }
容器的裝飾函數有兩種,wrap用於裝飾call,factory用於裝飾make:
public function testContainerWrap() { $result = $container->wrap(function (StdClass $foo, $bar = []) { return func_get_args(); }, ['bar' => 'taylor']); $this->assertInstanceOf('Closure', $result); $result = $result(); $this->assertInstanceOf('stdClass', $result[0]); $this->assertEquals('taylor', $result[1]); } public function testContainerGetFactory() { $container = new Container; $container->bind('name', function () { return 'Taylor’; }); $factory = $container->factory('name'); $this->assertEquals($container->make('name'), $factory()); }
容器的重置函數flush會清空容器內部的aliases、abstractAliases、resolved、bindings、instances
public function testContainerFlushFlushesAllBindingsAliasesAndResolvedInstances() { $container = new Container; $container->bind('ConcreteStub', function () { return new ContainerConcreteStub; }, true); $container->alias('ConcreteStub', 'ContainerConcreteStub'); $concreteStubInstance = $container->make('ConcreteStub'); $this->assertTrue($container->resolved('ConcreteStub')); $this->assertTrue($container->isAlias('ContainerConcreteStub')); $this->assertArrayHasKey('ConcreteStub', $container->getBindings()); $this->assertTrue($container->isShared('ConcreteStub')); $container->flush(); $this->assertFalse($container->resolved('ConcreteStub')); $this->assertFalse($container->isAlias('ContainerConcreteStub')); $this->assertEmpty($container->getBindings()); $this->assertFalse($container->isShared('ConcreteStub')); }
Written with StackEdit.