Реализация контейнера внедрения зависимостей PHP

задняя часть .NET PHP контейнер

0x00 Предисловие

Просматривая документацию Laravel, я обнаружил, что следующая глава руководства по началу работы посвящена базовой архитектуре, что является просто убеждением для моих одноклассников, которые читают шаг за шагом. Различные понятия, которые не были затронуты ранее, вызывают у людей головокружение, и контейнер является одним из них. Однако, прочитав несколько статей, я постепенно понял роль контейнеров, поэтому резюмирую.

0x01 Зачем нужны контейнеры?

Этот вопрос можно и можно заменить вопросом «Какую проблему решает контейнер?». Перед этим нам нужно понять концепцию внедрения зависимостей, вы можете взглянуть на эту статью:Кратко объясните, что такое внедрение зависимостей и инверсия управления.. Мы столкнемся с проблемой при внедрении зависимостей. Здесь я объясню это на примере кода. Код выглядит следующим образом:

class Bread
{
}

class Bacon
{
}

class Hamburger
{
    protected $materials;

    public function __construct(Bread $bread, Bacon $bacon)
    {
        $this->materials = [$bread, $bacon];
    }
}

class Cola
{
}

class Meal
{
    protected $food;

    protected $drink;

    public function __construct(Hamburger $hamburger, Cola $cola)
    {
        $this->food  = $hamburger;
        $this->drink = $cola;
    }
}

Приведенный выше фрагмент кода, реализованный в соответствии с внедрением зависимостей, Мы видим, что класс Meal зависит от гамбургера и колы, а гамбургер зависит от хлеба и бекона. Эффект слабой связи может быть достигнут за счет внедрения зависимостей, но это также делает создание экземпляра класса с несколькими зависимостями очень проблематичным.Следующий код является примером создания экземпляра класса пакета:

$bread = new Bread();
$bacon = new Bacon();

$hamburger = new Hamburger($bread, $bacon);
$cola = new Cola();

$meal = new Meal($hamburger, $cola);

Видно, что для того, чтобы получить объект пакета, нам нужно сначала создать экземпляры зависимостей объекта.Если зависимости все еще существуют, нам также нужно создать экземпляры зависимостей зависимостей... Чтобы решить эту проблему, появился контейнер. Позиционирование контейнера: «Инструменты для управления зависимостями классов и выполнения внедрения зависимостей». Через контейнер мы можем автоматизировать процесс инстанцирования, например, напрямую получить объект пакета одной строкой кода:

$container->get(Meal::class);

0x01 Реализация простых контейнеров

Следующий код представляет собой реализацию простого контейнера:

class Container
{
    /**
     * @var Closure[]
     */
    protected $binds = [];

    /**
     * Bind class by closure.
     *
     * @param string $class
     * @param Closure $closure
     * @return $this
     */
    public function bind(string $class, Closure $closure)
    {
        $this->binds[$class] = $closure;

        return $this;
    }

    /**
     * Get object by class
     *
     * @param string $class
     * @param array $params
     * @return object
     */
    public function make(string $class, array $params = [])
    {
        if (isset($this->binds[$class])) {
            return ($this->binds[$class])->call($this, $this, ...$params);
        }

        return new $class(...$params);
    }
}

Этот контейнер имеет только два методаbindиmake,bindметод связывает имя класса с замыканием, а затемmakeМетод выполнит замыкание, соответствующее указанному имени класса, и вернет возвращаемое значение замыкания. Мы углубляем наше понимание с помощью контейнеров:

$container = new Container();

$container->bind(Hamburger::class, function (Container $container) {
    $bread = $container->make(Bread::class);
    $bacon = $container->make(Bacon::class);

    return new Hamburger($bread, $bacon);
});

$container->bind(Meal::class, function (Container $container) {
    $hamburger = $container->make(Hamburger::class);
    $cola      = $container->make(Cola::class);
    return new Meal($hamburger, $cola);
});

// 输出 Meal
echo get_class($container->make(Meal::class));

