Если вы писали хоть сколько-нибудь сложный код, то наверняка сталкивались с зависимостями между классами. Эта статья поможет понять, как сделать работу с такими зависимостями чистой и управляемой.

Цель статьи — дать начинающим PHP-разработчикам практическое понимание того, как работает внедрение зависимостей (DI) и контейнер внедрения зависимостей (DI-контейнеры), а также показать, как эти принципы применяются в современных фреймворках.

Для понимания примеров необходимы знания базового синтаксиса php.

Полный код примеров можно посмотреть в репозитории.

Статья состоит из двух частей:

  • В первой части будут рассмотрены основные концепции внедрения зависимостей (DI), осознание и приведен пример реализации собственного DI-контейнера.

  • Во второй части будут рассмотрены реализации DI-контейнеров в популярных PHP-фреймворках: Symfony, Laravel и Yii3.

Часть 1. Как работает DI и DI-контейнер в PHP: понятный путь от нуля до autowiring.

Шаг 1. Пример корзины интернет-магазина.

Давайте предположим, перед вами стоит задача реализовать функциональность подсчёта стоимости корзины в интернет-магазине. В общем, задача простая, перебрать все товары в корзине, просуммировать их стоимость.

Базовые классы для реализации:

- Модель данных - CartItem. Описывает товар в корзине и управляет его количеством и стоимостью.

class CartItem
{
    public function __construct(
        public readonly int $id,
        private int $count,
        private int $price
    ) {}

    public function getCost(): int
    {
        return $this->price * $this->count;
    }
}

- Слой хранения - SimpleStorage. Представляет простое хранилище данных корзины в сессии. Отвечает за сохранение и загрузку списка товаров.

class SimpleStorage
{
    public function __construct(
        private readonly string $key
    ) {}

    /**
     * @return CartItem[]
     */
    public function load(): array
    {
        return isset($_SESSION[$this->key])
            ? unserialize($_SESSION[$this->key], ['allowed_classes' => [CartItem::class]])
            : [];
    }

    /**
     * @param CartItem[] $data
     */
    public function save(array $data): void
    {
        $_SESSION[$this->key] = serialize($data);
    }
}

- Слой логики расчёта - SimpleCalculator. Отвечает за вычисление общей стоимости всех товаров в корзине.

class SimpleCalculator
{
    /**
     * @param CartItem[] $items
     * @return int
     */
    public function getCost(array $items): int
    {
        $cost = 0;
        foreach ($items as $item) {
            $cost += $item->getCost();
        }
        return $cost;
    }
}

- Слой бизнес-логики - Cart.

Теперь необходимо реализовать функционал. Как это можно решить быстро? 1. Получить список товаров и хранилища. 2. Рассчитать общую стоимость корзины.

class Cart
{
    /** @var CartItem[] */
    private array $items = [];

    private bool $loaded = false;

    public function getCost(): int
    {
        $this->loadItems();

        return (new SimpleCalculator())->getCost($this->items);
    }

    private function loadItems(): void
    {
        if ($this->loaded) {
            return;
        }

        $this->items = (new SimpleStorage('cart'))->load();
        $this->loaded = true;
    }
}

Точка входа в приложение:

$cart = new Cart();
echo $cart->getCost() . PHP_EOL;

Проблема зависимостей

Как работает реализованный выше код? Потребовалось хранилище - создали new SimpleStorage и начали работать с хранилищем. Потребовался калькулятор - создали new SimpleCalculator и начали работать с калькулятором.

Класс Cart жестко зависит от конкретных реализаций SimpleStorage и SimpleCalculator, из-за этого возникнут проблемы при масштабировании и тестировании.

Получается, класс Cart содержит в себе и бизнес-логику и создает нужные зависимости.

Если, например, появится необходимость изменить хранилище SimpleStorage на DbStorage, или калькулятор SimpleCalculator на, скажем, DiscountCalculator, то придётся менять код внутри Cart, нарушая принцип открытости/закрытости (Open/Closed Principle, OCP) из SOLID.

Шаг 2. Получение зависимостей извне

Давайте улучшим код. Пусть зависимости, которые нужны компоненту Cart, передавались извне, а не создавались в нем.

class Cart
{
    /** @var CartItem[] */
    private array $items = [];
    
    private bool $loaded = false;

    public function __construct(
        private readonly SimpleCalculator $calculator,
        private readonly SimpleStorage $storage
    )
    {}

