Nette Documentation Preview

syntax
Függőségek átadása
******************

<div class=perex>

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
* property beállításával
* *inject* metódussal, annotációval vagy attribútummal

</div>

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:

```php
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 |https://blog.nette.org/hu/php-8-0-complete-overview-of-news#toc-constructor-property-promotion]), amely funkcionálisan ekvivalens:

```php
// PHP 8.0
class MyClass
{
	public function __construct(
		private Cache $cache,
	) {
	}
}
```

PHP 8.1 óta a property-t `readonly` jelzővel lehet ellátni, amely deklarálja, hogy a property tartalma már nem fog megváltozni:

```php
// 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 |autowiring] segítségével. Azokat az argumentumokat, amelyeket így nem lehet átadni (pl. stringek, számok, booleanek), [a konfigurációban írjuk le |services#Argumentumok].


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:

```php
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 |faq#Miért részesítjük előnyben 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 |nette:introduction-to-object-oriented-programming#Absztrakt osztályok] `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:

```php
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 property-be 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.

```php
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.

```php
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 |services#Setup] definiáljuk. Itt is automatikus függőségátadás történik az autowiring segítségével:

```neon
services:
	-	create: MyClass
		setup:
			- setCache
```


Property beállításával
======================

A függőségek közvetlenül a tagváltozóba (property-be) írással kerülnek átadásra:

```php
class MyClass
{
	public Cache $cache;
}

$obj = new MyClass;
$obj->cache = $cache;
```

Ez a módszer nem megfelelőnek tekinthető, mivel a property-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 property az osztály nyilvános interfészének részévé válik, ami nem feltétlenül kívánatos.

A property beállítását a DI konténer konfigurációjában a [setup szekcióban |services#Setup] definiáljuk:

```neon
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 |best-practices:inject-method-attribute] 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 property-k nem megfelelőek.

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
  • property 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 property-t readonly jelzővel lehet ellátni, amely deklarálja, hogy a property 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 property-be 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

Property beállításával

A függőségek közvetlenül a tagváltozóba (property-be) í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 property-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 property az osztály nyilvános interfészének részévé válik, ami nem feltétlenül kívánatos.

A property 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 property-k nem megfelelőek.