如何實現Laravel的服務容器
如何實現服務容器(Ioc Container)
1. 容器的本質
- 服務容器本身就是一個數組,鍵名就是服務名,值就是服務。
- 服務可以是一個原始值,也可以是一個物件,可以說是任意資料。
- 服務名可以是自定義名,也可以是物件的類名,也可以是介面名。
// 服務容器 $container = [ // 原始值 'text' => '這是一個字串', // 自定義服務名 'customName' => new StdClass(), // 使用類名作為服務名 'StdClass' => new StdClass(), // 使用介面名作為服務名 'Namespace\\StdClassInterface' => new StdClass(), ]; // ----------- ↓↓↓↓示例程式碼↓↓↓↓ ----------- // // 繫結服務到容器 $container['standard'] = new StdClass(); // 獲取服務 $standard = $container['standard']; var_dump($standard);
2. 封裝成類
為了方便維護,我們把上面的陣列封裝到類裡面。
$instances
還是上面的容器陣列。我們增加兩個方法,instance
用來繫結服務,get
用來從容器中獲取服務。
class BaseContainer { // 已繫結的服務 protected $instances = []; // 繫結服務 public function instance($name, $instance) { $this->instances[$name] = $instance; } // 獲取服務 public function get($name) { return isset($this->instances[$name]) ? $this->instances[$name] : null; } } // ----------- ↓↓↓↓示例程式碼↓↓↓↓ ----------- // $container = new BaseContainer(); // 繫結服務 $container->instance('StdClass', new StdClass()); // 獲取服務 $stdClass = $container->get('StdClass'); var_dump($stdClass);
3. 按需例項化
現在我們在繫結一個物件服務的時候,就必須要先把類例項化,如果繫結的服務沒有被用到,那麼類就會白白例項化,造成效能浪費。
為了解決這個問題,我們增加一個bind
函式,它支援繫結一個回撥函式,在回撥函式中例項化類。這樣一來,我們只有在使用服務時,才回調這個函式,這樣就實現了按需例項化。
這時候,我們獲取服務時,就不只是從陣列中拿到服務並返回了,還需要判斷如果是回撥函式,就要執行回撥函式。所以我們把get
方法的名字改成make
。意思就是生產一個服務,這個服務可以是已繫結的服務,也可以是已繫結的回撥函式,也可以是一個類名,如果是類名,我們就直接例項化該類並返回。
然後,我們增加一個新陣列$bindings
,用來儲存繫結的回撥函式。然後我們把bind
方法改一下,判斷下$instance
如果是一個回撥函式,就放到$bindings
陣列,否則就用make
方法例項化類。
class DeferContainer extend BaseContainer { // 已繫結的回撥函式 protected $bindings = []; // 繫結服務 public function bind($name, $instance) { if ($instance instanceof Closure) { // 如果$instance是一個回撥函式,就繫結到bindings。 $this->bindings[$name] = $instance; } else { // 呼叫make方法,建立例項 $this->instances[$name] = $this->make($name); } } // 獲取服務 public function make($name) { if (isset($this->instances[$name])) { return $this->instances[$name]; } if (isset($this->bindings[$name])) { // 執行回撥函式並返回 $instance = call_user_func($this->bindings[$name]); } else { // 還沒有繫結到容器中,直接new. $instance = new $name(); } return $instance; } } // ----------- ↓↓↓↓示例程式碼↓↓↓↓ ----------- // $container = new DeferContainer(); // 繫結服務 $container->bind('StdClass', function () { echo "我被執行了\n"; return new StdClass(); }); // 獲取服務 $stdClass = $container->make('StdClass'); var_dump($stdClass);
StdClass
這個服務繫結的是一個回撥函式,在回撥函式中才會真正的例項化類。如果沒有用到這個服務,那回調函式就不會被執行,類也不會被例項化。
4. 單例
從上面的程式碼中可以看出,每次呼叫make
方法時,都會執行一次回撥函式,並返回一個新的類例項。但是在某些情況下,我們希望這個例項是一個單例,無論make
多少次,只例項化一次。
這時候,我們給bind
方法增加第三個引數$shared
,用來標記是否是單例,預設不是單例。然後把回撥函式和這個標記都存到$bindings
數組裡。
為了方便繫結單例服務,再增加一個新的方法singleton
,它直接呼叫bind
,並且$shared
引數強制為true
。
對於make
方法,我們也要做修改。在執行$bindings
裡的回撥函式以後,做一個判斷,如果之前繫結時標記的shared
是true
,就把回撥函式返回的結果儲存到$instances
裡。由於我們是先從$instances
裡找服務,所以這樣下次再make
的時候就會直接返回,而不會再次執行回撥函式。這樣就實現了單例的繫結。
class SingletonContainer extends DeferContainer { // 繫結服務 public function bind($name, $instance, $shared = false) { if ($instance instanceof Closure) { // 如果$instance是一個回撥函式,就繫結到bindings。 $this->bindings[$name] = [ 'callback' => $instance, // 標記是否單例 'shared' => $shared ]; } else { // 呼叫make方法,建立例項 $this->instances[$name] = $this->make($name); } } // 繫結一個單例 public function singleton($name, $instance) { $this->bind($name, $instance, true); } // 獲取服務 public function make($name) { if (isset($this->instances[$name])) { return $this->instances[$name]; } if (isset($this->bindings[$name])) { // 執行回撥函式並返回 $instance = call_user_func($this->bindings[$name]['callback']); if ($this->bindings[$name]['shared']) { // 標記為單例時,儲存到服務中 $this->instances[$name] = $instance; } } else { // 還沒有繫結到容器中,直接new. $instance = new $name(); } return $instance; } } // ----------- ↓↓↓↓示例程式碼↓↓↓↓ ----------- // $container = new SingletonContainer(); // 繫結服務 $container->singleton('anonymous', function () { return new class { public function __construct() { echo "我被例項化了\n"; } }; }); // 無論make多少次,只會例項化一次 $container->make('anonymous'); $container->make('anonymous'); // 獲取服務 $anonymous = $container->make('anonymous'); var_dump($anonymous)
上面的程式碼用singleton
綁定了一個名為anonymous
的服務,回撥函式裡返回了一個匿名類的例項。這個匿名類在被例項化時會輸出一段文字。無論我們make
多少次anonymous
,這個回撥函式只會被執行一次,匿名類也只會被例項化一次。
5. 自動注入
自動注入是Ioc容器的核心,沒有自動注入就無法做到控制反轉。
自動注入就是指,在例項化一個類時,用反射類來獲取__construct
所需要的引數,然後根據引數的型別,從容器中找到已繫結的服務。我們只要有了__construct
方法所需的所有引數,就能自動例項化該類,實現自動注入。
現在,我們增加一個build
方法,它只接收一個引數,就是類名。build
方法會用反射類來獲取__construct
方法所需要的引數,然後返回例項化結果。
另外一點就是,我們之前在呼叫make
方法時,如果傳的是一個未繫結的類,我們直接new了這個類。現在我們把未繫結的類交給build
方法來構建,因為它支援自動注入。
class InjectionContainer extends SingletonContainer { // 獲取服務 public function make($name) { if (isset($this->instances[$name])) { return $this->instances[$name]; } if (isset($this->bindings[$name])) { // 執行回撥函式並返回 $instance = call_user_func($this->bindings[$name]['callback']); if ($this->bindings[$name]['shared']) { // 標記為單例時,儲存到服務中 $this->instances[$name] = $instance; } } else { // 使用build方法構建此類 $instance = $this->build($name); } return $instance; } // 構建一個類,並自動注入服務 public function build($class) { $reflector = new ReflectionClass($class); $constructor = $reflector->getConstructor(); if (is_null($constructor)) { // 沒有建構函式,直接new return new $class(); } $dependencies = []; // 獲取建構函式所需的引數 foreach ($constructor->getParameters() as $dependency) { if (is_null($dependency->getClass())) { // 引數型別不是類時,無法從容器中獲取依賴 if ($dependency->isDefaultValueAvailable()) { // 查詢引數的預設值,如果有就使用預設值 $dependencies[] = $dependency->getDefaultValue(); } else { // 無法提供類所依賴的引數 throw new Exception('找不到依賴引數:' . $dependency->getName()); } } else { // 引數型別是類時,就用make方法構建該類 $dependencies[] = $this->make($dependency->getClass()->name); } } return $reflector->newInstanceArgs($dependencies); } } // ----------- ↓↓↓↓示例程式碼↓↓↓↓ ----------- // class Redis { } class Cache { protected $redis; // 建構函式中依賴Redis服務 public function __construct(Redis $redis) { $this->redis = $redis; } } $container = new InjectionContainer(); // 繫結Redis服務 $container->singleton(Redis::class, function () { return new Redis(); }); // 構建Cache類 $cache = $container->make(Cache::class); var_dump($cache);
6. 自定義依賴引數
現在有個問題,如果類依賴的引數不是類或介面,只是一個普通變數,這時候就無法從容器中獲取依賴引數了,也就無法例項化類了。
那麼接下來我們就支援一個新功能,在呼叫make
方法時,支援傳第二個引數$parameters
,這是一個數組,無法從容器中獲取的依賴,就從這個陣列中找。
當然,make
方法是用不到這個引數的,因為它不負責例項化類,它直接傳給build
方法。在build
方法尋找依賴的引數時,就先從$parameters
中找。這樣就實現了自定義依賴引數。
需要注意的一點是,build
方法是按照引數的名字來找依賴的,所以parameters
中的鍵名也必須跟__construct
中引數名一致。
class ParametersContainer extends InjectionContainer { // 獲取服務 public function make($name, array $parameters = []) { if (isset($this->instances[$name])) { return $this->instances[$name]; } if (isset($this->bindings[$name])) { // 執行回撥函式並返回 $instance = call_user_func($this->bindings[$name]['callback']); if ($this->bindings[$name]['shared']) { // 標記為單例時,儲存到服務中 $this->instances[$name] = $instance; } } else { // 使用build方法構建此類 $instance = $this->build($name, $parameters); } return $instance; } // 構建一個類,並自動注入服務 public function build($class, array $parameters = []) { $reflector = new ReflectionClass($class); $constructor = $reflector->getConstructor(); if (is_null($constructor)) { // 沒有建構函式,直接new return new $class(); } $dependencies = []; // 獲取建構函式所需的引數 foreach ($constructor->getParameters() as $dependency) { if (isset($parameters[$dependency->getName()])) { // 先從自定義引數中查詢 $dependencies[] = $parameters[$dependency->getName()]; continue; } if (is_null($dependency->getClass())) { // 引數型別不是類或介面時,無法從容器中獲取依賴 if ($dependency->isDefaultValueAvailable()) { // 查詢預設值,如果有就使用預設值 $dependencies[] = $dependency->getDefaultValue(); } else { // 無法提供類所依賴的引數 throw new Exception('找不到依賴引數:' . $dependency->getName()); } } else { // 引數型別是類時,就用make方法構建該類 $dependencies[] = $this->make($dependency->getClass()->name); } } return $reflector->newInstanceArgs($dependencies); } } // ----------- ↓↓↓↓示例程式碼↓↓↓↓ ----------- // class Redis { } class Cache { protected $redis; protected $name; protected $default; // 建構函式中依賴Redis服務和name引數,name的型別不是類,無法從容器中查詢 public function __construct(Redis $redis, $name, $default = '預設值') { $this->redis = $redis; $this->name = $name; $this->default = $default; } } $container = new ParametersContainer(); // 繫結Redis服務 $container->singleton(Redis::class, function () { return new Redis(); }); // 構建Cache類 $cache = $container->make(Cache::class, ['name' => 'test']); var_dump($cache);
提示:實際上,Laravel容器的build
方法並沒有第二個引數$parameters
,它是用類屬性來維護自定義引數。原理都是一樣的,只是實現方式不一樣。這裡為了方便理解,不引入過多概念。
7. 服務別名
別名可以理解成小名
、外號
。服務別名就是給已繫結的服務設定一些外號
,使我們通過外號
也能找到該服務。
這個就比較簡單了,我們增加一個新的陣列$aliases
,用來儲存別名。再增加一個方法alias
,用來讓外部註冊別名。
唯一需要我們修改的地方,就是在make
時,要先從$aliases
中找到真實的服務名。
class AliasContainer extends ParametersContainer { // 服務別名 protected $aliases = []; // 給服務繫結一個別名 public function alias($alias, $name) { $this->aliases[$alias] = $name; } // 獲取服務 public function make($name, array $parameters = []) { // 先用別名查詢真實服務名 $name = isset($this->aliases[$name]) ? $this->aliases[$name] : $name; return parent::make($name, $parameters); } } // ----------- ↓↓↓↓示例程式碼↓↓↓↓ ----------- // $container = new AliasContainer(); // 繫結服務 $container->instance('text', '這是一個字串'); // 給服務註冊別名 $container->alias('string', 'text'); $container->alias('content', 'text'); var_dump($container->make('string')); var_dump($container->make('content'));
8. 擴充套件繫結
有時候我們需要給已繫結的服務做一個包裝,這時候就用到擴充套件綁定了。我們先看一個實際的用法,理解它的作用後,才看它是如何實現的。
// 繫結日誌服務 $container->singleton('log', new Log()); // 對已繫結的服務再次包裝 $container->extend('log', function(Log $log){ // 返回了一個新服務 return new RedisLog($log); });
現在我們看它是如何實現的。增加一個$extenders
陣列,用來存放擴充套件器。再增加一個extend
方法,用來註冊擴充套件器。
然後在make
方法返回$instance
之前,按順序依次呼叫之前註冊的擴充套件器。
class ExtendContainer extends AliasContainer { // 存放擴充套件器的陣列 protected $extenders = []; // 給服務繫結擴充套件器 public function extend($name, $extender) { if (isset($this->instances[$name])) { // 已經例項化的服務,直接呼叫擴充套件器 $this->instances[$name] = $extender($this->instances[$name]); } else { $this->extenders[$name][] = $extender; } } // 獲取服務 public function make($name, array $parameters = []) { $instance = parent::make($name, $parameters); if (isset($this->extenders[$name])) { // 呼叫擴充套件器 foreach ($this->extenders[$name] as $extender) { $instance = $extender($instance); } } return $instance; } } // ----------- ↓↓↓↓示例程式碼↓↓↓↓ ----------- // class Redis { public $name; public function __construct($name = 'default') { $this->name = $name; } public function setName($name) { $this->name = $name; } } $container = new ExtendContainer(); // 繫結Redis服務 $container->singleton(Redis::class, function () { return new Redis(); }); // 給Redis服務繫結一個擴充套件器 $container->extend(Redis::class, function (Redis $redis) { $redis->setName('擴充套件器'); return $redis; }); $redis = $container->make(Redis::class); var_dump($redis->name);
9. 上下文繫結
有時侯我們可能有兩個類使用同一個介面,但希望在每個類中注入不同的實現,例如兩個控制器,分別為它們注入不同的Log
服務。
class ApiController { public function __construct(Log $log) { } } class WebController { public function __construct(Log $log) { } }
最終我們要用以下方式實現:
// 當ApiController依賴Log時,給它一個RedisLog $container->addContextualBinding('ApiController','Log',new RedisLog()); // 當WebController依賴Log時,給它一個FileLog $container->addContextualBinding('WebController','Log',new FileLog());
為了更直觀更方便更語義化的使用,我們把這個過程改成鏈式操作:
$container->when('ApiController') ->needs('Log') ->give(new RedisLog());
我們增加一個$context
陣列,用來儲存上下文。同時增加一個addContextualBinding
方法,用來註冊上下文繫結。以ApiController
為例,$context
的真實模樣是:
$context['ApiController']['Log'] = new RedisLog();
然後build
方法例項化類時,先從上下文中查詢依賴引數,就實現了上下文繫結。
接下來,看看鏈式操作是如何實現的。
首先定義一個類Context
,這個類有兩個方法,needs
和give
。
然後在容器中,增加一個when
方法,它返回一個Context
物件。在Context
物件的give
方法中,我們已經具備了註冊上下文所需要的所有引數,所以就可以在give
方法中呼叫addContextualBinding
來註冊上下文了。
class ContextContainer extends ExtendContainer { // 依賴上下文 protected $context = []; // 構建一個類,並自動注入服務 public function build($class, array $parameters = []) { $reflector = new ReflectionClass($class); $constructor = $reflector->getConstructor(); if (is_null($constructor)) { // 沒有建構函式,直接new return new $class(); } $dependencies = []; // 獲取建構函式所需的引數 foreach ($constructor->getParameters() as $dependency) { if (isset($this->context[$class]) && isset($this->context[$class][$dependency->getName()])) { // 先從上下文中查詢 $dependencies[] = $this->context[$class][$dependency->getName()]; continue; } if (isset($parameters[$dependency->getName()])) { // 從自定義引數中查詢 $dependencies[] = $parameters[$dependency->getName()]; continue; } if (is_null($dependency->getClass())) { // 引數型別不是類或介面時,無法從容器中獲取依賴 if ($dependency->isDefaultValueAvailable()) { // 查詢預設值,如果有就使用預設值 $dependencies[] = $dependency->getDefaultValue(); } else { // 無法提供類所依賴的引數 throw new Exception('找不到依賴引數:' . $dependency->getName()); } } else { // 引數型別是一個類時,就用make方法構建該類 $dependencies[] = $this->make($dependency->getClass()->name); } } return $reflector->newInstanceArgs($dependencies); } // 繫結上下文 public function addContextualBinding($when, $needs, $give) { $this->context[$when][$needs] = $give; } // 支援鏈式方式繫結上下文 public function when($when) { return new Context($when, $this); } } class Context { protected $when; protected $needs; protected $container; public function __construct($when, ContextContainer $container) { $this->when = $when; $this->container = $container; } public function needs($needs) { $this->needs = $needs; return $this; } public function give($give) { // 呼叫容器繫結依賴上下文 $this->container->addContextualBinding($this->when, $this->needs, $give); } } // ----------- ↓↓↓↓示例程式碼↓↓↓↓ ----------- // class Dog { public $name; public function __construct($name) { $this->name = $name; } } class Cat { public $name; public function __construct($name) { $this->name = $name; } } $container = new ContextContainer(); // 給Dog類設定上下文繫結 $container->when(Dog::class) ->needs('name') ->give('小狗'); // 給Cat類設定上下文繫結 $container->when(Cat::class) ->needs('name') ->give('小貓'); $dog = $container->make(Dog::class); $cat = $container->make(Cat::class); var_dump('Dog:' . $dog->name); var_dump('Cat:' . $cat->name);
10. 完整程式碼
class Container { // 已繫結的服務 protected $instances = []; // 已繫結的回撥函式 protected $bindings = []; // 服務別名 protected $aliases = []; // 存放擴充套件器的陣列 protected $extenders = []; // 依賴上下文 protected $context = []; // 繫結服務例項 public function instance($name, $instance) { $this->instances[$name] = $instance; } // 繫結服務 public function bind($name, $instance, $shared = false) { if ($instance instanceof Closure) { // 如果$instance是一個回撥函式,就繫結到bindings。 $this->bindings[$name] = [ 'callback' => $instance, // 標記是否單例 'shared' => $shared ]; } else { // 呼叫make方法,建立例項 $this->instances[$name] = $this->make($name); } } // 繫結一個單例 public function singleton($name, $instance) { $this->bind($name, $instance, true); } // 給服務繫結一個別名 public function alias($alias, $name) { $this->aliases[$alias] = $name; } // 給服務繫結擴充套件器 public function extend($name, $extender) { if (isset($this->instances[$name])) { // 已經例項化的服務,直接呼叫擴充套件器 $this->instances[$name] = $extender($this->instances[$name]); } else { $this->extenders[$name][] = $extender; } } // 獲取服務 public function make($name, array $parameters = []) { // 先用別名查詢真實服務名 $name = isset($this->aliases[$name]) ? $this->aliases[$name] : $name; if (isset($this->instances[$name])) { return $this->instances[$name]; } if (isset($this->bindings[$name])) { // 執行回撥函式並返回 $instance = call_user_func($this->bindings[$name]['callback']); if ($this->bindings[$name]['shared']) { // 標記為單例時,儲存到服務中 $this->instances[$name] = $instance; } } else { // 使用build方法構建此類 $instance = $this->build($name, $parameters); } if (isset($this->extenders[$name])) { // 呼叫擴充套件器 foreach ($this->extenders[$name] as $extender) { $instance = $extender($instance); } } return $instance; } // 構建一個類,並自動注入服務 public function build($class, array $parameters = []) { $reflector = new ReflectionClass($class); $constructor = $reflector->getConstructor(); if (is_null($constructor)) { // 沒有建構函式,直接new return new $class(); } $dependencies = []; // 獲取建構函式所需的引數 foreach ($constructor->getParameters() as $dependency) { if (isset($this->context[$class]) && isset($this->context[$class][$dependency->getName()])) { // 先從上下文中查詢 $dependencies[] = $this->context[$class][$dependency->getName()]; continue; } if (isset($parameters[$dependency->getName()])) { // 從自定義引數中查詢 $dependencies[] = $parameters[$dependency->getName()]; continue; } if (is_null($dependency->getClass())) { // 引數型別不是類或介面時,無法從容器中獲取依賴 if ($dependency->isDefaultValueAvailable()) { // 查詢預設值,如果有就使用預設值 $dependencies[] = $dependency->getDefaultValue(); } else { // 無法提供類所依賴的引數 throw new Exception('找不到依賴引數:' . $dependency->getName()); } } else { // 引數型別是一個類時,就用make方法構建該類 $dependencies[] = $this->make($dependency->getClass()->name); } } return $reflector->newInstanceArgs($dependencies); } // 繫結上下文 public function addContextualBinding($when, $needs, $give) { $this->context[$when][$needs] = $give; } // 支援鏈式方式繫結上下文 public function when($when) { return new Context($when, $this); } } class Context { protected $when; protected $needs; protected $container; public function __construct($when, Container $container) { $this->when = $when; $this->container = $container; } public function needs($needs) { $this->needs = $needs; return $this; } public function give($give) { // 呼叫容器繫結依賴上下文 $this->container->addContextualBinding($this->when, $this->needs, $give); } }