    public function getCost(): int
    {
        $this->loadItems();

        return $this->calculator->getCost($this->items);
    }

    private function loadItems(): void
    {
        if ($this->loaded) {
            return;
        }

        $this->items = $this->storage->load();
        $this->loaded = true;
    }
}

Также необходимо изменить код точки входа:

$cart = new Cart(new SimpleCalculator(), new SimpleStorage('cart'));
echo $cart->getCost() . PHP_EOL;

Сейчас мы получили явное внедрение зависимостей.

Cart больше не создаёт ни SimpleCalculator, ни SimpleStorage внутри себя. Эти компоненты передаются в конструктор, то есть ответственность за создание объектов перенесена наружу.

Внедрение зависимостей (dependency injection, DI) - грубо говоря, всё сводится к тому, что зависимости создаются вне объекта и передаются ему уже готовыми. Сам объект не отвечает за создание зависимостей, он просто использует их.

Есть несколько способов внедрения зависимостей: через отдельный метод-сеттер или через конструктор.

Внедрение зависимости через конструктор имеет преимущества:

  • Нельзя забыть передать зависимость в нуждающийся объект.

  • Зависимость легко увидеть, посмотрев на конструктор.

Теперь наша корзина стала чище.

Однако есть некоторые неудобства. В частности, наша Корзина жестко зависит от конкретной реализации хранилища SimpleStorage и подсчета итоговой суммы SimpleCalculator или их наследников, с измененным поведением (что уже удобнее для тестирования).

Однако при масштабировании нам нужно больше гибкости - именно поэтому следующим шагом станет использование интерфейсов.

Шаг 3. Зависимость от интерфейсов

А что, если мы решим хранить данные в БД? Или, например, хранить корзину не авторизованных пользователей в сессии, а авторизованных — в базе данных?

А если сумму считать с учетом какой-нибудь скидки?

Давайте изменим код класса Cart таким образом, чтобы он зависел не от конкретной реализации, а мог работать со всеми объектами, реализующими нужные интерфейсы. Для этого создадим интерфейсы:

interface StorageInterface
{
    /** 
     * @return CartItem[] 
     */
    public function load(): array;

    /**
     * @param CartItem[] $data 
     */
    public function save(array $data): void;
}

interface CalculatorInterface
{
    /**
     * @param CartItem[] $items
     * @return int
     */
    public function getCost(array $items): int;
}

Теперь класс SimpleStorage должен реализовать интерфейс StorageInterface, а класс SimpleCalculator - CalculatorInterface:

class SimpleStorage implements StorageInterface
{
    // Код не меняется.
}

class SimpleCalculator implements CalculatorInterface
{
    // Код не меняется.
}

Класс Cart:

class Cart
{
    public function __construct(
        private readonly CalculatorInterface $calculator,
        private readonly StorageInterface $storage
    )
    {}
    
    // Остальной код не меняется.
}

Теперь мы используем на полную полиморфизм, можем в любой момент поменять хранилище или калькулятор.

Также на этом шаге мы добились инверсии зависимостей.

Принцип Dependency Inversion Principle (DIP) — это принцип из SOLID, который гласит, что модули верхнего уровня не должны зависеть от модулей нижнего уровня — все должны зависеть от абстракций.

По сути, DI (Dependency Injection) — это один из подходов к реализации инверсии управления IoC (Inversion of Control), который помогает реализовать DIP (Dependency Inversion Principle).

Отступление

IoC (Inversion of Control) — это архитектурный принцип, при котором контроль за выполнением программы (или созданием объектов, управлением зависимостями, жизненным циклом) переносится от пользовательского кода к внешней системе (фреймворку, контейнеру и т. п.).

Также одним из подходов к реализации инверсии управления IoC (Inversion of Control) является SL (Service Locator) - когда объект сам запрашивает свои зависимости из специального реестра.

- При DI: зависимости создаются (или конфигурируются) где-то снаружи и передаются объекту;

- При SL: зависимости тоже создаются снаружи (например, заранее регистрируются в Service Locator-е), но объект сам знает, что ему нужно сходить в Service Locator и получить их. Например, клас Cart выглядел бы так:

class Cart {
    public function __construct(ServiceLocator $locator) {
        $this->storage = $locator->get(StorageInterface::class);
    }
}

Шаг 4. Создание простого DI контейнера

На предыдущих шагах мы добились того, что Cart больше не зависит от конкретных реализаций, а работает с абстракциями. Но остаётся важная проблема — управление зависимостями.

