DI: 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 inject*()
  • anotací inject u proměnné

První tři způsoby platí obecně ve všech objektově orientovaných jazycích, druhé dva jsou specifické pro presentery v Nette. Nyní si jednotlivé možnosti přiblížíme a ukážeme na konkrétních případech.

Předávání konstruktorem

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

class MyService
{
	/** @var Cache */
	private $cache;

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

$service = new MyService($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, která je funkčně ekvivaletní:

// PHP 8.0
class MyService
{
	public function __construct(
		private Cache $service,
	) {
	}
}

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 MyService
{
	public function __construct(
		private readonly Cache $service,
	) {
	}
}

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.

class MyService
{
	/** @var Cache */
	private $cache;

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

$service = new MyService;
$service->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 proměnnou $cache příznakem readonly.

class MyService
{
	/** @var Cache */
	private $cache;

	public function setCache(Cache $service): void
	{
		if ($this->cache) {
			throw new RuntimeException('The dependency has already been set');
		}
		$this->cache = $service;
	}
}

Nastavením proměnné

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

class MyService
{
	/** @var Cache */
	public $cache;
}

$service = new MyService;
$service->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í.

Předávání metodou inject*()

Tento způsob je specifický pro presentery v Nette. Jedná se o zvláštní případ setteru, kdy metoda začíná prefixem inject.

class MyPresenter extends Nette\Application\UI\Presenter
{
	/** @var Cache */
	private $cache;

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

Základní rozdíl od setteru je ten, že Nette DI takto pojmenované metody v presenterech automaticky volá hned po vytvoření instance a předá jim všechny požadované závislosti. Proč se v presenterech používají rozebíráme níže.

Metod inject*() může presenter obsahovat více a každá může mít libovolný počet parametrů.

Anotace inject

Další způsob specifický pro presentery v Nette. Jedná se o zvláštní případ předávání závislosti do veřejné členské proměnné.

Tentokrát je však proměnná označena anotací @inject v dokumentačním komentáři. Typ závislosti je možné uvést také v dokumentačním komentáři, pokud používáte PHP nižší než 7.4.

class MyPresenter extends Nette\Application\UI\Presenter
{
	/** @var Cache @inject */
	public $cache;
}

Od PHP 8.0 lze proměnnou označit pomocí atributu Inject:

use Nette\DI\Attributes\Inject;

class MyPresenter extends Nette\Application\UI\Presenter
{
	#[Inject]
	public Cache $cache;
}

Nette DI opět takto anotovaným proměnným v presenteru automaticky předá závislosti hned po vytvoření instance.

Tento způsob má stejné nedostatky, jako předávání závislosti do veřejné proměnné. V presenterech se používá proto, že nekomplikuje kód a vyžaduje jen minimum psaní.

Jaký způsob zvolit?

Předávání závislostí pomocí konstruktoru je vhodné pro povinné závislosti, které třída nezbytně potřebuje ke své funkci. Předávání závislostí pomocí setteru je naopak vhodné pro nepovinné závislosti, nebo závislosti, které lze mít možnost dále měnit. Předávání závislostí zápisem do veřejné proměnné vhodné není.

Pro všechny způsoby je společné, že automatické dosazování závislostí (autowiring) funguje pochopitelně pouze tehdy, když se objekty vytváří prostřednictvím DI Containeru nebo továren. Pokud objekt vytváříme voláním new ve vlastním kódu, musíme závislosti vždy předat sami.

Jaký způsob zvolit v presenterech?

V presenterech je preferovaný způsob předávání závislostí pomocí konstruktoru. Pokud však presenter dědí od společného předka (např. BasePresenter), je lepší v tomto předkovi použít metody inject*(). Jejich použitím si totiž ponecháme konstruktor volný pro potomky:

abstract class BasePresenter extends Nette\Application\UI\Presenter
{
	/** @var Foo */
	private $foo;

	public function injectBase(Foo $foo): void
	{
		$this->foo = $foo;
	}
}

class MyPresenter extends BasePresenter
{
	/** @var Bar */
	private $bar;

	public function __construct(Bar $bar)
	{
		$this->bar = $bar;
	}
}

Je také možné použít anotaci @inject, je však třeba mít na paměti, že dojde k porušení zapouzdření.

Předávání pomocí konstruktoru je u společných předků nedoporučované, protože při dědění je nutné získávat závislosti i všech rodičovských presenterů a předávat je do parent::__construct():

abstract class BasePresenter extends Nette\Application\UI\Presenter
{
	/** @var Foo */
	private $foo;

	public function __construct(Foo $foo)
	{
		$this->foo = $foo;
	}
}

class MyPresenter extends BasePresenter
{
	/** @var Bar */
	private $bar;

	public function __construct(Foo $foo, Bar $bar)
	{
		parent::__construct($foo);
		$this->bar = $bar;
	}
}

Metody inject*() se hodí také v případech, kdy je presenter složen z více trait a každá z nich si vyžádá vlastní závislost. V takovém případě je nutné dohlédnout na to, aby název každé inject metody byl unikátní:

class ArticlePresenter extends Nette\Application\UI\Presenter
{
	use StandardTemplateFilters;
	use RequireLoggedUser;
}

trait StandardTemplateFilters
{
	public function injectStandardTemplateFilters(TemplateBuilder $builder): void
	{
		$this->onRender[] = function () use ($builder) {
			$builder->setupTemplate($this->template);
		};
	}
}

trait RequireLoggedUser
{
	public function injectRequireLoggedUser(): void
	{
		$this->onStartup[] = function () {
			if (!$this->getUser()->isLoggedIn()) {
				$this->redirect('Sign:in', $this->storeRequest());
			}
		};
	}
}

V uvedených traitách se využívá skutečnosti, že všechny inject metody se při vytvoření presenteru zavolají a vykoná se tak inicializace v nich obsažená. Přičemž traita RequireLoggedUser metodu k předání závislosti vůbec nevyužívá.


Presenterům je možné předat parametry, které nelze dosadit pomocí autowire, tím že jej zapíšeme do konfiguračního souboru:

services:
	- App\Presenters\ImagePresenter("%wwwDir%/media")
class ImagePresenter extends Nette\Application\UI\Presenter
{
	private $imageDir;
	private $optimizer;

	public function __construct(string $imageDir, ImageOptimizer $optimizer)
	{
		$this->imageDir = $imageDir;
		$this->optimizer = $optimizer;
	}
}

Presenteru se pak jako první parametr konstruktoru předá uvedený řetězec, zbylé parametry se doplní pomocí autowire.

Při navrhování presenterů však buďte opatrní a dbejte na to, aby v nich nebyla obsažena aplikační logika, která patří do služeb. Pokud potřebujete presenter dodatečně konfigurovat, často to bývá známkou toho, že nemáte problém dostatečně dekomponovaný.


Poznámka: uvedením konfigurační direktivy inject: true lze zapnout automatické volání metod inject*() a předávání závislostí do členských proměnných označených anotací @inject pro libovolnou službu:

services:
	foo:
		factory: App\Service
		inject: true