Передача зависимостей

Аргументы, или в терминологии 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. О них рассказывается в отдельной главе.

Какой способ выбрать?

  • конструктор подходит для обязательных зависимостей, которые класс непременно нуждается для своей работы
  • сеттер, наоборот, подходит для необязательных зависимостей или зависимостей, которые можно будет изменять в дальнейшем
  • публичные переменные не подходят
версия: 3.x