Предположим, нам необходимо использовать корзину в нескольких местах в коде, например:

  • в контроллере каталога товаров (чтобы показать сумму и количество),

  • на странице оформления заказа (checkout),

  • в тестах и фоновом анализе.

В каждом из этих случаев нам нужно создавать экземпляр Cart и передавать в него CalculatorInterface и StorageInterface, а они, в свою очередь, могут иметь свои зависимости.

Это приводит к «ручной сборке дерева зависимостей» — неудобно, сложно и легко ошибиться.

А что если нужен будет один и тот же экземпляр Cart в разных местах? Хотя из сессий и БД все равно будут одни и те же данные, н�� все же.

Было бы круто, если нужный нам объект создавался "на лету" там, где он нам потребуется.

Это и есть роль DI контейнера.

Требования к контейнеру внедрения зависимостей описаны в стандарте PSR-11: Container Interface.

Этот стандарт определяет минимальный контракт, которому должен соответствовать любой DI контейнер, чтобы быть совместимыми между библиотеками и фреймворками. А именно, он описывает методы: get() и has().

Давайте реализуем свой простой DI-контейнер и посмотрим, как он упростит нам жизнь.

Для этого создадим класс Container, который реализует Psr\Container\ContainerInterface из PSR-11.

Дополнительно кроме методов get() и set(), реализуем свой метод set, который будет регистрировать анонимную функцию (создания нужного объекта).

Контейнер по нашему требованию будет создавать и возвращать нужный объект:

class Container implements ContainerInterface
{
    private array $definitions = [];

    public function set($id, $callback): void
    {
        $this->definitions[$id] = $callback;
    }

    public function get($id)
    {
        if (!$this->has($id)) {
            throw new ServiceNotFoundException('Undefined service: ' . $id);
        }
        return call_user_func($this->definitions[$id], $this);
    }

    public function has(string $id): bool
    {
        return isset($this->definitions[$id]);
    }
}

class ServiceNotFoundException extends \Exception implements NotFoundExceptionInterface
{
}

Такой подход позволит регистрировать хоть сколько компонентов под разными именами, передавая только функции, которые их будет создавать, вместо того чтобы сразу их создавать.

Изменим код точки входа:

//___________ этот код можно вынести куда-нибудь в конфигурационный фа��л
$container = new Container();
$container->set(SimpleStorage::class, fn () => new SimpleStorage('cart'));

$container->set(SimpleCalculator::class, fn () => new SimpleCalculator());

$container->set(Cart::class, fn (Container $container) => new Cart(
    $container->get(SimpleCalculator::class),
    $container->get(SimpleStorage::class))
);
//___________

$cart = $container->get(Cart::class);
echo $cart->getCost() . PHP_EOL;

Получился простейший вариант получения зависимостей через контейнер. Мы регистрируем сервисы, используя в качестве идентификатора название полное название класса.

Требуемые объекты создаются только в тот момент, когда их запрашивают. Таким образом, можно зарегистрировать множество объектов и не бояться переполнения памяти.

Однако весь код по созданию объектов мы перенесли в анонимную функцию и регистрируем их. Это тоже не совсем удобно.

А было бы круто, если наш контейнер сам умел бы парсить конструктор класса перед созданием объекта, и если там указана какая-то зависимость, то автоматически создал бы.

Тогда при регистрации сервиса не нужно было бы передавать анонимную функцию.

Шаг 5. Автоматическое разрешение зависимостей (autowiring)

Реализовать autowiring можно с помощью рефлексии — она позволит контейнеру "заглянуть" в конструктор класса и автоматически создать все необходимые зависимости.

Давайте расширим наш контейнер: если сервис зарегистрирован как строка (имя класса), то создавать его через рефлексию, иначе — вызывать анонимную функцию.

class Container implements ContainerInterface
{
    private array $definitions = [];

    public function set(string $id, string|callable $callback): void
    {
        $this->definitions[$id] = $callback;
    }

    public function get($id)
    {
        if (!$this->has($id)) {
            throw new ServiceNotFoundException('Undefined service: ' . $id);
        }

        $definition = $this->definitions[$id];

        $component = is_string($definition)
            ? $this->make($definition)
            : $definition($this);

        if (!$component) {
            throw new ContainerException('Undefined component ' . $id);
        }

        return $component;
    }

    public function has(string $id): bool
    {
        return isset($this->definitions[$id]);
    }

