Container 機制是一種依賴注入的設計模式,它允許您將對象的創建和解析分離。這意味著您可以在應用程序中創建一個容器,然後使用容器來解析對象,而不是直接創建它們。這樣做的好處是,您可以更輕鬆地管理對象之間的依賴關係,並且可以更輕鬆地進行單元測試。
Container 機制通常由一個容器類別實現,該類別包含一個綁定數組,用於存儲抽象類別和具體類別之間的映射。當您需要解析一個對象時,容器會查找該對象的綁定,然後使用綁定中指定的具體類別或 Closure 創建該對象。
Container 機制還支持單例模式。如果您需要在應用程序中共享一個對象,則可以使用 singleton 方法來綁定抽象類別到具體類別或 Closure,並將 shared 參數設置為 true。這樣做可以確保每次解析該對象時都返回同一個實例。
使用 ReflectionClass 來解析對象,並使用 ReflectionParameter 來獲取對象的構造函數的參數列表。然後,它遞歸地解析每個參數的依賴關係,直到所有依賴關係都被解析為止。如果對象沒有構造函數,則直接返回一個新的對象實例。否則,它會使用 ReflectionClass::newInstanceArgs() 方法創建一個新的對象實例,並傳遞解析後的依賴關係作為參數。這種方法的好處是,它可以更輕鬆地管理對象之間的依賴關係,並且可以更輕鬆地進行單元測試。
基礎 Container 實作
<?php
require_once 'Container.php';
class Foo {}
$container = new Container;
// 綁定 foo => new Foo
$container->bind('foo', function ($container) {
return new Foo;
});
$foo1 = $container->make('foo');
var_dump($foo1); // object(Foo)#3 (0) { }
具體來說是怎麼實現這種綁定 Container,以下會逐步實現:
實現 bind
class Container
{
protected $bindings = [];
// 綁定一個實例進入 container
public function bind($abstract, $concrete = null, $shared = false)
{
// $shared 參數用於指示是否應該返回同一個實例。
// 如果 shared 參數設置為 true,則返回同一個實例。
// 這對於需要共享的對象非常有用,例如資料庫連接或日誌記錄器。
// 如果 shared 參數設置為 false,則每次調用 make 方法時都會創建一個新的實例。
$this->bindings[$abstract] = compact('concrete', 'shared');
// array(1) { ["foo"]=> array(2) { ["concrete"]=> object(Closure)#1 (1) { ["parameter"]=> array(1) { ["$container"]=> string(10) "" } } ["shared"]=> bool(false) } }
}
....
這邊可以看到我們已經將 key: foo 與 closure => new Foo 綁定在 bindings array 內,那麼接下來如何讓我們在呼叫的時候直接調用到 Foo 這個類呢?
實現 make
public function make($abstract)
{
// 檢查是否有綁定在目前容器內
if (isset($this->bindings[$abstract])) {
// 有的話取得對應對象
$concrete = $this->bindings[$abstract]['concrete'];
// 檢查是否需要返回同一個實例<singleton>
if ($this->bindings[$abstract]['shared']) {
static $shared = []; // 將設定為共享的實例放到這邊防止多次調用提升性能
if (!isset($shared[$abstract])) {
// function($container) {
// 這邊可以取得當前 container bindings
// return Foo;
// }
$shared[$abstract] = $concrete($this);
}
//$shared = ['foo' => function($container) { return new Foo; }];
// 後面都是從 $shared 取對應的對象
return $shared[$abstract];
}
// function($container) { return new Foo; };
return $concrete($this);
}
throw new Exception("{$abstract} is not bound in the container.");
}
短短的幾行就實現了 Container 最核心的機制,非常厲害,接下來依照這樣子的思路去拓展 $shared
來達到 singleton:
實現 singleton
public function singleton($abstract, $concrete)
{
$this->bind($abstract, $concrete, true);
}
可以看到 singleton 內是把 $shared
修改為 true,那麼這個參數對後面的的 make
會有什麼效果:
// 檢查是否需要返回同一個實例<singleton>
if ($this->bindings[$abstract]['shared']) {
static $shared = []; // 將設定為共享的實例放到這邊防止多次調用提升性能
if (!isset($shared[$abstract])) {
// function($container) {
// 這邊可以取得當前 container bindings
// return Foo;
// }
$shared[$abstract] = $concrete($this);
}
//$shared = ['foo' => function($container) { return new Foo; }];
// 後面都是從 $shared 取對應的對象
return $shared[$abstract];
}
這邊可以看到如果配置為 true,會將當前 object 存入 static $shared 這個陣列內,如果又有呼叫的話則會直接返回先前的實例 $shared[$abstract] 來達到 singleton 的行為。
這邊可以作一個簡單的測試來確保是否是同個 object:
$container->bind('foo', function ($container) {
return new Foo;
});
$foo1 = $container->make('foo');
$foo2 = $container->make('foo');
var_dump($foo1, $foo2); // object(Foo)#3 (0) { } object(Foo)#5 (0) { }
var_dump($foo1 === $foo2); // false
這邊可以看到如果用 bind 的方式他在每次呼叫 make 時則會是 new 一個新的物件,但如果使用 singleton 行為則會是:
$container->singleton('bar', function () {
return new Foo;
});
$bar1 = $container->make('bar');
$bar2 = $container->make('bar');
var_dump($bar1, $bar2); // object(Foo)#5 (0) { } object(Foo)#5 (0) { }
var_dump($bar1 === $bar2); // true
接下來想要支持非 closure,如傳入 Bar 類也要能自動支持這樣子的方法,像是:
$container->bind('foo', Foo::class);
$foo1 = $container->make('foo');
而 bind 則需要加入判斷:這個邏輯則是將傳入的 concrete (Foo::class) 打包成 closure 並綁定到 container 內:
public function bind($abstract, $concrete = null, $shared = false)
{
// 如果不是閉包就生成
if (!$concrete instanceof Closure) {
$concrete = function ($container) use ($concrete) {
return $container->build($concrete);
};
}
// .....
這邊會透過 ReflectionClass
解析對象之後,取得類的 constructor 跟 parameters,並取得 instances:
protected function build($concrete)
{
// 使用 ReflectionClass 來解析對象,並使用 ReflectionParameter 來獲取對象的構造函數的參數列表。然後,它遞歸地解析每個參數的依賴關係,直到所有依賴關係都被解析為止。
// 如果對象沒有構造函數,則直接返回一個新的對象實例。否則,它會使用 ReflectionClass::newInstanceArgs() 方法創建一個新的對象實例,並傳遞解析後的依賴關係作為參數。
// 這種方法的好處是,它可以更輕鬆地管理對象之間的依賴關係,並且可以更輕鬆地進行單元測試。
$reflector = new ReflectionClass($concrete);
// 如果不是可實例化的則跳出 exception
if (!$reflector->isInstantiable()) {
throw new Exception("{$concrete} is not instantiable.");
}
// 透過 reflection class 取對應的 constructor
$constructor = $reflector->getConstructor();
// 無 constructor 就 new concrete
if (is_null($constructor)) {
return new $concrete;
}
// 取得 constructor 的參數
$dependencies = $constructor->getParameters();
try {
// 取得依賴
$instances = $this->getDependencies($dependencies);
return $reflector->newInstanceArgs($instances);
} catch (Exception $e) {
throw new Exception("{$e->getMessage()}");
}
}
得到這些 parameters 跑檢查是否有預設值,並開始解析與 make 類:
protected function getDependencies($parameters)
{
$dependencies = [];
foreach ($parameters as $parameter) {
// 取得類
$dependency = $parameter->getType();
// 如果不是類,必須檢查是否有默認值
if (is_null($dependency)) {
// 透過反射檢查
if ($parameter->isDefaultValueAvailable()) {
// 取得預設值
$dependencies[] = $parameter->getDefaultValue();
} else {
throw new Exception("Can not resolve dependency {$parameter}.");
}
} else {
// 解析 bind 依賴
$dependencies[] = $this->resolveClass(strtolower($dependency->getName()));
}
}
return $dependencies;
}
protected function resolveClass($class)
{
return $this->make($class);
}
到此整個簡易的 Container 實作就完成了,接下來來測試功能的正確性:
class Foo {
public function __construct(protected Bar $bar, protected Baz $baz){}
}
class Bar {}
class Baz {}
// 依序將類綁定到 container 內
$container = new Container;
$container->bind('bar', Bar::class);
$container->bind('baz', Baz::class);
$container->bind('foo', Foo::class);
$foo1 = $container->make('foo'); //object(Foo)#11 (2) { ["bar":protected]=> object(Bar)#12 (0) { } ["baz":protected]=> object(Baz)#13 (0) { } }
總結
透過實作一個簡易的 Container 了解了關於這個東西的基礎理論,在下一篇當中會直接進入查看 Laravel Container 的機制並理解其設計概念,透過了解設計的機制進而對架構設計和代碼分層有更深一層的理解。
以下為完整實際代碼:
<?php
class Container
{
protected $bindings = [];
// 綁定一個實例進入 container
public function bind($abstract, $concrete = null, $shared = false)
{
// 如果不是閉包就生成
if (!$concrete instanceof Closure) {
$concrete = function ($container) use ($concrete) {
return $container->build($concrete);
};
}
// $shared 參數用於指示是否應該返回同一個實例。
// 如果 shared 參數設置為 true,則返回同一個實例。
// 這對於需要共享的對象非常有用,例如資料庫連接或日誌記錄器。
// 如果 shared 參數設置為 false,則每次調用 make 方法時都會創建一個新的實例。
$this->bindings[$abstract] = compact('concrete', 'shared');
}
public function singleton($abstract, $concrete)
{
$this->bind($abstract, $concrete, true);
}
public function make($abstract)
{
// 檢查是否有綁定在目前容器內
if (isset($this->bindings[$abstract])) {
// 有的話取得對應對象
$concrete = $this->bindings[$abstract]['concrete'];
// 檢查是否需要返回同一個實例
if ($this->bindings[$abstract]['shared']) {
static $shared = []; // 將設定為共享的實例放到這邊防止多次調用提升性能
if (!isset($shared[$abstract])) {
// function($container) {
// 這邊可以取得當前 container bindings
// return Foo;
// }
$shared[$abstract] = $concrete($this);
}
//$shared = ['foo' => function($container) { return new Foo; }];
// 後面都是從 $shared 取對應的對象
return $shared[$abstract];
}
// function($container) { return new Foo; };
return $concrete($this);
}
throw new Exception("{$abstract} is not bound in the container.");
}
protected function build($concrete)
{
// 使用 ReflectionClass 來解析對象,並使用 ReflectionParameter 來獲取對象的構造函數的參數列表。然後,它遞歸地解析每個參數的依賴關係,直到所有依賴關係都被解析為止。
// 如果對象沒有構造函數,則直接返回一個新的對象實例。否則,它會使用 ReflectionClass::newInstanceArgs() 方法創建一個新的對象實例,並傳遞解析後的依賴關係作為參數。
// 這種方法的好處是,它可以更輕鬆地管理對象之間的依賴關係,並且可以更輕鬆地進行單元測試。
$reflector = new ReflectionClass($concrete);
// 如果不是可實例化的則跳出 exception
if (!$reflector->isInstantiable()) {
throw new Exception("{$concrete} is not instantiable.");
}
// 透過 reflection class 取對應的 constructor
$constructor = $reflector->getConstructor();
// 無 constructor 就 new concrete
if (is_null($constructor)) {
return new $concrete;
}
// 取得 constructor 的參數
$dependencies = $constructor->getParameters();
try {
// 取得依賴
$instances = $this->getDependencies($dependencies);
return $reflector->newInstanceArgs($instances);
} catch (Exception $e) {
throw new Exception("{$e->getMessage()}");
}
}
protected function getDependencies($parameters)
{
$dependencies = [];
foreach ($parameters as $parameter) {
// 取得類
$dependency = $parameter->getType();
// 如果不是類,必須檢查是否有默認值
if (is_null($dependency)) {
// 透過反射檢查
if ($parameter->isDefaultValueAvailable()) {
// 取得預設值
$dependencies[] = $parameter->getDefaultValue();
} else {
throw new Exception("Can not resolve dependency {$parameter}.");
}
} else {
// 解析 bind 依賴
$dependencies[] = $this->resolveClass(strtolower($dependency->getName()));
}
}
return $dependencies;
}
protected function resolveClass($class)
{
return $this->make($class);
}
}
<?php
require_once 'Container.php';
class Foo {
public function __construct(protected Bar $bar, protected Baz $baz){}
}
class Bar {}
class Baz {}
$container = new Container;
$container->bind('bar', Bar::class);
$container->bind('baz', Baz::class);
$container->bind('foo', Foo::class);
// $container->bind('foo', function ($container) {
// return new Foo;
// });
$foo1 = $container->make('foo');