Прехвърляне на зависимостта

Аргументите или “зависимостите” в терминологията на DI могат да се предават на класовете по следните основни начини:

  • предаване чрез конструктор
  • предаване по метод (нарича се setter)
  • чрез задаване на свойство
  • чрез метод, анотация или атрибут *inject

Сега ще илюстрираме различните варианти с конкретни примери.

Внедряване чрез конструктор

Зависимостите се предават като аргументи на конструктора при създаването на обекта:

class MyClass
{
	private Cache $cache;

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

$obj = new MyClass($cache);

Тази форма е полезна за задължителни зависимости, които са абсолютно необходими за функционирането на класа, тъй като без тях не може да се създаде инстанция.

От версия 8.0 на PHP можем да използваме по-кратка форма на запис (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 предава зависимостите на конструктора автоматично, като използва автоматично свързване. Аргументите, които не могат да бъдат предадени по този начин (напр. низове, числа, булеви стойности), се записват в конфигурацията.

Адът на конструкторите

Терминът ад на конструкторите се отнася до ситуация, при която наследник наследява родителски клас, чийто конструктор изисква зависимости, и наследникът също изисква зависимости. То също трябва да поеме и предаде зависимостите на родителя:

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);

Този метод е полезен за незадължителни зависимости, които не са необходими за функционирането на класа, тъй като не е гарантирано, че обектът действително ще ги получи (т.е. че потребителят ще извика метода).

В същото време този метод позволява многократно извикване на setter за промяна на зависимостта. Ако това не е желателно, добавете проверка към метода или, от версия 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;
	}
}

Извикването на setter се дефинира в конфигурацията на контейнера DI в раздела за настройка. И тук автоматичното предаване на зависимостите се използва чрез autowiring:

services:
	-	create: MyClass
		setup:
			- setCache

Изпълнение чрез свойства

Зависимостите се предават директно на свойството:

class MyClass
{
	public Cache $cache;
}

$obj = new MyClass;
$obj->cache = $cache;

Този метод се счита за неприемлив, тъй като свойството трябва да бъде декларирано като public. Следователно нямаме контрол върху това дали предадената зависимост действително има зададения тип (това беше вярно преди PHP 7.4) и губим възможността да реагираме на новоназначената зависимост със собствен код, например за да предотвратим последващи промени. В същото време свойството става част от публичния интерфейс на класа, което може да е нежелателно.

Настройката на променливата се дефинира в конфигурацията на контейнера DI в раздел настройка:

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

Инжектиране

Докато предишните три метода са общовалидни във всички обектно-ориентирани езици, инжектирането чрез метод, анотация или атрибут inject е специфично за презентаторите на Nette. Те са разгледани в отделна глава.

Кой път да избера?

  • Конструкторът е подходящ за задължителни зависимости, от които класът се нуждае, за да функционира.
  • сетърът, от друга страна, е подходящ за незадължителни зависимости или за зависимости, които могат да бъдат променяни.
  • публичните променливи не се препоръчват
версия: 3.x