    private function make(string $definition): ?object
    {
        if (!class_exists($definition)) {
            return null;
        }

        $reflection = new ReflectionClass($definition);
        $arguments = [];
        if (($constructor = $reflection->getConstructor()) !== null) {
            foreach ($constructor->getParameters() as $param) {
                $paramClass = $param->getType();

                $arguments[] = $paramClass ? $this->get($paramClass->getName()) : null;
            }
        }

        return $reflection->newInstanceArgs($arguments);
    }
}

PSR-11: Container Interface предусматривает, что идентификатором сервиса может быть любая допустимая PHP строка, состоящая как минимум из одного символа, которая однозначно идентифицирует элемент внутри контей��ера. На практике чаще всего идентификаторами выступают имена интерфейсов.

Давайте изменим код регистрации сервисов, в соответствии с последними улучшениями:

$container = new Container();
$container->set(StorageInterface::class, fn () => new SimpleStorage('cart'));
$container->set(CalculatorInterface::class, SimpleCalculator::class);
$container->set(Cart::class, Cart::class);

$cart = $container->get(Cart::class);
echo $cart->getCost() . PHP_EOL;

Теперь наш контейнер умеет автоматически разрешать зависимости с помощью рефлексии.

Но есть проблема: при каждом вызове $container->get(Cart::class) создается новый экземпляр класса Cart.

PSR-11: Container Interface предусматривает:

Два последовательных вызова get с одним и тем же идентификатором ДОЛЖНЫ возвращать одно и то же значение. Однако, в зависимости от конфигураций, могут быть возвращены разные значения, поэтому НЕ СТОИТ полагаться на получение одного и того же значения при двух последовательных вызовах.

Давайте реализуем такой функционал, при котором во время регистрации сервиса была бы возможность указывать параметр, определяющий, как будут возвращаться сервис: при каждом вызове новый или же один и тот же.

Шаг 6. Контроль создания сервиса

Контейнер должен уметь управлять жизненным циклом сервиса:

  • Singleton — один экземпляр создаётся и используется повторно.

  • Prototype — создаётся новый экземпляр при каждом запросе.

Можно реализовать это по-разному:

  • Завести два метода: set() и setShared(). При регистрации сервисов через метод set(), при получении будет возвращаться новое значения. А через метод setShared() - новый объект будет создан только при первом вызове, а последующие вызовы будут возвращать уже созданный экземпляр.

  • Добавить флаг $isShared в set(), по умолчанию сделать true - возвращать одно и то же значение при последовательных вызовах. Если для какого-то сервиса нужно, чтоб он каждый раз создавался указать false.

Выберем первый вариант - два метода:

class Container implements ContainerInterface
{
    private array $definitions = [];
    private array $shared = [];

    public function set(string $id, string|callable $callback): void
    {
        $this->shared[$id] = null;
        $this->definitions[$id] = [
            'value' => $callback,
            'shared' => false,
        ];
    }

    public function setShared(string $id, string|callable $callback): void
    {
        $this->shared[$id] = null;
        $this->definitions[$id] = [
            'value' => $callback,
            'shared' => true,
        ];
    }

    public function get($id)
    {
        if (!$this->has($id)) {
            throw new ServiceNotFoundException('Undefined service: ' . $id);
        }

        if (isset($this->shared[$id])) {
            return $this->shared[$id];
        }

        if (array_key_exists($id, $this->definitions)) {
            $definition = $this->definitions[$id]['value'];
            $shared = $this->definitions[$id]['shared'];
        } else {
            $definition = $id;
            $shared = false;
        }

        $component = is_string($definition)
            ? $this->make($definition)
            : $definition($this);

        if (!$component) {
            throw new ContainerException('Undefined component ' . $id);
        }

        if ($shared) {
            $this->shared[$id] = $component;
        }

        return $component;
    }

    public function has(string $id): bool
    {
        if (isset($this->definitions[$id])) {
            return true;
        }
        if (class_exists($id)) {
            return true;
        }
        return false;
    }

    private function make(string $definition): ?object
    {
        if (!class_exists($definition)) {
            return null;
        }

        $reflection = new ReflectionClass($definition);
        $arguments = [];
        if (($constructor = $reflection->getConstructor()) !== null) {
            foreach ($constructor->getParameters() as $param) {
                $paramClass = $param->getType();

                $arguments[] = $paramClass ? $this->get($paramClass->getName()) : null;
            }
        }

        return $reflection->newInstanceArgs($arguments);
    }
}

