Передача зависимостей
Аргументы, или в терминологии DI «зависимости», можно передавать в классы следующими основными способами:
- передача через конструктор
- передача через метод (так называемый сеттер)
- установка переменной
- методом, аннотацией или атрибутом inject
Теперь покажем каждый вариант на конкретных примерах.
Передача через конструктор
Зависимости передаются в момент создания объекта как аргументы конструктора:
class MyClass
{
private Cache $cache;
public function __construct(Cache $cache)
{
$this->cache = $cache;
}
}
$obj = new MyClass($cache);
Эта форма подходит для обязательных зависимостей, которые класс непременно нуждается для своей работы, так как без них экземпляр создать не получится.
Начиная с PHP 8.0, мы можем использовать более короткую форму записи (constructor property promotion), которая функционально эквивалентна:
// PHP 8.0
class MyClass
{
public function __construct(
private Cache $cache,
) {
}
}
Начиная с PHP 8.1, переменную можно пометить флагом readonly
, который
объявляет, что содержимое переменной больше не изменится:
// PHP 8.1
class MyClass
{
public function __construct(
private readonly Cache $cache,
) {
}
}
DI-контейнер передает зависимости конструктору автоматически с помощью autowiring. Аргументы, которые таким образом передать нельзя (например, строки, числа, булевы значения), записываем в конфигурации.
Constructor hell
Термин constructor hell обозначает ситуацию, когда потомок наследует от родительского класса, конструктор которого требует зависимости, и в то же время потомок требует зависимости. При этом он должен принять и передать также родительские:
abstract class BaseClass
{
private Cache $cache;
public function __construct(Cache $cache)
{
$this->cache = $cache;
}
}
final class MyClass extends BaseClass
{
private Database $db;
// ⛔ CONSTRUCTOR HELL
public function __construct(Cache $cache, Database $db)
{
parent::__construct($cache);
$this->db = $db;
}
}
Проблема возникает в момент, когда мы захотим изменить конструктор
класса BaseClass
, например, когда добавится новая зависимость. Тогда
необходимо изменить также все конструкторы потомков. Что превращает
такое изменение в ад.
Как этого избежать? Решение — отдавать предпочтение композиции перед наследованием.
То есть спроектируем код иначе. Будем избегать абстрактных
Base*
классов. Вместо того чтобы MyClass
получал определенную
функциональность путем наследования от BaseClass
, он получит эту
функциональность как зависимость:
final class SomeFunctionality
{
private Cache $cache;
public function __construct(Cache $cache)
{
$this->cache = $cache;
}
}
final class MyClass
{
private SomeFunctionality $sf;
private Database $db;
public function __construct(SomeFunctionality $sf, Database $db) // ✅
{
$this->sf = $sf;
$this->db = $db;
}
}
Передача сеттером
Зависимости передаются вызовом метода, который сохраняет их в
приватную переменную. Обычное соглашение об именовании этих
методов — форма set*()
, поэтому их называют сеттерами, но они,
конечно, могут называться как угодно иначе.
class MyClass
{
private Cache $cache;
public function setCache(Cache $cache): void
{
$this->cache = $cache;
}
}
$obj = new MyClass;
$obj->setCache($cache);
Этот способ подходит для необязательных зависимостей, которые не являются необходимыми для работы класса, так как не гарантируется, что объект действительно получит зависимость (т. е. что пользователь вызовет метод).
В то же время этот способ позволяет вызывать сеттер повторно и таким
образом изменять зависимость. Если это нежелательно, добавим в метод
проверку, или с PHP 8.1 пометим свойство $cache
флагом readonly
.
class MyClass
{
private Cache $cache;
public function setCache(Cache $cache): void
{
if (isset($this->cache)) {
throw new RuntimeException('The dependency has already been set');
}
$this->cache = $cache;
}
}
Вызов сеттера определяем в конфигурации DI-контейнера в ключе setup. Здесь также используется автоматическая передача зависимостей с помощью autowiring:
services:
- create: MyClass
setup:
- setCache
Установка переменной
Зависимости передаются записью непосредственно в переменную-член:
class MyClass
{
public Cache $cache;
}
$obj = new MyClass;
$obj->cache = $cache;
Этот способ считается неподходящим, поскольку переменная-член
должна быть объявлена как public
. Следовательно, у нас нет контроля
над тем, что переданная зависимость действительно будет данного типа
(действовало до PHP 7.4), и мы теряем возможность реагировать на вновь
назначенную зависимость собственным кодом, например, предотвратить
последующее изменение. В то же время переменная становится частью
публичного интерфейса класса, что может быть нежелательно.
Установку переменной определяем в конфигурации DI-контейнера в секции setup:
services:
- create: MyClass
setup:
- $cache = @\Cache
Inject
В то время как предыдущие три способа применимы в целом во всех объектно-ориентированных языках, инъекция методом, аннотацией или атрибутом inject специфична исключительно для презентеров в Nette. О них рассказывается в отдельной главе.
Какой способ выбрать?
- конструктор подходит для обязательных зависимостей, которые класс непременно нуждается для своей работы
- сеттер, наоборот, подходит для необязательных зависимостей или зависимостей, которые можно будет изменять в дальнейшем
- публичные переменные не подходят