Если вы писали хоть сколько-нибудь сложный код, то наверняка сталкивались с зависимостями между классами. Эта статья поможет понять, как сделать работу с такими зависимостями чистой и управляемой.
Цель статьи — дать начинающим 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-разработчика, стремящегося писать чистый, поддерживаемый, тестируемый и масштабируемый код. Эти знания не только помогут вам создавать более качественные приложения, но и позво��ят глубже разобраться в архитектуре современных фреймворков и библиотек.