Метод get() проверяет, есть ли уже созданный экземпляр. Если есть — возвращает его, иначе создаёт новый.

Метод has() - будет возвращать true в двух случаях: есть зарегистрированный сервис под указанным идентификатором или есть класс с указанным названием.

Регистрация сервисов:

$container = new Container();
$container->set(StorageInterface::class, fn () => new SimpleStorage('cart'));
$container->set(CalculatorInterface::class, SimpleCalculator::class);
$container->setShared(Cart::class, Cart::class);

Чтобы убедиться, что сервис Cart является Singleton, добавим публичное свойство description:

class Cart
{
    public string $description = 'default value';
    
    // Остальной код не меняется.
}

Теперь можем проверить:

/** @var Cart $cart */
$cart = $container->get(Cart::class);
echo $cart->description . PHP_EOL; // default value
$cart->description = 'this object is singleton';

$cart = $container->get(Cart::class);
echo $cart->description . PHP_EOL; // this object is singleton

В итоге мы добились того, что при регистрации сервиса через метод setShared(), контейнер сохраняет созданные ранее объекты-сервисы и при повторном запросе сервиса не создавать вторую его копию, а возвращать ранее созданный объект.

Шаг 7. Регистрация скалярных параметров

Сейчас наш контейнер умеет внедрять только классы, используя getType() и рекурсивный вызов get().

Но если конструктор принимает скалярные параметры (например, string, int, bool и т.п.), он подставляет null, что работает только, если есть значения по умолчанию.

У нас как раз есть сервис SimpleStorage, у которого конструктор в качестве аргумента принимает строку.

Если мы зарегистрируем сервис просто указав имя класса $container->set(StorageInterface::class, SimpleStorage::class);, возникнет ошибка: Undefined service: string.

Контейнер не сможет создать SimpleStorage, потому что не знает, какое значение передать в параметр типа string.

Поэтому мы решали эту проблему регистрацией зависимости с параметром $container->set(StorageInterface::class, fn () => new SimpleStorage('cart'));.

Однако мы хотим добиться того, чтобы это работало и с autowiring, без ручного объявления.

Можно это решить несколькими способами, например,

  • Разделять регистрацию параметров и регистрацию сервисов.

  • Передача параметров при регистрации как дополнительный аргумент метода set() и setShared().

Реализуем первый вариант - разделим регистрацию параметров и регистрацию сервисов. Заведем отдельное свойство $parameters, добавим методы setParameter и getParameter. А в методе make() добавим обработку параметров.

class Container implements ContainerInterface
{
    private array $definitions = [];
    private array $parameters = [];
    private array $shared = [];

    public function setParameter(string $name, mixed $value): void
    {
        $this->parameters[$name] = $value;
    }

    public function getParameter(string $name): mixed
    {
        if (!array_key_exists($name, $this->parameters)) {
            throw new \InvalidArgumentException("Parameter '$name' not found.");
        }
        return $this->parameters[$name];
    }

    public function set(string $id, string|callable $callback, array $arguments = []): void
    {
        $this->shared[$id] = null;
        $this->definitions[$id] = [
            'value' => $callback,
            'shared' => false,
            'arguments' => $arguments,
        ];
    }

    public function setShared(string $id, string|callable $callback, array $arguments = []): void
    {
        $this->shared[$id] = null;
        $this->definitions[$id] = [
            'value' => $callback,
            'shared' => true,
            'arguments' => $arguments,
        ];
    }

    public function get($id)
    {
        if (!$this->has($id)) {
            throw new ServiceNotFoundException('Undefined service: ' . $id);
        }

        if (isset($this->shared[$id])) {
            return $this->shared[$id];
        }

        if (array_key_exists($id, $this->definitions)) {
            $definition = $this->definitions[$id]['value'];
            $shared = $this->definitions[$id]['shared'];
            $arguments = $this->definitions[$id]['arguments'];
        } else {
            $definition = $id;
            $shared = false;
            $arguments = [];
        }

        $component = is_string($definition)
            ? $this->make($definition, $arguments)
            : $definition($this);

        if (!$component) {
            throw new ContainerException('Undefined component ' . $id);
        }

        if ($shared) {
            $this->shared[$id] = $component;
        }

        return $component;
    }

