「服務容器」是Lumen框架整個系統功能調度配置的核心,它提供了整個框架運行過程當中的一系列服務。「服務容器」就是提供服務(服務能夠理解爲系統運行中須要的東西,如:對象、文件路徑、系統配置等)的載體,在系統運行的過程當中動態的爲系統提供這些服務。下邊是服務容器工做示意圖:php
Lumen框架中,服務容器是由illuminate/container/Container.php中Container類完成的,該類實現了服務容器的核心功能。laravel/lumen-framework/src/Application.php中Application類繼承了該類,實現了服務容器初始化配置和功能拓展。源碼中生成服務容器的代碼是在bootstrap/app.php中:laravel
$app = new Laravel\Lumen\Application( dirname(__DIR__) );
也就是Lumen框架在處理每個請求的時候,都會首先爲這個請求生成一個服務容器,用於容納請求處理須要的服務。編程
服務容器生成之後,就能夠向其中添加服務,服務綁定能夠理解爲一個服務和一個關鍵字綁定,看做鍵值對的形式就是:一個"key" 對應一個服務。要綁定的服務不一樣,使用的容器中的綁定函數也不一樣,框架初始化時使用到的是回調函數服務綁定和實例對象服務綁定。回調函數綁定分兩種:一種是普通綁定,另一種是單例綁定,經過bind()函數中的參數$shared進行區分,項目代碼中的singleton()綁定單例就是bind()函數中$shared參數爲true的狀況。源碼以下:json
public function singleton($abstract, $concrete = null) { $this->bind($abstract, $concrete, true); }
單例綁定在整個Lumen生命週期中只會生成並使用一個實例對象。綁定一個實例對象到容器中使用的是instance()函數,綁定以後生成的實例對象會在$instance屬性中記錄。回調函數的綁定在$bindings屬性中記錄。bootstrap
有一種狀況是綁定具體類名稱,實際上也是綁定回調函數的方式,只是回調函數是服務容器根據提供的參數自動生成的,下面章節咱們會詳細講解。源碼中有以下代碼:數組
$app->singleton( Illuminate\Contracts\Debug\ExceptionHandler::class, App\Exceptions\Handler::class ); $app->singleton( Illuminate\Contracts\Console\Kernel::class, App\Console\Kernel::class );
在服務綁定過程當中,儘可能使用接口名稱和服務進行綁定,這樣可使得一個具體的功能僅僅和接口實現了耦合,當應用需求變化時能夠修改具體類,只要這個類還符合接口規範,程序依然能夠健壯的運行。這種「面向接口」編程是一種新的,更有效的解決依賴的編程模式。Lumen框架的接口定義規範都放在/learnLumen/vendor/illuminate/contracts 文件夾下。閉包
服務綁定到容器以後,運行程序就能夠隨時從容器中取出服務,這個過程稱爲「服務解析」。服務解析的步驟就是運行程序先獲取到容器對象,而後使用容器對象解析相應的服務。服務解析有經常使用幾種方式:架構
$this->app->make(App\Service\ExampleService::class);
app(App\Service\ExampleService::class);
\App::make(App\Service\ExampleService::class);
app[App\Service\ExampleService::class];
ArrayAccess(數組式訪問)接口很是有用,提供了像訪問數組同樣訪問對象的能力的接口。app
使用依賴注入的方式也能夠實現服務的自動解析。即在類的構造函數中,使用相應的類提示符,容器會利用自身的反射機制自動解析依賴並實現注入。須要注意的是:在服務註冊之後使用依賴注入功能,則該服務名稱和服務是要遵循必定規範的。即服務名稱通常爲服務生成的類名稱或者接口名稱,只有這樣當服務根據依賴限制查找到服務後生成的實例對象才能知足這個限制,不然就會報錯。框架
並非Lumen框架中全部的類都能實現自動依賴注入的功能,只有「服務容器」建立的類實例才能實現依賴自動注入。
控制反轉是框架設計的一種原則,在很大程度上下降了代碼模塊之間的耦合度,有利於框架維護和拓展。實現控制反轉最多見的方法是「依賴注入」,還有一種方法叫「依賴查找」。控制反轉將框架中解決依賴的邏輯從實現代碼類庫的內部提取到了外部來管理實現。
咱們用簡單代碼模擬一下Lumen處理用戶請求的邏輯,框架中要使用到最簡單的Request請求模塊、Response請求模塊,咱們使用單例模式簡單實現一下:
//Request模塊實現 class Request { static private $instance = null; private function __construct() { } private function __clone() { } static function getInstance() { if (self::$instance == null) self::$instance = new self(); return self::$instance; } public function get($key) { return $_GET[$key] ? $_GET[$key] : ''; } public function post($key) { return $_POST[$key] ? $_POST[$key] : ''; } } //Response模塊實現 class Response { static private $instance = null; private function __construct() { } private function __clone() { } static function getInstance() { if (self::$instance == null) self::$instance = new self(); return self::$instance; } public function json($data) { return json_encode($data); } }
咱們先來使用「依賴查找」的工廠模式來實現控制反轉,咱們須要一個工廠,簡單實現一下:
include_once 'Request.php'; include_once 'Response.php'; include_once 'ExceptionHandler.php'; abstract class Factory { static function Create($type, array $params = []) { //根據接收到的參數肯定要生產的對象 switch ($type) { case 'request': return Request::getInstance(); break; case 'response': return Response::getInstance(); break; case 'exception': return new ExceptionHandler(); break; } } }
接下來就開始實現用戶邏輯,咱們首先加入錯誤處理的簡單實現:
//開啓報告代碼中的錯誤處理 class ExceptionHandler { public function __construct() { error_reporting(-1); ini_set('display_errors', true); } }
咱們模擬一個請求用戶列表的邏輯:
include_once 'Factory.php'; Factory::Create('exception'); //用戶邏輯 class UserLogic { private $modules = []; public function __construct(array $modules) { foreach ($modules as $key => $module) { $this->modules[$key] = Factory::Create($module); } } public function getUserList() { if ($this->modules['request']->get('path') == 'userlist') { $userList = [ ['name' => '張三', 'age' => 18], ['name' => '李四', 'age' => 22] ]; return $this->modules['response']->json($userList); } } } try { $userLogic = new UserLogic(['request' => 'request', 'response' => 'response']); echo $userLogic->getUserList(); } catch (\Error $e) { var_dump($e); exit(); }
能夠看到咱們使用工廠模式管理依賴的時候,能夠在處理業務邏輯外部根據處理請求須要依賴的模塊自行進行注入。好比例子中就注入了request、response模塊。這種模式雖然解決了咱們處理邏輯對外部模塊的依賴管理問題,可是並非太完美,咱們的程序只是將原來邏輯對一個個實例子對象的依賴轉換成了工廠對這些實例子對象的依賴,工廠和這些實例子對象之間的耦合還存在,隨着工廠愈來愈大,用戶邏輯實現愈來愈複雜,這種「依賴查找」實現控制反轉的模式對於用戶來說依然很痛苦。
接下來咱們使用Ioc服務容器來實現依賴注入,下邊先實現一個簡單的服務容器:
class Container { //用於裝提供實例的回調函數,真正的容器還會裝實例等其餘內容 protected $bindings = []; //容器共享實例數組(單例) protected $instances = []; public function bind($abstract, $concrete = null, $shared = false) { if (! $concrete instanceof Closure) { //若是提供的參數不是回調函數,則產生默認的回調函數 $concrete = $this->getClosure($abstract, $concrete); } $this->bindings[$abstract] = compact('concrete', 'shared'); } public function getBuildings() { return $this->bindings; } //默認生成實例的回調函數 protected function getClosure($abstract, $concrete) { return function ($c) use ($abstract, $concrete) { $method = ($abstract == $concrete) ? 'build' : 'make'; //調用的是容器的build或make方法生成實例 return $c->$method($concrete); }; } //生成實例對象,首先解決接口和要實例化類之間的依賴關係 public function make($abstract) { $concrete = $this->getConcrete($abstract); if ($this->isBuildable($concrete, $abstract)) { $object = $this->build($this->build($concrete)); } else { $object = $this->make($concrete); } return $object; } protected function isBuildable($concrete, $abstract) { return $concrete === $abstract || $concrete instanceof Closure; } //獲取綁定的回調函數 protected function getConcrete($abstract) { if (!isset($this->bindings[$abstract])) { return $abstract; } return $this->bindings[$abstract]['concrete']; } //實例化一個對象 public function build($concrete) { if ($concrete instanceof Closure) { return $concrete($this); } $reflector = new ReflectionClass($concrete); if (! $reflector->isInstantiable()) { echo $message = "Target [$concrete] is not instantiable."; } $constructor = $reflector->getConstructor(); if(is_null($constructor)) { return new $concrete; } $dependencies = $constructor->getParameters(); $instances = $this->getDependencies($dependencies); return $reflector->newInstanceArgs($instances); } //經過反射機制實例化對象時的依賴 protected function getDependencies($parameters) { $dependencies = []; foreach($parameters as $parameter) { $dependency = $parameter->getClass(); if(is_null($dependency)) { $dependencies[] = NULL; } else { $dependencies[] = $this->resolveClass($parameter); } } return (array) $dependencies; } protected function resolveClass(ReflectionParameter $parameter) { return $this->make($parameter->getClass()->name); } //註冊一個實例並綁定到容器中 public function singleton($abstract, $concrete = null){ $this->bind($abstract, $concrete, true); } }
該服務容器能夠稱爲Lumen服務容器的簡化版,可是它實現的功能和Lumen服務容器是同樣的,雖然只有一百多行的代碼,可是理解起來有難度,這裏就詳細講解清楚簡化版容器的代碼和原理,接下來章節對Lumen服務容器源碼分析時就僅僅只對方法作簡單介紹。
根據對服務容器介紹章節所講:容器中有兩個關鍵屬性$bindings和$instance,其中$bindings中存在加入到容器中的回調函數,而$instance存放的是容器中綁定的實例對象。咱們還知道$singleton方法用來綁定單例對象,其底層只是調用了bind方法而已,只不過$shared屬性爲true,意爲容器中全局共享:
//註冊一個實例並綁定到容器中 public function singleton($abstract, $concrete = null){ $this->bind($abstract, $concrete, true); }
bind方法的實現也很簡單,只是將用戶指定的服務解析好以後存放入相應的屬性當中:
public function bind($abstract, $concrete = null, $shared = false) { if (! $concrete instanceof Closure) { //若是提供的參數不是回調函數,則產生默認的回調函數 $concrete = $this->getClosure($abstract, $concrete); } $this->bindings[$abstract] = compact('concrete', 'shared'); }
Closure是php中的匿名函數類類型。$abstract和$concrete能夠抽象理解爲KV鍵值對,K就是$abstract,是服務名;V是$concrete,是服務的具體實現。咱們理解容器,首先要將思惟從日常的業務邏輯代碼中轉換回來。業務邏輯中操做的通常是用戶數據,而容器中,咱們操做的是對象、類、接口之類的,在框架中可稱爲「服務」。若是用戶要綁定的具體實現$concrete不是匿名函數,則調用getClosure方法生成一個匿名函數:
//獲取綁定的回調函數 //默認生成實例的回調函數 protected function getClosure($abstract, $concrete) { return function ($c) use ($abstract, $concrete) { $method = ($abstract == $concrete) ? 'build' : 'make'; //調用的是容器的build或make方法生成實例 return $c->$method($concrete); }; }
getClosure是根據用戶傳入的參數來決定調用系統的build和make方法。其中build方法就是構建匿名函數和類實例的關鍵實現,使用了php中的反射機制,解析出類實例:
//實例化一個對象 public function build($concrete) { if ($concrete instanceof Closure) { return $concrete($this); } $reflector = new ReflectionClass($concrete); if (! $reflector->isInstantiable()) { echo $message = "Target [$concrete] is not instantiable."; } $constructor = $reflector->getConstructor(); if(is_null($constructor)) { return new $concrete; } $dependencies = $constructor->getParameters(); $instances = $this->getDependencies($dependencies); return $reflector->newInstanceArgs($instances); }
build首先判斷參數$concrete是一個匿名函數,就返回調用匿名函數的一個閉包。不然$concrete是一個類,利用反射機制解析類的信息,首先判斷類是否可以被實例化(例如單例就不能被實例化,容器中的單例是經過屬性$shared來區分的);確保了類可以被實例化之後,使用getConstructor()判斷類是否認義了構造函數,若是沒有定義構造函數,直接實例化獲得一個類的實例。不然就再次調用getParameters獲取構造函數中都傳入了哪些參數(也就是判斷$concrete類都有哪些依賴),getDependencies方法就是來生成$concrete依賴的函數:
//經過反射機制實例化對象時的依賴 protected function getDependencies($parameters) { $dependencies = []; foreach($parameters as $parameter) { $dependency = $parameter->getClass(); if(is_null($dependency)) { $dependencies[] = NULL; } else { $dependencies[] = $this->resolveClass($parameter); } } return (array) $dependencies; }
獲得了類依賴的實例之後,就調用newInstanceArgs($instances)來生成類的實例。
服務解析函數make主要由build函數實現:
//生成實例對象,首先解決接口和要實例化類之間的依賴關係 public function make($abstract) { $concrete = $this->getConcrete($abstract); if ($this->isBuildable($concrete, $abstract)) { $object = $this->build($this->build($concrete)); } else { $object = $this->make($concrete); } return $object; }
有了服務容器之後,咱們就可使用服務容器來存儲處理請求中須要的服務,並實現服務中的依賴自動注入。不過首先咱們須要將Request、Response單例作修改,由於服務容器對單例的管理,是經過$shared屬性進行設置的。因此Request、Response要可以被實例化,才能保存到容器的$bindings數組中:
class Request { public function __construct() { } public function get($key) { return $_GET[$key] ? $_GET[$key] : ''; } public function post($key) { return $_POST[$key] ? $_POST[$key] : ''; } } class Response { public function __construct() { } public function json($data) { return json_encode($data); } }
咱們再來看使用容器後處理用戶請求的源代碼:
include_once 'Container.php'; include_once 'Request.php'; include_once 'Response.php'; include_once 'ExceptionHandler.php'; $app = new Container(); //綁定錯誤處理 $app->bind('exception', 'ExceptionHandler'); //將請求、響應單例組件添加到容器中 $app->singleton('request', 'Request'); $app->singleton('response', 'Response'); //解析錯誤處理 $app->make('exception'); //用戶邏輯 class UserLogic { public $app = null; public function __construct(Container $app) { $this->app = $app; } public function getUserList() { if ($this->app->make('request')->get('path') == 'userlist') { $userList = [ ['name' => '張三', 'age' => 18], ['name' => '李四', 'age' => 22] ]; return $this->app->make('response')->json($userList); } } } try { $userLogic = new UserLogic($app); echo $userLogic->getUserList(); } catch (\Error $e) { var_dump($e); exit(); }
咱們仍是按照以前的步驟,使用容器將錯誤處理類綁定到容器中,而後解析出來使用。使用singleton方法將Request和Response類綁定到容器中,類型是單例。這樣咱們管理服務模塊、實現依賴注入這些問題全都交給容器來作就行了。咱們想要什麼樣的服務,就向容器中添加,在須要使用的時候,就利用容器解析使用就能夠了。lumen框架中的服務容器是全局的,不須要像例子中同樣,手動注入到邏輯代碼中使用。
對於lumen框架來說,服務容器至關於發動機,綁定與解析框架啓動和運行生命週期中全部的服務。它的大體架構以下所示:
源碼中bind實現代碼以下:
public function bind($abstract, $concrete = null, $shared = false) { $this->dropStaleInstances($abstract); if (is_null($concrete)) { $concrete = $abstract; } if (! $concrete instanceof Closure) { $concrete = $this->getClosure($abstract, $concrete); } $this->bindings[$abstract] = compact('concrete', 'shared'); if ($this->resolved($abstract)) { $this->rebound($abstract); } }
從源碼中咱們可知:使用bind方法綁定服務,每次都會從新進行綁定(刪除原來的綁定,再從新綁定)。咱們類比服務容器中服務的綁定爲KV健值對。key爲接口名稱,而value爲具體的服務實現,之因此推薦使用接口名稱做爲key,是由於只要開發者遵循相關的接口約束規範,就能夠對服務進行拓展和改進,這也是面向接口編程比較新穎之處。另外咱們能夠看到bind方法核心實現方法是調用rebound方法。
bindif方法核心是調用bind方法,只不過對容器是否綁定服務作了一個判斷:
public function bindIf($abstract, $concrete = null, $shared = false) { if (! $this->bound($abstract)) { $this->bind($abstract, $concrete, $shared); } }
singleton是bind方法的一種特例,shared=true表示爲單例綁定:
public function singleton($abstract, $concrete = null) { $this->bind($abstract, $concrete, true); }
instance是綁定對象實例到容器中(不用使用make進行解析了):
public function instance($abstract, $instance) { $this->removeAbstractAlias($abstract); $isBound = $this->bound($abstract); unset($this->aliases[$abstract]); $this->instances[$abstract] = $instance; if ($isBound) { $this->rebound($abstract); } return $instance; }
數組綁定是Container類繼承了ArrayAccess接口,在offsetSet中調用了bind方法進行註冊:
public function offsetSet($key, $value) { $this->bind($key, $value instanceof Closure ? $value : function () use ($value) { return $value; }); }
extend方法實現了當原來的類註冊或者實例化出來後,對其進行拓展:
public function extend($abstract, Closure $closure) { $abstract = $this->getAlias($abstract); if (isset($this->instances[$abstract])) { $this->instances[$abstract] = $closure($this->instances[$abstract], $this); $this->rebound($abstract); } else { $this->extenders[$abstract][] = $closure; if ($this->resolved($abstract)) { $this->rebound($abstract); } } }
Context綁定是針對於兩個類使用同一個接口,可是咱們在類中注入了不一樣的實現,這時候咱們就須要使用when方法了:
public function when($concrete) { $aliases = []; foreach (Arr::wrap($concrete) as $c) { $aliases[] = $this->getAlias($c); } return new ContextualBindingBuilder($this, $aliases); }
繼續看ContextualBindingBuilder類的源碼咱們知道,上下文綁定的基本思路就是$this->app->when()->needs()->give();
好比有幾個控制器分別依賴IlluminateContractsFilesystemFilesystem的不一樣實現:
$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)); });//須要依賴注入
有一些場景,咱們但願當接口改變之後對已實例化的對象從新作一些改變,這就是rebinding 函數的用途:
public function rebinding($abstract, Closure $callback) { $this->reboundCallbacks[$abstract = $this->getAlias($abstract)][] = $callback; if ($this->bound($abstract)) { return $this->make($abstract); } }
在服務容器解析以前,Lumen框架會將經常使用的服務起一些別名,方便系統Facade方法調用和解析。
public function withAliases($userAliases = []) { $defaults = [ 'Illuminate\Support\Facades\Auth' => 'Auth', 'Illuminate\Support\Facades\Cache' => 'Cache', 'Illuminate\Support\Facades\DB' => 'DB', 'Illuminate\Support\Facades\Event' => 'Event', 'Illuminate\Support\Facades\Gate' => 'Gate', 'Illuminate\Support\Facades\Log' => 'Log', 'Illuminate\Support\Facades\Queue' => 'Queue', 'Illuminate\Support\Facades\Route' => 'Route', 'Illuminate\Support\Facades\Schema' => 'Schema', 'Illuminate\Support\Facades\Storage' => 'Storage', 'Illuminate\Support\Facades\URL' => 'URL', 'Illuminate\Support\Facades\Validator' => 'Validator', ]; if (! static::$aliasesRegistered) { static::$aliasesRegistered = true; $merged = array_merge($defaults, $userAliases); foreach ($merged as $original => $alias) { class_alias($original, $alias); } } } ... protected function registerContainerAliases() { $this->aliases = [ 'Illuminate\Contracts\Foundation\Application' => 'app', 'Illuminate\Contracts\Auth\Factory' => 'auth', 'Illuminate\Contracts\Auth\Guard' => 'auth.driver', 'Illuminate\Contracts\Cache\Factory' => 'cache', 'Illuminate\Contracts\Cache\Repository' => 'cache.store', 'Illuminate\Contracts\Config\Repository' => 'config', 'Illuminate\Container\Container' => 'app', 'Illuminate\Contracts\Container\Container' => 'app', 'Illuminate\Database\ConnectionResolverInterface' => 'db', 'Illuminate\Database\DatabaseManager' => 'db', 'Illuminate\Contracts\Encryption\Encrypter' => 'encrypter', 'Illuminate\Contracts\Events\Dispatcher' => 'events', 'Illuminate\Contracts\Hashing\Hasher' => 'hash', 'log' => 'Psr\Log\LoggerInterface', 'Illuminate\Contracts\Queue\Factory' => 'queue', 'Illuminate\Contracts\Queue\Queue' => 'queue.connection', 'request' => 'Illuminate\Http\Request', 'Laravel\Lumen\Routing\Router' => 'router', 'Illuminate\Contracts\Translation\Translator' => 'translator', 'Laravel\Lumen\Routing\UrlGenerator' => 'url', 'Illuminate\Contracts\Validation\Factory' => 'validator', 'Illuminate\Contracts\View\Factory' => 'view', ]; } ......
lumen服務容器中經過alias方法添加服務別名:
public function alias($abstract, $alias) { $this->aliases[$alias] = $abstract; $this->abstractAliases[$abstract][] = $alias; }
經過getAlias得到服務的別名:
public function getAlias($abstract) { if (! isset($this->aliases[$abstract])) { return $abstract; } if ($this->aliases[$abstract] === $abstract) { throw new LogicException("[{$abstract}] is aliased to itself."); } return $this->getAlias($this->aliases[$abstract]);
經過getAlias咱們知道,服務別名是支持遞歸設置的。
服務容器解析一個對象時會觸發resolving和afterResolving函數。分別在以前以後觸發:
public function resolving($abstract, Closure $callback = null) { if (is_string($abstract)) { $abstract = $this->getAlias($abstract); } if (is_null($callback) && $abstract instanceof Closure) { $this->globalResolvingCallbacks[] = $abstract; } else { $this->resolvingCallbacks[$abstract][] = $callback; } } public function afterResolving($abstract, Closure $callback = null) { if (is_string($abstract)) { $abstract = $this->getAlias($abstract); } if ($abstract instanceof Closure && is_null($callback)) { $this->globalAfterResolvingCallbacks[] = $abstract; } else { $this->afterResolvingCallbacks[$abstract][] = $callback; } }
服務容器中有一些裝飾函數,wrap裝飾call,factory裝飾make:
public function call($callback, array $parameters = [], $defaultMethod = null) { return BoundMethod::call($this, $callback, $parameters, $defaultMethod); } ...... public function wrap(Closure $callback, array $parameters = []) { return function () use ($callback, $parameters) { return $this->call($callback, $parameters); }; }
服務容器的解析方法和函數以前已經說過,有幾種經常使用的方法,這裏就再也不一一贅述了。
能夠服務容器中flush()方法用於清空容器中全部的服務:
public function flush() { $this->aliases = []; $this->resolved = []; $this->bindings = []; $this->instances = []; $this->abstractAliases = []; }
Lumen中的服務容器源碼實現很是複雜,可是對其工做原理了解清楚以後,看起來也就有些頭緒了,每一個函數所作的工做也能夠結合註釋和源碼進行理解了。