Przekazywanie zależności

Argumenty, lub “zależności” w terminologii DI, mogą być przekazywane do klas na następujące główne sposoby:

  • przekazywanie przez konstruktora
  • przekazywanie przez metodę (zwane setterem)
  • poprzez ustawienie zmiennej
  • przez metodę, adnotację lub atrybut inject.

Zilustrujemy teraz różne warianty na konkretnych przykładach.

Przekazywanie przez konstruktora

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

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óre klasa bezwzględnie potrzebuje do funkcjonowania, ponieważ bez nich nie można utworzyć instancji.

Od PHP 8.0 możemy używać krótszej formy notacji (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 zmienna może być oznaczona flagą readonly, która deklaruje, że zawartość zmiennej nie będzie się już zmieniać:

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

Kontener DI przekazuje zależność do konstruktora automatycznie przez autowiring. Argumenty, które nie mogą być przekazane w ten sposób (np. ciągi znaków, liczby, booleans) są zapisywane w konfiguracji.

Piekło konstruktorów

Termin constructor hell odnosi się do sytuacji, w której dziecko dziedziczy po klasie rodzica, której konstruktor wymaga zależności, a dziecko również wymaga zależności. Musi ono również przejąć i przekazać zależności rodzica:

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ę, gdy chcemy zmienić konstruktor klasy BaseClass, na przykład gdy dodamy nową zależność. Wtedy musimy zmodyfikować również wszystkie konstruktory dzieci. Co czyni taką modyfikację piekłem.

Jak temu zapobiec? Rozwiązaniem jest przedkładanie kompozycji nad dziedziczenie.

Zaprojektujmy więc kod inaczej. Unikniemy abstrakcyjnych klas Base*. Zamiast MyClass uzyskiwać pewną funkcjonalność poprzez dziedziczenie z BaseClass, będzie mieć tę funkcjonalność przekazaną 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 setera

Zależności są przekazywane przez wywołanie metody, która przechowuje je w prywatnej właściwości. Zwykła konwencja nazewnicza dla tych metod ma postać set*(), dlatego są one nazywane setterami, ale oczywiście mogą być nazywane inaczej.

class MyClass
{
	private Cache $cache;

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

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

Ta metoda jest przydatna dla opcjonalnych zależności, które nie są konieczne dla funkcji klasy, ponieważ nie jest gwarantowane, że obiekt faktycznie otrzyma zależność (tj. Że użytkownik wywoła metodę).

Jednocześnie metoda ta pozwala na wielokrotne wywoływanie setera w celu zmiany zależności. Jeśli nie jest to pożądane, dodaj do metody kontrolę lub od PHP 8.1 oznacz zmienną $cache flagą 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;
	}
}

Wywołanie setera jest zdefiniowane w konfiguracji kontenera DI w sekcji setup. Również w tym przypadku wykorzystywane jest automatyczne przekazywanie zależności przez autowiring:

services:
	-	create: MyClass
		setup:
			- setCache

Poprzez ustawienie zmiennej

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

class MyClass
{
	public Cache $cache;
}

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

Metoda ta jest uważana za niewłaściwą, ponieważ zmienna członkowska musi być zadeklarowana jako public. Tym samym nie mamy kontroli nad tym, czy przekazana zależność jest rzeczywiście danego typu (tak było przed PHP 7.4) i tracimy możliwość reagowania na nowo przypisaną zależność własnym kodem, na przykład w celu zapobieżenia późniejszej zmianie. W tym samym czasie zmienna staje się częścią publicznego interfejsu klasy, co może nie być pożądane.

Ustawienia zmiennej definiujemy w konfiguracji kontenera DI w sekcji setup:

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

Inject

O ile poprzednie trzy metody obowiązują ogólnie we wszystkich językach obiektowych, o tyle wstrzykiwanie za pomocą metody, adnotacji lub atrybutu inject jest specyficzne dla prezenterów Nette. Zostały one omówione w osobnym rozdziale.

Jaką metodę wybrać?

  • Konstruktor jest odpowiedni dla obowiązkowych zależności, które klasa potrzebuje do działania
  • setter jest odpowiedni dla opcjonalnych zależności lub zależności, które mogą być zmienione
  • zmienne publiczne nie są odpowiednie
wersja: 3.x