    public function has(string $id): bool
    {
        if (isset($this->definitions[$id])) {
            return true;
        }
        if (class_exists($id)) {
            return true;
        }
        return false;
    }

    private function make(string $definition, array $forcedArguments = []): ?object
    {
        if (!class_exists($definition)) {
            return null;
        }

        $reflection = new ReflectionClass($definition);
        $arguments = [];
        if (($constructor = $reflection->getConstructor()) !== null) {
            foreach ($constructor->getParameters() as $index => $param) {
                if (array_key_exists($index, $forcedArguments)) {
                    $arg = $forcedArguments[$index];

                    // Поддержка %param%
                    if (is_string($arg) && preg_match('/^%(.+)%$/', $arg, $matches)) {
                        $arg = $this->getParameter($matches[1]);
                    }

                    $arguments[] = $arg;
                    continue;
                }

                $paramClass = $param->getType();

                $arguments[] = $paramClass ? $this->get($paramClass->getName()) : null;
            }
        }

        return $reflection->newInstanceArgs($arguments);
    }
}

Теперь можем зарегистрировать параметр cart_store:

$container = new Container();
$container->setParameter('cart_store', 'cart');
$container->set(StorageInterface::class, SimpleStorage::class, ['%cart_store%']);
$container->set(CalculatorInterface::class, SimpleCalculator::class);
$container->setShared(Cart::class, Cart::class);

$cart = $container->get(Cart::class);
echo $cart->getCost() . PHP_EOL;

Второй вариант предлагаю попробовать реализовать самостоятельно.

После реализации всех семи шагов у нас получился примитивный DI контейнер. Да, конечно, его нужно еще дорабатывать, добавить проверки типа: "если зарегистрировать 2 сервиса под одинаковым именем должна вызывать выброс исключения ContainerException, так как у каждого сервиса должно быть свое, уникальное имя", и так далее, однако считаю, что для понимания принципа работы DI контейнера, мы реализовали достаточно.

В первой части статьи мы прошли путь от жёстких зависимостей до гибкого DI-контейнера с autowiring и управлением жизненным циклом.


Часть 2. Реализации DI-контейнеров в популярных PHP-фреймворках

Конечно, современный разработчик не будет писать свой DI контейнер. В реальных проектах используются готовые и мощные решения.

Поэтому, когда мы разобрались с принципами работы DI-контейнеров, давайте посмотрим, как они реализованы в популярных PHP-фреймворках.

Symfony

Установка компонента dependency-injection фреймворка Symfony через композер: composer require symfony/dependency-injection.

Регистрация сервисов через Symfony\Component\DependencyInjection\ContainerBuilder:

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

$container = new ContainerBuilder();
$container->setParameter('cart_store', 'cart');

$container->register(StorageInterface::class, SimpleStorage::class)
    ->addArgument('%cart_store%')
    ->setShared(false);

$container->register(CalculatorInterface::class, SimpleCalculator::class)
    ->setShared(false);

$container->register(Cart::class, Cart::class)
    ->addArgument(new Reference(CalculatorInterface::class))
    ->addArgument(new Reference(StorageInterface::class))
    ->setShared(true);

Обратите внимание, аргумент конструктора класса SimpleStorage мы зарегистрировали как отдельный параметр: $container->setParameter('cart_store', 'cart');, а затем сослались на него addArgument('%cart_store%').

Также можно передавать напрямую addArgument('cart').

Вызов метода setShared(false) означает, что каждый раз при запрашивании сервиса будет создаваться новый экземпляр.

Так как сервис Cart зависит от сервисов хранилища и калькулятор, то необходимо указать контейнеру внедрить зависимости при инициализации сервиса Cart.

В Symfony можно использовать конфигурацию YAML (чаще встречается на практике). Для этого нужно установить компонент Config: composer require symfony/config.

Пример файла services.yaml:

parameters:
  cart_store: cart

services:
  StorageInterface:
    class: App\Storage\SimpleStorage
    arguments: ['%cart_store%']
    shared: false

  CalculatorInterface:
    class: App\Calculator\SimpleCalculator

  App\Cart:
    class: App\Cart
    arguments: ['@CalculatorInterface', '@StorageInterface']

Код загрузки yaml:

$container = new \Symfony\Component\DependencyInjection\ContainerBuilder();
$loader = new \Symfony\Component\DependencyInjection\Loader\YamlFileLoader(
    $container,
    new Symfony\Component\Config\FileLocator(__DIR__)
);
$loader->load('services.yml');


