DI: předávání závislostí
Existuje několik možností, jakým do presenterů, komponent a služeb můžeme dostat (injektovat) jejich závislosti. V tomto článku probereme:
- obecné možnosti získávání závislostí, nejen těch z DI Containeru v Nette a
- konkrétní příklady a doporučení pro presentery, komponenty a služby.
Jak získávat závislosti?
Závislosti je možné do aplikačních tříd získávat čtyřmi hlavními způsoby:
- předávání konstruktorem,
- předávání setterem nebo nastavením členské proměnné,
- metodou
inject*
- anotací
@inject
u proměnné s typem přístupupublic
.
První dva způsoby platí obecně ve všech objektově orientovaných jazycích, zbylé jsou specifické pro Nette. Nyní si jednotlivé možnosti přiblížíme a poté přejdeme k jejich aplikaci v konkrétních případech.
Předávání konstruktorem
Závislosti, které se předávají v okamžiku vytváření objektu. Závislost je deklarována jako parametr konstruktoru a její typ je uveden jako Type Hint:
class MyService
{
/** @var AnotherService */
private $anotherService;
public function __construct(AnotherService $service)
{
$this->anotherService = $service;
}
}
Třída MyService
takto deklaruje, že při vytváření objektu jí musí být předána instance třídy
AnotherService
. Tato deklarace závislostí je vhodná pro povinné závislosti, které třída nezbytně potřebuje
ke své funkci, neboť bez ní nepůjde instanci vytvořit.
Předávání setterem či nastavením proměnné
Tyto závislosti jsou předávány až po vytvoření objektu. Nejprve uveďme příklad nastavení závislosti setterem. Typ požadované závislosti je uveden jako Type Hint:
class MyService
{
/** @var AnotherService */
private $anotherService;
public function setAnotherService(AnotherService $service)
{
$this->anotherService = $service;
}
}
Objektu je možné závislost předat až po jeho vytvoření. Tento způsob je vhodný pouze pro nepovinné závislosti, které nejsou pro funkci třídy nezbytné, neboť není garantováno, že objekt závislost skutečně dostane.
Podobně pak bude fungovat nastavení členské proměnné:
class MyService
{
/** @var AnotherService */
public $anotherService;
}
Tento způsob je však nevhodný, neboť č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. Přicházíme také o možnost reagovat na
nově přiřazenou závislost vlastním kódem a porušujeme také princip zapouzdření.
Tímto způsobem se tedy nebudeme dále zabývat.
Předávání metodou inject*
Tento způsob je specifický pro DI Container v Nette. Jedná se o zvláštní případ setteru, metoda začíná prefixem
inject
.
class MyService
{
/** @var AnotherService */
private $anotherService;
public function injectAnotherService(AnotherService $service)
{
$this->anotherService = $service;
}
}
Metod inject*
může třída obsahovat několik a každá může mít více parametrů. Každá metoda musí mít
samozřejmě unikátní název.
Základní rozdíl od setteru je ten, že Nette je takto pojmenovanou metodu schopné v presenterech najít a automaticky zavolat se správnými parametry. U služeb lze vyhledání a nalezení vynutit konfigurací – bude vysvětleno dále.
Anotace @inject
Další způsob specifický pro DI Container v Nette. Jedná se o zvláštní případ závislostí předávaných pomocí
public proměnné, tentokrát je však proměnná označena anotací @inject
v dokumentačním komentáři. Typ
předávané závislosti je uveden v dokumentačním komentáři:
class MyService
{
/** @var AnotherService @inject */
public $anotherService;
}
Nette opět může takto anotovanou proměnnou najít a automaticky závislost injektovat. Jako typ třídy musí být uveden
její plný název včetně celého namespace. Lze využít aliasů definovaných pomocí direktiv use
.
Tento způsob má podobné nedostatky, jako předávání závislosti do veřejné proměnné – opět nelze vynutit typ předávané třídy a musíme se spolehnout na to, že bude předána instance správné třídy.
V některých případech však může jít o vhodnou variantu, neboť nekomplikuje kód a vyžaduje jen minimum psaní navíc.
Jaký způsob zvolit?
Předávání závislostí pomocí konstruktoru je dostupné u všech vytvářených tříd, podobně pak použití setteru
u nepovinných závislostí. Další techniky, tedy metody inject*
a členské proměnné označené anotací
@inject
, jsou pak méně čisté techniky a jsou dostupné jen v presenterech, případně je možné je vynutit
konfigurací u služeb vytvářených DI Containerem. Používáme je tedy pouze ve specifických případech, například
u již zmíněných presenterů.
Pro všechny způsoby je společné, že automatické dosazování závislostí
(autowiring) funguje pouze u objektů, které Nette vytváří automaticky 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.
Nyní se podíváme na jednotlivé příklady a uvedeme preferovaný způsob předávání závislostí.
Presentery
Presentery vytváří Nette Framework automaticky prostřednictvím příslušné továrny, která i zajišťuje předání příslušných závislostí. Automaticky jsou předány:
- závislosti uvedené v konstruktoru,
- závislosti voláním metod
inject*
, - závislosti předané do proměnných s anotací
@inject
.
Následující presenter ilustruje všechny tři způsoby předávání:
class MyPresenter extends Nette\Application\UI\Presenter
{
// 1) Předání konstruktorem:
private $service1;
public function __construct(Service1 $service)
{
$this->service1 = $service;
}
// 2) Předání metodou inject*:
private $service2;
public function injectService2(Service2 $service)
{
$this->service2 = $service;
}
// 3) Předání do proměnné s anotací @inject:
/** @var Service3 @inject */
public $service3;
}
Preferovaný způsob předávání závislostí je pomocí konstruktoru, nebo v případě rodičovských presenterů pomocí
metody inject*
. Použitím metody inject*
u rodičovských presenterů zachováme zapouzdření a
zároveň si ponecháme konstruktor čistý pro potomky.
U obou případů je také možné použít anotaci @inject
, je třeba však mít na paměti, že dojde
k porušení zapouzdření.
Předávání pomocí konstruktoru je u rodičovských presenterů spíše nedoporučované, protože při dědění presenterů je nutné získávat závislosti i všech rodičovských presenterů a to komplikuje signaturu konstruktoru.
Komponenty
Komponenty jsou typicky vytvářeny přímo v kódu presenteru, nebo prostřednictvím továren, které jsou specifické pro
danou aplikaci. V těchto případech Nette nemůže závislosti automaticky předat a není možné použít metody
inject*
, ani anotaci @inject
.
Uvažujme následující komponentu:
class MyControl extends Nette\Application\UI\Control
{
// 1) Předání konstruktorem:
private $service1;
public function __construct(Service1 $service)
{
$this->service1 = $service;
}
// 2) Předání setterem:
private $service2;
public function setService2(Service2 $service)
{
$this->service2 = $service;
}
}
Její použití v presenteru by vypadalo následovně:
class MyPresenter extends Nette\Application\UI\Presenter
{
/** @var Service1 */
private $service1;
/** @var Service2 */
private $service2;
public function __construct(Service1 $service1, Service2 $service2)
{
$this->service1 = $service1;
$this->service2 = $service2;
}
protected function createComponentMyControl()
{
$control = new MyControl($this->service1);
$control->setService2($this->service2);
return $control;
}
}
Protože závislosti nejsou z DI Containeru předávány automaticky, musíme je získat ve třídě, která danou komponentu vytváří. V našem případě jde o presenter. Pokud bychom komponentu vytvářeli v jiné komponentě, musíme její závislosti přidat k závislostem rodičovské komponenty:
class MySecondControl extends Nette\Application\UI\Control
{
// Závislost pro MySecondControl:
private $service3;
// Závislosti pro vnořenou MyControl:
private $service1;
private $service2;
public function __construct(Service1 $service1, Service2 $service2, Service3 $service3)
{
// přiřazení do členských proměnných $service1, $service2, $service3
}
protected function createComponentMyControl()
{
$control = new MyControl($this->service1);
$control->setService2($this->service2);
return $control;
}
}
Protože instance komponent typicky vytváříme ručně, je preferovaný způsob předávání závislostí závislý na tom, zda je závislost povinná nebo není. V případě povinných závislostí použijeme konstruktor, v případě nepovinných setter.
Služby
Služby jsou registrovány v DI Containeru a závislosti jsou tedy předávány automaticky. Pokud neuvedeme dodatečnou konfiguraci, budou předány pouze závislosti pomocí konstruktoru:
services:
service1: App\Service1
Takto definované službě budou při vytváření automaticky předány všechny závislosti uvedené v konstruktoru:
namespace App;
class Service1
{
private $anotherService;
public function __construct(AnotherService $service)
{
$this->anotherService = $service;
}
}
Předávání konstruktorem je preferovaný způsob pro služby.
Pokud chceme předávat závislosti pomocí setteru, můžeme definici služby rozšířit o sekci setup
:
services:
service2:
factory: App\Service2
setup:
- setAnotherService
Třída služby:
namespace App;
class Service2
{
private $anotherService;
public function setAnotherService(AnotherService $service)
{
$this->anotherService = $service;
}
}
Uvedením konfigurační direktivy inject: true
pak můžeme zapnout i automatické volání metod
inject*
a předávání závislostí do členských proměnných označených anotací @inject
:
services:
service3:
factory: App\Service3
inject: true
Závislost Service1
bude předána voláním metody inject*
, závislost Service2
bude
přiřazena do proměnné $service2
:
namespace App;
class Service3
{
// 1) Metoda inject*:
private $service1;
public function injectService1(Service1 $service)
{
$this->service1 = $service1;
}
// 2) Přiřazení do proměnné s anotací @inject:
/** @var Service2 @inject */
public $service2;
}
Další možnosti
Existuje několik dalších možností, jak ovlivnit konfiguraci a předávání závislostí pro presentery a komponenty.
Presenter jako služba
Presenter je možné zaregistrovat jako službu do konfiguračního souboru. Při jeho vytváření se však postupuje stejně, jako u libovolné jiné služby. Je tedy možné předat presenteru i parametry, které nelze dosadit pomocí autowire, a přidat volání setterů.
Volání metod inject*
a předávání závislostí do členských proměnných s anotací @inject
se vykoná vždy, není tedy nutné uvádět v konfiguraci inject: true
.
Definice presenteru může vypadat například takto:
services:
- App\Presenters\ImagePresenter("%wwwDir%/media")
class ImagePresenter extends Nette\Application\UI\Presenter
{
private $imageDir;
private $optimizer;
public function __construct($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ý.
Továrna na komponenty
Podobně jako presentery, i komponenty můžeme zaregistrovat v konfiguračním souboru. Avšak zatímco u presenteru se dá očekávat, že bude existovat v průběhu vyřizování celého požadavku pouze jeden, komponent může být víc. Proto místo registrace jako služby použijeme továrnu.
Od Nette 2.1 je možné používat továrny generované proti rozhraní. U rozhraní stačí u příslušných metod uvést
anotaci @return
, která říká, instanci jaké třídy má továrna vytvářet. Nette pak pro rozhraní vygeneruje
správnou implementaci.
Rozhraní musí mít právě jednu metodu pojmenovanou create
. Naše rozhraní pro komponentu může vypadat
například takto:
namespace App\Components;
interface IUserTableFactory
{
/**
* @return UserTable
*/
public function create();
}
Metoda create
bude vytvářet komponentu UserTable
s následující definicí:
namespace App\Components;
class UserTable extends Control
{
private $userManager;
public function __construct(UserManager $userManager)
{
$this->userManager = $userManager;
}
}
Továrnu zaregistrujeme v konfiguračním souboru:
services:
- App\Components\IUserTableFactory
Nette zkontroluje, zda se jedná o rozhraní a pokud ano, tak vygeneruje příslušnou implementaci továrny. Uvedený zápis by bylo možné také rozepsat takto:
services:
userTableFactory:
implement: App\Components\IUserTableFactory
Při zápisu tímto způsobem je možné pro komponentu definovat argumenty pomocí klíče arguments
a
doplňující konfiguraci pomocí setup
stejně, jako u ostatních služeb.
V presenteru pak bude stačit získat příslušnou závislost a zavolat metodu create
:
class UserPresenter extends Nette\Application\UI\Presenter
{
/** @var \App\Components\IUserTableFactory @inject */
public $userTableFactory;
protected function createComponentUserTable()
{
return $this->userTableFactory->create();
}
}
Vytvořené komponentě budou automaticky konstruktorem předány její závislosti.
Související články na blogu
Services don't need names
I love Nette Framework's dependency injection solution. I really do. This post is here to share this passion, explaining why I think it is the best…
Dependency Injection: úvod (1/6)
Pamatujete si na svůj první program? Netuším sice, v jakém jazyce byl napsaný, ale kdyby to bylo PHP 7, nejspíš by vypadal nějak takto: function…
Ako automaticky zaregistrovať triedy do DIC
Veľa z vás to možno nevedelo, no Nette 3 má zabudované rozšírenie na automatické registrovanie tried do DI kontajnera. Takéto rozšírenie dokáže …
Nabušené DI srdce pro vaše aplikace
Jednou z nejzajímavějších částí Nette, kterou vychvalují i uživatelé jiných frameworků, je Dependency Injection Container (dále Nette DI). Podívejte…