Nette Documentation Preview

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

<div class=perex>

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*.

</div>

Most konkrét példákkal illusztráljuk a különböző változatokat.


Konstruktor-befecskendezés .[#toc-constructor-injection]
========================================================

A függőségek az objektum létrehozásakor argumentumként kerülnek átadásra a konstruktornak:

```php
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 |https://blog.nette.org/hu/php-8-0-teljes-attekintes-az-ujdonsagokrol#toc-constructor-property-promotion]):

```php
// 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
// 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. Az ilyen módon nem átadható argumentumok (pl. stringek, számok, booleans) [a konfigurátorban íródnak |services#Arguments].


Constructor Hell .[#toc-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:

```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 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** |faq#Why composition is preferred over inheritance].

Tervezzük meg tehát a kódot másképp. Kerüljük az [absztrakt |nette:introduction-to-object-oriented-programming#abstract-classes] `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:

```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;
	}
}
```


Setter injektálás .[#toc-setter-injection]
==========================================

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.

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

```php
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 |services#Setup] van definiálva. Itt is a függőségek automatikus átadását használja az autowiring:

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


Property Injection .[#toc-property-injection]
=============================================

A függőségek közvetlenül a tulajdonsághoz kerülnek átadásra:

```php
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 |services#Setup] határozzuk meg:

```neon
services:
	-	create: MyClass
		setup:
			- $cache = @\Cache
```


Injektálás .[#toc-inject]
=========================

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 |best-practices:inject-method-attribute] tárgyaljuk.


Melyik utat válasszuk? .[#toc-which-way-to-choose]
==================================================

- 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

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