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