首頁  >  文章  >  後端開發  >  教你如何手動建立PHP DI容器

教你如何手動建立PHP DI容器

藏色散人
藏色散人轉載
2021-12-01 14:23:063394瀏覽

關於依賴注入相信大家應該都經常接觸或至少有所耳聞,比較知名的框架都支持依賴注入,比如Java的Spring,PHP的Laravel、Symfony等。現在我開始手動實作一個簡陋的DI容器吧。

由開車開始

先開個車,為大家舉個栗子:

class Driver{    public function drive()
    {
        $car = new Car();        echo '老司机正在驾驶', $car->getCar(), PHP_EOL;
    }
}class Car{    protected $name = '普通汽车';    public function getCar()
    {        return $this->name;
    }
}

有兩個類,Driver和Car,老司機Driver有個方法driver,在呼叫的時候先得整輛車$car,然後發車。大多數同學都寫過這樣或類似的代碼,這樣的代碼單看沒啥毛病,挺正常的。但是,如果我要換車,開普通車撩不到妹。

class Benz extends Car{    protected $name = '奔驰';
}

這時候就需要做一個比較噁心的操作了,得改老司機的程式碼了。 (老司機:我做錯了什麼?換車子還得讓我重學駕照…)。因此我們需要把讓Car為外界注入,將Driver和Car解耦,不是老司機自己開車的時候還要自己去造車。於是就有了下面的結果

class Driver{    protected $car;    public function __construct(Car $car)
    {        $this->car = $car;
    }    public function drive()
    {        echo '老司机正在驾驶', $this->car->getCar(), PHP_EOL;
    }
}

此時Driver和Car兩個類別已經解耦,這兩個類別的依賴,依靠上層程式碼去管理。此時,老司機會這樣「開車」:

$car = new Car();
$driver = new Driver($car);
$driver->drive();

此時,我們建立Driver依賴的實例,並注入。上面的例子,我們實現了依賴注入,不過是手動的,寫起來感覺還是不爽。這麼繁重的活怎麼能手動來做呢,得讓程式自己做。於是乎,DI容器誕生。

依賴注入容器

依賴注入與IoC模式類似工廠模式,是解決呼叫者和被呼叫者依賴耦合關係的模式。它解決了物件之間的依賴關係,使得物件只依賴IoC/DI容器,不再直接相互依賴,實現鬆散耦合,然後在物件創建時,由IoC/DI容器將其依賴(Dependency)的物件注入( Inject)其內,這樣做可以最大程度實現鬆散耦合。依賴注入說白一點,就是容器將某個類別依賴的其他類別的實例注入到這個類別的實例中。

這段話可能說的有點抽象,回到剛才的例子吧。剛剛我手動完成了依賴注入,比較麻煩,如果一個大型的專案這樣做肯定會覺得很繁瑣,而且不夠優雅。因此我們需要有一位總管來代替我們去幹這個,這個總管就是容器。類別的依賴管理全部交給容器去完成。因此,一般來說容器是一個全局的對象,大家共有的。

做一個自己的DI容器

寫一個功能,我們首先需要分析問題,因此我們先要明白,對於一個簡單的DI容器需要哪些功能,這直接關係到我們程式碼的編寫。對於一個簡單的容器,至少需要滿足以下幾點:

  • 建立所需類別的實例

  • 完成依賴管理(DI)

  • 可以取得單例的實例

  • 全域唯一

##綜上,我們的容器類大約長這樣:

class Container{    /**
     * 单例
     * @var Container
     */
    protected static $instance;    /**
     * 容器所管理的实例
     * @var array
     */
    protected $instances = [];    private function __construct(){}  
    private function __clone(){}    /**
     * 获取单例的实例
     * @param string $class
     * @param array ...$params
     * @return object
     */
    public function singleton($class, ...$params)
    {}    /**
     * 获取实例(每次都会创建一个新的)
     * @param string $class
     * @param array ...$params
     * @return object
     */
    public function get($class, ...$params)
    {}    /**
     * 工厂方法,创建实例,并完成依赖注入
     * @param string $class
     * @param array $params
     * @return object
     */
    protected function make($class, $params = [])
    {}    /**
     * @return Container
     */
    public static function getInstance()
    {        if (null === static::$instance) {            static::$instance = new static();
        }        return static::$instance;
    }
}
大體骨架已經確定,接下來進入最核心的make方法:

