Függőségek átadása
Az argumentumokat, vagy a DI terminológiájában „függőségeket”, a következő fő módokon lehet átadni az osztályoknak:
- konstruktoron keresztüli átadás
- metóduson (úgynevezett setteren) keresztüli átadás
- változó beállításával
- inject metódussal, annotációval vagy attribútummal
Most az egyes változatokat konkrét példákon mutatjuk be.
Konstruktoron keresztüli átadás
A függőségek az objektum létrehozásának pillanatában kerülnek átadásra a konstruktor argumentumaiként:
class MyClass
{
private Cache $cache;
public function __construct(Cache $cache)
{
$this->cache = $cache;
}
}
$obj = new MyClass($cache);
Ez a forma alkalmas a kötelező függőségekre, amelyekre az osztálynak feltétlenül szüksége van a működéséhez, mivel nélkülük nem lehet példányt létrehozni.
PHP 8.0 óta használhatunk rövidebb írásmódot (constructor property promotion), amely funkcionálisan ekvivalens:
// PHP 8.0
class MyClass
{
public function __construct(
private Cache $cache,
) {
}
}
PHP 8.1 óta a változót readonly
jelzővel lehet ellátni, amely deklarálja, hogy a változó tartalma már
nem fog megvá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. Azokat az argumentumokat, amelyeket így nem lehet átadni (pl. stringek, számok, booleanek), a konfigurációban írjuk le.
Constructor hell
A constructor hell kifejezés azt a helyzetet jelöli, amikor egy leszármazott egy szülő osztálytól örököl, amelynek konstruktora függőségeket igényel, és ugyanakkor a leszármazott is függőségeket igényel. Eközben át kell vennie és át kell adnia a szülő függőségeit is:
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 merül fel, amikor meg akarjuk változtatni a BaseClass
osztály konstruktorát, például ha
új függőség kerül hozzáadásra. Ekkor ugyanis módosítani kell az összes leszármazott konstruktorát is. Ami egy ilyen
módosítást pokollá tesz.
Hogyan előzzük ezt meg? A megoldás az, hogy előnyben részesítjük a kompozíciót az öröklődéssel szemben.
Tehát másképp tervezzük meg a kódot. Kerülni fogjuk az absztrakt Base*
osztályokat. Ahelyett, hogy a MyClass
bizonyos funkcionalitást úgy szerezne meg, hogy a
BaseClass
-tól örököl, ezt a funkcionalitást függőségként kapja meg:
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;
}
}
Setteren keresztüli átadás
A függőségek egy metódus hívásával kerülnek átadásra, amely egy privát változóba menti őket. Ezeknek a
metódusoknak a szokásos elnevezési konvenciója a set*()
forma, ezért settereknek nevezik őket, de
természetesen bármilyen más néven is nevezhetők.
class MyClass
{
private Cache $cache;
public function setCache(Cache $cache): void
{
$this->cache = $cache;
}
}
$obj = new MyClass;
$obj->setCache($cache);
Ez a módszer alkalmas a nem kötelező függőségekre, amelyek nem szükségesek az osztály működéséhez, mivel nincs garantálva, hogy az objektum ténylegesen megkapja a függőséget (azaz hogy a felhasználó meghívja a metódust).
Ugyanakkor ez a módszer lehetővé teszi a setter ismételt meghívását és a függőség megváltoztatását. Ha ez nem
kívánatos, adjunk hozzá egy ellenőrzést a metódushoz, vagy PHP 8.1 óta jelöljük a $cache
property-t
readonly
jelzővel.
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;
}
}
A setter hívását a DI konténer konfigurációjában a setup kulcsban definiáljuk. Itt is automatikus függőségátadás történik az autowiring segítségével:
services:
- create: MyClass
setup:
- setCache
Változó beállításával
A függőségek közvetlenül a tagváltozóba írással kerülnek átadásra:
class MyClass
{
public Cache $cache;
}
$obj = new MyClass;
$obj->cache = $cache;
Ez a módszer nem megfelelőnek tekinthető, mivel a tagváltozót public
-ként kell deklarálni. Így nincs
ellenőrzésünk afölött, hogy az átadott függőség valóban a megadott típusú-e (ez a PHP 7.4 előtt volt érvényes),
és elveszítjük a lehetőséget, hogy saját kóddal reagáljunk az újonnan hozzárendelt függőségre, például
megakadályozzuk a későbbi módosítást. Ugyanakkor a változó 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 szekcióban definiáljuk:
services:
- create: MyClass
setup:
- $cache = @\Cache
Inject
Míg az előző három módszer általánosan érvényes minden objektumorientált nyelvben, a metódussal, annotációval vagy inject attribútummal történő injektálás kizárólag a Nette presenterjeire jellemző. Ezekről egy külön fejezet szól.
Melyik módszert válasszuk?
- A konstruktor alkalmas a kötelező függőségekre, amelyekre az osztálynak feltétlenül szüksége van a működéséhez.
- A setter viszont alkalmas a nem kötelező függőségekre, vagy olyan függőségekre, amelyeket lehetőség szerint tovább lehet módosítani.
- A public változók nem megfelelőek.