Передача залежностей

Аргументи, або “залежності” в термінології DI, можуть бути передані класам такими основними способами:

  • передача за допомогою конструктора
  • передача методом (називається setter)
  • шляхом встановлення властивості
  • методом, анотацією або атрибутом 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. Аргументи, які не можна передавати таким чином (наприклад, рядки, числа, булеви) записати в конфігурації.

Пекло конструктора

Термін пекло конструктора стосується ситуації, коли дочірній клас успадковує від батьківського класу, конструктор якого потребує залежностей, і дочірній клас також потребує залежностей. Він також повинен перейняти і передати залежності батьківського класу:

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 ($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 в розділі section setup:

services:
	-
		create: MyClass
		setup:
			- $cache = @\Cache

Ін'єкція

У той час як попередні три методи загалом працюють у всіх об'єктно-орієнтованих мовах, ін'єкція за методом, анотацією або атрибутом inject є специфічною для презентаторів Nette. Вони обговорюються в окремій главі.

Який шлях обрати?

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