Из приведенного выше примера мы можем знать, чтоbindМетод передает замыкание, которое «возвращает созданный объект, соответствующий имени класса», и замыкание также получает контейнер в качестве параметра, поэтому мы также можем использовать контейнер для получения зависимостей внутри замыкания. Приведенный выше код кажется более эффективным, чем использованиеnewКлючевые слова тоже сложны, но на самом деле для каждого класса нам нужно толькоbindТолько раз. Каждый раз, когда вам понадобится объект в будущем, используйте его напрямуюmakeМетода достаточно, и он точно сэкономит много кода в нашем проекте.

0x02 Усилить контейнер отражением

Официальный справочник «Отражение»php.net/manual/this/no…

В примере с простым контейнером выше нам также нужно передатьbindМетод пишет созданный «скрипт», поэтому давайте представим, есть ли способ напрямую сгенерировать нужный нам экземпляр? На самом деле, через «отражение» и «класс подсказки типа», задающий параметры в конструкторе, мы можем добиться функции автоматического разрешения зависимостей. Поскольку посредством отражения мы можем получить параметры и типы параметров, требуемые указанным конструктором класса, наш контейнер может автоматически разрешать эти зависимости. Пример кода выглядит следующим образом:

/**
 * Get object by class
 *
 * @param string $class
 * @param array $params
 * @return object
 */
public function make(string $class, array $params = [])
{
    if (isset($this->binds[$class])) {
        return ($this->binds[$class])->call($this, $this, ...$params);
    }

    return $this->resolve($class);
}

/**
 * Get object by reflection
 *
 * @param $abstract
 * @return object
 * @throws ReflectionException
 */
protected function resolve($abstract)
{
    // 获取反射对象
    $constructor = (new ReflectionClass($abstract))->getConstructor();
    // 构造函数未定义,直接实例化对象
    if (is_null($constructor)) {
        return new $abstract;
    }
    // 获取构造函数参数
    $parameters = $constructor->getParameters();
    $arguments  = [];
    foreach ($parameters as $parameter) {
        // 获得参数的类型提示类
        $paramClassName = $parameter->getClass()->name;
        // 参数没有类型提示类,抛出异常
        if (is_null($paramClassName)) {
            throw new Exception('Fail to get instance by reflection');
        }
        // 实例化参数
        $arguments[] = $this->make($paramClassName);
    }

    return new $abstract(...$arguments);
}

Приведенный выше код основан только на изменении исходного класса контейнера.makeметод,bindsВыполнить после того, как указанное закрытие привязки класса не найдено в массивеresolveметод. вresolveМетод заключается в том, чтобы просто получить конструктор указанного класса посредством отражения, создать экземпляр его зависимостей и, наконец, создать экземпляр указанного класса. После этого шага нам действительно нужна только одна строка кода для создания экземпляра класса пакета, даже не конфигурация :-D.

$container->make(Meal::class);

Конечно, этот контейнер и сейчас достаточно прост, потому что если указанный класс зависит от скалярных значений (таких как: строки, массивы, числа и другие необъектные типы), то исключение будет выброшено напрямую, а частичные зависимости не может быть указан. Будет ошибка /(ㄒoㄒ)/~~, но эти функции доступны в некоторых зрелых библиотеках контейнеров. Если вам интересно, вы можете перейти к их исходным кодам, здесь я рекомендуюPipmleэтот проект.

0x03 Сводка

В этой статье в основном представлены сценарии применения контейнеров и реализован простой контейнер.Используя контейнеры, мы можем легко решить проблемы, вызванные внедрением зависимостей. Однако контейнер не лишен недостатков, поскольку в большинстве контейнеров используется технология отражения, что приведет к большому потреблению производительности, а инстанс IDE, сгенерированный косвенно через контейнер, часто не может распознать его тип, поэтому автоматических подсказок не будет (Это можно решить, написав комментарии к документации). Однако лично я считаю, что внедрение контейнеров на самом деле больше плюсов, чем минусов (сугубо личное ощущение)!

Реализация контейнера внедрения зависимостей PHP — исходный адрес