Předávání závislostí

Argumenty, nebo v terminologii DI „závislosti“, lze do tříd předávat těmito hlavními způsoby:

  • předávání konstruktorem
  • předávání metodou (tzv. setterem)
  • nastavením proměnné
  • metodou, anotací či atributem inject

Nyní si jednotlivé varianty ukážeme na konkrétních příkladech.

Předávání konstruktorem

Závislosti jsou předávány v okamžiku vytváření objektu jako argumenty konstruktoru:

class MyClass
{
	private Cache $cache;

	public function __construct(Cache $cache)
	{
		$this->cache = $cache;
	}
}

$obj = new MyClass($cache);

Tato forma je vhodná pro povinné závislosti, které třída nezbytně potřebuje ke své funkci, neboť bez nich nepůjde instanci vytvořit.

Od PHP 8.0 můžeme použít kratší formu zápisu (constructor property promotion), která je funkčně ekvivaletní:

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

Od PHP 8.1 lze proměnnou označit příznakem readonly, který deklaruje, že obsah proměnné se už nezmění:

// PHP 8.1
class MyClass
{
	public function __construct(
		private readonly Cache $cache,
	) {
	}
}

DI kontejner předá konstruktoru závislosti automaticky pomocí autowiringu. Argumenty, které takto předat nelze (např. řetězce, čísla, booleany) zapíšeme v konfiguraci.

Constructor hell

Termín constructor hell označuje situaci, když potomek dědí od rodičovské třídy, jejíž konstruktor vyžaduje závislosti, a zároveň potomek vyžaduje závislosti. Přitom musí převzít a předat i ty rodičovské:

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

Problém nastane v okamžiku, kdy budeme chtít změnit kontruktor třídy BaseClass, třeba když přibude nová závislost. Pak je totiž nutné upravit také všechny konstruktory potomků. Což z takové úpravy dělá peklo.

Jak tomu předcházet? Řešením je dávat přednost kompozici před dědičností.

Tedy navrhneme kód jinak. Budeme se vyhýbat abstraktním Base* třídám. Místo toho, aby MyClass získávala určitou funkčnost tím, že dědí od BaseClass, si tuto funkčnost nechá předat jako závislost:

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

Předávání setterem

Závislosti jsou předávány voláním metody, která je uloží do privátní proměnné. Obvyklou konvencí pojmenování těchto metod je tvar set*(), proto se jim říká settery, ale mohou se samozřejmě jmenovat jakkoliv jinak.

class MyClass
{
	private Cache $cache;

	public function setCache(Cache $cache): void
	{
		$this->cache = $cache;
	}
}

$obj = new MyClass;
$obj->setCache($cache);

Tento způsob je vhodný pro nepovinné závislosti, které nejsou pro funkci třídy nezbytné, neboť není garantováno, že objekt závislost skutečně dostane (tj. že uživatel metodu zavolá).

Zároveň tento způsob připouští volat setter opakovaně a závislost tak měnit. Pokud to není žádoucí, přidáme do metody kontrolu, nebo od PHP 8.1 označíme property $cache příznakem readonly.

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

Volání setteru definujeme v konfiguraci DI kontejneru v klíči setup. I tady se využívá automatického předávání závislostí pomocí autowiringu:

services:
	-	create: MyClass
		setup:
			- setCache

Nastavením proměnné

Závislosti jsou předávány zapsáním přímo do členské proměnné:

class MyClass
{
	public Cache $cache;
}

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

Tento způsob se považuje za nevhodný, protože členská proměnná musí být deklarována jako public. A tudíž nemáme kontrolu nad tím, že předaná závislost bude skutečně daného typu (platilo před PHP 7.4) a přicházíme o možnost reagovat na nově přiřazenou závislost vlastním kódem, například zabránit následné změně. Zároveň se proměnná stává součástí veřejného rozhraní třídy, což nemusí být žádoucí.

Nastavení proměnné definujeme v konfiraci DI kontejneru v sekci setup:

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

Inject

Zatímco předchozí tři způsoby platí obecně ve všech objektově orientovaných jazycích, injektování metodou, anotací či atributem inject je specifické čistě pro presentery v Nette. Pojednává o nich samostatná kapitola.

Jaký způsob zvolit?

  • konstruktor je vhodný pro povinné závislosti, které třída nezbytně potřebuje ke své funkci
  • setter je naopak vhodný pro nepovinné závislosti, nebo závislosti, které lze mít možnost dále měnit
  • veřejné proměnné vhodné nejsou
verze: 3.x 2.x