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

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

abstract class BaseClass
{
	private Cache $cache;

	public function __construct(Cache $cache)
	{
		$this->cache = $cache;
	}
}

final class MyClass extends BaseClass
{
	private Database $db;

	// ⛔ ПЕКЛО КОНСТРУКТОРІВ
	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('Залежність вже встановлена');
		}
		$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