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