Függőségek átadása
Az argumentumok, vagy DI terminológiában “függőségek”, a következő főbb módokon adhatók át az osztályoknak:
- konstruktor általi átadás
- átadás metóduson keresztül (úgynevezett setter)
- egy tulajdonság beállításával
- módszerrel, annotációval vagy attribútummal injektálással.
Most konkrét példákkal illusztráljuk a különböző változatokat.
Konstruktor-befecskendezés
A függőségek az objektum létrehozásakor argumentumként kerülnek átadásra a konstruktornak:
class MyClass
{
private Cache $cache;
public function __construct(Cache $cache)
{
$this->cache = $cache;
}
}
$obj = new MyClass($cache);
Ez a forma olyan kötelező függőségek esetében hasznos, amelyekre az osztálynak feltétlenül szüksége van a működéshez, mivel nélkülük a példány nem hozható létre.
A PHP 8.0 óta használhatunk egy rövidebb jelölési formát, amely funkcionálisan egyenértékű (constructor property promotion):
// PHP 8.0
class MyClass
{
public function __construct(
private Cache $cache,
) {
}
}
A PHP 8.1 óta egy tulajdonságot megjelölhetünk egy readonly
jelzővel, amely kijelenti, hogy a tulajdonság
tartalma nem fog változni:
// PHP 8.1
class MyClass
{
public function __construct(
private readonly Cache $cache,
) {
}
}
A DI konténer automatikusan átadja a függőségeket a konstruktornak az autowiring segítségével. Az ilyen módon nem átadható argumentumok (pl. stringek, számok, booleans) a konfigurátorban íródnak.
Constructor Hell
A construktor hell kifejezés arra a helyzetre utal, amikor egy gyermek egy olyan szülő osztálytól örököl, amelynek konstruktora függőségeket igényel, és a gyermek is függőségeket igényel. A szülő függőségeit is át kell vennie és tovább kell adnia:
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;
}
}
A probléma akkor jelentkezik, amikor a BaseClass
osztály konstruktorát meg akarjuk változtatni, például egy
új függőség hozzáadásakor. Ekkor a gyerekek összes konstruktorát is módosítani kell. Ami pokollá teszi az ilyen
módosítást.
Hogyan lehet ezt megelőzni? A megoldás az, hogy elsőbbséget adunk a kompozíciónak az örökléssel szemben.
Tervezzük meg tehát a kódot másképp. Kerüljük az absztrakt Base*
osztályokat. Ahelyett, hogy a MyClass
a BaseClass
osztályból örökölve kapna bizonyos funkciókat,
ahelyett, hogy függőségként átadnánk ezeket a funkciókat:
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;
}
}
Setter injektálás
A függőségek átadása egy olyan metódus meghívásával történik, amely egy privát tulajdonságban tárolja őket.
Ezeknek a metódusoknak a szokásos elnevezési konvenciója a set*()
, ezért hívják őket settereknek, de
természetesen hívhatók máshogy is.
class MyClass
{
private Cache $cache;
public function setCache(Cache $cache): void
{
$this->cache = $cache;
}
}
$obj = new MyClass;
$obj->setCache($cache);
Ez a metódus olyan opcionális függőségek esetében hasznos, amelyek nem szükségesek az osztály működéséhez, mivel nem garantált, hogy az objektum valóban megkapja őket (azaz a felhasználó meghívja a metódust).
Ugyanakkor ez a metódus lehetővé teszi a setter ismételt meghívását a függőség megváltoztatására. Ha ez nem
kívánatos, adjunk hozzá egy ellenőrzést a metódushoz, vagy a PHP 8.1-től kezdve jelöljük meg a $cache
tulajdonságot a readonly
flaggel.
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;
}
}
A setter hívás a DI konténer konfigurációjában a setup szakaszban van definiálva. Itt is a függőségek automatikus átadását használja az autowiring:
services:
- create: MyClass
setup:
- setCache
Property Injection
A függőségek közvetlenül a tulajdonsághoz kerülnek átadásra:
class MyClass
{
public Cache $cache;
}
$obj = new MyClass;
$obj->cache = $cache;
Ez a módszer nem tekinthető megfelelőnek, mivel a tulajdonságot a public
címen kell deklarálni. Így nincs
befolyásunk arra, hogy az átadott függőség valóban a megadott típusú lesz-e (ez a PHP 7.4 előtt volt igaz), és
elveszítjük a lehetőséget, hogy saját kódunkkal reagáljunk az újonnan hozzárendelt függőségre, például a későbbi
változások megakadályozására. Ugyanakkor a tulajdonság az osztály nyilvános interfészének részévé válik, ami nem
feltétlenül kívánatos.
A változó beállítását a DI konténer konfigurációjában, a setup szakaszban határozzuk meg:
services:
- create: MyClass
setup:
- $cache = @\Cache
Injektálás
Míg az előző három módszer általában minden objektumorientált nyelvben érvényes, a metódus, annotáció vagy inject attribútum általi injektálás a Nette prezenterekre jellemző. Ezeket külön fejezetben tárgyaljuk.
Melyik utat válasszuk?
- A konstruktor alkalmas a kötelező függőségekre, amelyekre az osztálynak szüksége van a működéshez.
- a setter ezzel szemben az opcionális, vagy megváltoztatható függőségekhez alkalmas.
- a nyilvános változók nem ajánlottak