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

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.
verzió: 3.x