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

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