Przekazywanie zależności

Argumenty, lub w terminologii DI „zależności”, można przekazywać do klas na następujące główne sposoby:

  • przekazywanie przez konstruktor
  • przekazywanie przez metodę (tzw. setter)
  • ustawienie zmiennej
  • metodą, adnotacją lub atrybutem inject

Teraz pokażemy poszczególne warianty na konkretnych przykładach.

Przekazywanie przez konstruktor

Zależności są przekazywane w momencie tworzenia obiektu jako argumenty konstruktora:

class MyClass
{
	private Cache $cache;

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

$obj = new MyClass($cache);

Ta forma jest odpowiednia dla obowiązkowych zależności, których klasa bezwzględnie potrzebuje do swojego działania, ponieważ bez nich nie da się utworzyć instancji.

Od PHP 8.0 możemy użyć krótszej formy zapisu (constructor property promotion), która jest funkcjonalnie równoważna:

// PHP 8.0
class MyClass
{
	public function __construct(
		private Cache $cache,
	) {
	}
}

Od PHP 8.1 można zmienną oznaczyć flagą readonly, która deklaruje, że zawartość zmiennej już się nie zmieni:

// PHP 8.1
class MyClass
{
	public function __construct(
		private readonly Cache $cache,
	) {
	}
}

Kontener DI przekaże konstruktorowi zależności automatycznie za pomocą autowiringu. Argumenty, których w ten sposób przekazać nie można (np. stringi, liczby, booleany) zapiszemy w konfiguracji.

Constructor hell

Termin constructor hell (piekło konstruktorów) oznacza sytuację, gdy potomek dziedziczy po klasie rodzicielskiej, której konstruktor wymaga zależności, a jednocześnie potomek wymaga zależności. Przy tym musi przejąć i przekazać również te rodzicielskie:

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

Problem pojawia się w momencie, gdy będziemy chcieli zmienić konstruktor klasy BaseClass, na przykład gdy pojawi się nowa zależność. Wtedy konieczne jest również zmodyfikowanie wszystkich konstruktorów potomków. Co z takiej modyfikacji czyni piekło.

Jak temu zapobiegać? Rozwiązaniem jest preferowanie kompozycji nad dziedziczeniem.

Czyli zaprojektujemy kod inaczej. Będziemy unikać abstrakcyjnych klas Base*. Zamiast tego, aby MyClass uzyskiwała pewną funkcjonalność przez dziedziczenie po BaseClass, tę funkcjonalność przekażemy jej jako zależność:

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

Przekazywanie przez setter

Zależności są przekazywane przez wywołanie metody, która zapisuje je do prywatnej zmiennej. Zwykłą konwencją nazewnictwa tych metod jest forma set*(), dlatego nazywa się je setterami, ale mogą oczywiście nazywać się jakkolwiek inaczej.

class MyClass
{
	private Cache $cache;

	public function setCache(Cache $cache): void
	{
		$this->cache = $cache;
	}
}

$obj = new MyClass;
$obj->setCache($cache);

Ten sposób jest odpowiedni dla nieobowiązkowych zależności, które nie są niezbędne do działania klasy, ponieważ nie ma gwarancji, że obiekt faktycznie otrzyma zależność (tj. że użytkownik wywoła metodę).

Jednocześnie ten sposób pozwala na wielokrotne wywoływanie settera i tym samym zmianę zależności. Jeśli nie jest to pożądane, dodamy do metody kontrolę, lub od PHP 8.1 oznaczymy właściwość $cache flagą 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;
	}
}

Wywołanie settera definiujemy w konfiguracji kontenera DI w kluczu setup. Również tutaj wykorzystuje się automatyczne przekazywanie zależności za pomocą autowiringu:

services:
	-	create: MyClass
		setup:
			- setCache

Ustawienie zmiennej

Zależności są przekazywane przez zapisanie bezpośrednio do zmiennej członkowskiej:

class MyClass
{
	public Cache $cache;
}

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

Ten sposób uważa się za niewłaściwy, ponieważ zmienna członkowska musi być zadeklarowana jako public. A zatem nie mamy kontroli nad tym, że przekazana zależność będzie faktycznie danego typu (obowiązywało przed PHP 7.4) i tracimy możliwość reagowania na nowo przypisaną zależność własnym kodem, na przykład zapobiegania późniejszej zmianie. Jednocześnie zmienna staje się częścią publicznego interfejsu klasy, co może nie być pożądane.

Ustawienie zmiennej definiujemy w konfiguracji kontenera DI w sekcji setup:

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

Inject

Podczas gdy poprzednie trzy sposoby obowiązują ogólnie we wszystkich językach zorientowanych obiektowo, wstrzykiwanie metodą, adnotacją lub atrybutem inject jest specyficzne wyłącznie dla prezenterów w Nette. Omawiają je osobny rozdział.

Który sposób wybrać?

  • konstruktor jest odpowiedni dla obowiązkowych zależności, których klasa bezwzględnie potrzebuje do swojego działania
  • setter jest natomiast odpowiedni dla nieobowiązkowych zależności, lub zależności, które można mieć możliwość dalej zmieniać
  • publiczne zmienne nie są odpowiednie
wersja: 3.x