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');