protected function make($class, $params = []){  //如果不是反射类根据类名创建
  $class = is_string($class) ? new ReflectionClass($class) : $class;  //如果传的入参不为空,则根据入参创建实例
  if (!empty($params)) {    return $class->newInstanceArgs($params);
  }  //获取构造方法
  $constructor = $class->getConstructor();  //获取构造方法参数
  $parameterClasses = $constructor ? $constructor->getParameters() : [];  if (empty($parameterClasses)) {    //如果构造方法没有入参,直接创建
    return $class->newInstance();
  } else {    //如果构造方法有入参,迭代并递归创建依赖类实例
    foreach ($parameterClasses as $parameterClass) {
      $paramClass = $parameterClass->getClass();
      $params[] = $this->make($paramClass);
    }    //最后根据创建的参数创建实例,完成依赖的注入
    return $class->newInstanceArgs($params);
  }
}
為了容器的易用,我做了一些完善:

  • 實作ArrayAccess接口,使單例實例可以直接透過array的方式取得,如果該實例沒有,則建立

  • 重寫__get方法,更方便的取得

最終版:

class Container implements ArrayAccess{    /**
     * 单例
     * @var Container
     */
    protected static $instance;    /**
     * 容器所管理的实例
     * @var array
     */
    protected $instances = [];    private function __construct(){}    private function __clone(){}    /**
     * 获取单例的实例
     * @param string $class
     * @param array  ...$params
     * @return object
     */
    public function singleton($class, ...$params)
    {        if (isset($this->instances[$class])) {            return $this->instances[$class];
        } else {            $this->instances[$class] = $this->make($class, $params);
        }        return $this->instances[$class];
    }    /**
     * 获取实例(每次都会创建一个新的)
     * @param string $class
     * @param array  ...$params
     * @return object
     */
    public function get($class, ...$params)
    {        return $this->make($class, $params);
    }    /**
     * 工厂方法,创建实例,并完成依赖注入
     * @param string $class
     * @param array  $params
     * @return object
     */
    protected function make($class, $params = [])
    {        //如果不是反射类根据类名创建
        $class = is_string($class) ? new ReflectionClass($class) : $class;        //如果传的入参不为空,则根据入参创建实例
        if (!empty($params)) {            return $class->newInstanceArgs($params);
        }        //获取构造方法
        $constructor = $class->getConstructor();        //获取构造方法参数
        $parameterClasses = $constructor ? $constructor->getParameters() : [];        if (empty($parameterClasses)) {            //如果构造方法没有入参,直接创建
            return $class->newInstance();
        } else {            //如果构造方法有入参,迭代并递归创建依赖类实例
            foreach ($parameterClasses as $parameterClass) {
                $paramClass = $parameterClass->getClass();
                $params[] = $this->make($paramClass);
            }            //最后根据创建的参数创建实例,完成依赖的注入
            return $class->newInstanceArgs($params);
        }
    }    /**
     * @return Container
     */
    public static function getInstance()
    {        if (null === static::$instance) {            static::$instance = new static();
        }        return static::$instance;
    }    public function __get($class)
    {        if (!isset($this->instances[$class])) {            $this->instances[$class] = $this->make($class);
        }        return $this->instances[$class];
    }    public function offsetExists($offset)
    {        return isset($this->instances[$offset]);
    }    public function offsetGet($offset)
    {        if (!isset($this->instances[$offset])) {            $this->instances[$offset] = $this->make($offset);
        }        return $this->instances[$offset];
    }    public function offsetSet($offset, $value)
    {
    }    public function offsetUnset($offset) {        unset($this->instances[$offset]);
    }
}
現在藉助容器我們寫一下上面的程式碼:

$driver = $app->get(Driver::class);
$driver->drive();//output:老司机正在驾驶普通汽车复制代码
就這麼簡單,老司機就能發車。這裡預設注入的是Car的實例,如果需要開賓士,那隻需要這樣:

$benz = $app->get(Benz::class);
$driver = $app->get(Driver::class, $benz);
$driver->drive();//output:老司机正在驾驶奔驰复制代码
依照PSR-11的要求,依賴注入容器需要實作Psr\Container\ContainerInterface接口,這裡只是示範並未去實現,因為那需要引入Psr依賴庫,比較麻煩,其實也很簡單,只是多了幾個方法,有興趣的可以自己去了解下PSR-11的要求(傳送門)。

這裡只是實作了一個非常簡陋的DI容器,實際上還需要考慮很多,而且這裡的容器功能上還很簡陋。還有一些坑沒處理,像是出現循環依賴怎麼處理、延遲載入的機制…

這裡只是我週末閒暇練手的一點記錄,大家如果有興趣可以閱讀以下Laravel或Symfony容器那塊的原始碼,或者了解Spring的容器。後續有空我也會繼續完善。 【推薦學習:《

PHP影片教學》】

以上是教你如何手動建立PHP DI容器的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:csdn.net。如有侵權,請聯絡admin@php.cn刪除