/** @var Cart $cart */
$cart = $container->get(Cart::class);
echo $cart->getCost() . PHP_EOL;

Итак, мы рассмотрели два базовых приёма работы с контейнером Symfony:

Также есть способ через атрибуты.

Этих подходов достаточно для понимания базовых принципов DI в Symfony и создания собственных примеров.

Важно понимать, что в реальных проектах Symfony контейнер создаётся и управляется самим фреймворком, а сервисы внедряются автоматически — напрямую запрашивать сервисы у контейнера вручную, как в примерах, обычно не требуется.

Symfony поддерживает автоматическое внедрение зависимостей (autowiring), когда вам не нужно указывать аргументы вручную — контейнер сам определит, что нужно передать. Для этого достаточно в конфигурации включить опцию autowire: true. Аналогично autoconfigure: true позволяет автоматически применять теги и т.п.

Laravel

Установка компонента dependency-injection фреймворка Laravel через композер: composer require illuminate/container.

Регистрация сервисов в Laravel осуществляется через класс Illuminate\Container\Container():

$container = new \Illuminate\Container\Container();

$container->bind(StorageInterface::class, fn () => new SimpleStorage('cart'));
$container->bind(CalculatorInterface::class, fn () => new SimpleCalculator());
$container->singleton(Cart::class, fn ($container) => new Cart(
    $container->make(CalculatorInterface::class),
    $container->make(StorageInterface::class)
));

Что здесь стоит отметить:

  • нет "параметров" как в Symfony — просто передаём нужные значения в конструктор (можно через замыкания (closure)).

  • Вместо register + addArgument — используется bind() с фабрикой (анонимной функцией).

  • Для синглтона — вызываем singleton(), аналог setShared(true) в Symfony.

Контейнер Laravel сам вызовет make() и внедрит нужные зависимости.

Использование DI контейнера Laravel представлено здесь.

Yii3

Установка компонента dependency-injection фреймворка yii3 через композер: composer require yiisoft/di.

Контейнер из пакета yiisoft/di работает с массивом конфигураций. Регистрация сервисов выглядит следующим образом:

$config = ContainerConfig::create()
    ->withDefinitions([
        StorageInterface::class => [
            'class' => SimpleStorage::class,
            '__construct()' => ['cart'],
        ],
        CalculatorInterface::class => SimpleCalculator::class,
        Cart::class => [
            '__construct()' => [
                Reference::to(CalculatorInterface::class),
                Reference::to(StorageInterface::class),
            ],
        ],
    ]);

$container = new Container($config);

Можно сохранить определения (definitions) в .php файле.

По умолчанию все сервисы являются экземплярами singleton. Если нужно получать новый объект каждый раз, необходимо управлять: 'definitionScope' => \Yiisoft\Di\Definition\DefinitionScope::PROTOTYPE.

Использование DI контейнера Yii3 представлено здесь.

Следует отметить, что использование контейнера напрямую — плохая практика.

Гораздо лучше положиться на автоматическое подключение, предоставляемое Injector, доступным в пакете yiisoft/injector.

Во второй части статьи мы рассмотрели DI контейнеры фреймворков Symfony, Laravel, Yii3 и пришли к выводу, что в реальных проектах используют готовые DI-контейнеры фреймворков, но понимание их работы помогает писать более качественный код.


Заключение

В статье шаг за шагом были рассмотрены понятия: внедрение зависимостей (DI), контейнер внедрения зависимостей (DI-контейнер) и автоматическое разрешение зависимостей (autowiring). Также затронули понятия инверсии управления (IoC) и принцип инверсии зависимостей (DIP).

Мы разработали собственный контейнер, который показал, как можно автоматически разрешать зависимости (autowiring) с помощью рефлексии, контролировать создание сервисов (одиночные экземпляры – Singleton, или новые при каждом запросе – Prototype) и даже обрабатывать скалярные параметры. Разумеется, наш самописный контейнер является лишь учебным примером, требующим дальнейшей доработки для использования в реальных проектах.

Также рассмотрели реализацию DI контейнеры фреймворков Symfony, Laravel, Yii3.

В конечном итоге понимание DI и работы DI-контейнеров — это ключевой навык для любого PHP-разработчика, стремящегося писать чистый, поддерживаемый, тестируемый и масштабируемый код. Эти знания не только помогут вам создавать более качественные приложения, но и позво��ят глубже разобраться в архитектуре современных фреймворков и библиотек.