Przekazywanie zależności
Argumenty, czyli w terminologii DI “zależności”, można przekazywać klasom na następujące główne sposoby:
- przez konstruktor
- przez metodę (tak zwany setter injection)
- przez właściwość
- za pomocą metody
inject*()albo atrybutu#[Inject]
Pokażmy każdy wariant na konkretnych przykładach.
Przekazywanie przez konstruktor
Zależności podawane są jako argumenty konstruktora w chwili tworzenia instancji obiektu:
class MyClass
{
private Cache $cache;
public function __construct(Cache $cache)
{
$this->cache = $cache;
}
}
$obj = new MyClass($cache);
To podejście nadaje się do zależności obowiązkowych, których klasa bezwzględnie potrzebuje do działania, bo bez nich nie da się utworzyć instancji.
Od PHP 8.0 możemy użyć krótszego zapisu (promocja właściwości w konstruktorze), funkcjonalnie równoważnego:
// PHP 8.0
class MyClass
{
public function __construct(
private Cache $cache,
) {
}
}
Od PHP 8.1 właściwość można oznaczyć flagą readonly, która deklaruje, że wartość właściwości nie
zmieni się po inicjalizacji:
// PHP 8.1
class MyClass
{
public function __construct(
private readonly Cache $cache,
) {
}
}
Kontener DI przekazuje zależności do konstruktora automatycznie za pomocą autowiringu. Argumenty, których nie da się w ten sposób podać (np. stringi, liczby, wartości logiczne), podaje się w konfiguracji.
Piekło konstruktorów
Termin piekło konstruktorów opisuje sytuację, w której klasa potomna dziedziczy po klasie nadrzędnej, której konstruktor wymaga zależności, a klasa potomna również wymaga zależności. Musi wtedy przyjąć i przekazać dalej również 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;
// ⛔ PIEKŁO KONSTRUKTORÓW
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 dochodzi nowa
zależność. Wtedy trzeba zmodyfikować również wszystkie konstruktory klas potomnych. Co zamienia taką modyfikację w
piekło.
Jak temu zapobiec? Rozwiązaniem jest preferowanie kompozycji nad dziedziczeniem.
Projektujemy więc kod inaczej. Unikniemy abstrakcyjnych klas
Base*. Zamiast tego, aby MyClass uzyskiwała pewną funkcjonalność przez dziedziczenie po
BaseClass, otrzyma 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 setter
Zależności podawane są przez wywołanie metody, która zapisuje je do prywatnej właściwości. Powszechną konwencją
nazewniczą tych metod jest wzorzec set*(), stąd nazywane są setterami, ale oczywiście mogą nazywać się
inaczej.
class MyClass
{
private Cache $cache;
public function setCache(Cache $cache): void
{
$this->cache = $cache;
}
}
$obj = new MyClass;
$obj->setCache($cache);
To podejście nadaje się do zależności opcjonalnych, które nie są niezbędne do działania klasy, bo nie ma gwarancji, że obiekt rzeczywiście otrzyma zależność (czyli że wywołujący wywoła metodę).
Jednocześnie metoda ta pozwala wywoływać setter wielokrotnie, aby zmienić zależność. Jeśli jest to niepożądane, dodaj
w metodzie sprawdzenie albo, od PHP 8.1, oznacz 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 definiuje się w konfiguracji kontenera DI pod kluczem setup. Również tutaj używane jest automatyczne podawanie zależności przez autowiring:
services:
- create: MyClass
setup:
- setCache
Przekazywanie przez właściwość
Zależności podawane są przez bezpośredni zapis do właściwości składowej:
class MyClass
{
public Cache $cache;
}
$obj = new MyClass;
$obj->cache = $cache;
Metoda ta uznawana jest za nieodpowiednią, bo właściwość składowa musi być zadeklarowana jako public. W
konsekwencji tracimy kontrolę nad tym, czy przekazana zależność rzeczywiście jest wymaganego typu (dotyczyło to zwłaszcza
czasów przed deklaracjami typów właściwości w PHP 7.4) i tracimy możliwość zareagowania własną logiką na nowo
przypisaną zależność, na przykład aby zapobiec późniejszej modyfikacji. Jednocześnie właściwość staje się częścią
publicznego API klasy, co może nie być zamierzone.
Przypisanie do właściwości definiuje się w konfiguracji kontenera DI w sekcji setup:
services:
- create: MyClass
setup:
- $cache = @\Cache
Inject
Podczas gdy poprzednie trzy podejścia obowiązują ogólnie we wszystkich językach obiektowych, wstrzykiwanie przez metody
inject*() albo atrybut #[Inject] używane jest typowo z presenterami Nette, gdzie jest domyślnie
włączone; dowolna inna usługa może włączyć je przez inject: true. Omówione są w osobnym rozdziale.
Który sposób wybrać?
- Konstruktor nadaje się do zależności obowiązkowych, których klasa bezwzględnie potrzebuje do działania.
- Setter przeciwnie, nadaje się do zależności opcjonalnych albo takich, które trzeba będzie później zmienić.
- Właściwości publiczne generalnie nie są zalecane.