Získá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řístupu public.

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 */
    public $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 \App\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. Od verze Nette 2.2 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í (auto-wiring) 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:

  1. závislosti uvedené v konstruktoru,
  2. závislosti voláním metod inject*,
  3. 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 \App\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)
    {
        parent::__construct();
        $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 \App\Service1 @inject */
    private $service1;
    /** @var \App\Service2 @inject */
    private $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)
    {
        parent::__construct();
        // přiřazení do členských proměnných $service1, $service2, $service3
    }

    protected function createCompoonentMyControl()
    {
        $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.

Pozor, v případě předávání do konstruktoru nesmíme zapomenout na volání konstruktoru rodiče: parent::__construct()!

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:

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:

class Service2
{
    private $anotherService;

    public function setAnotherService(AnotherService $service)
    {
        $this->anotherService = $service;
    }
}

Uvedením konfigurační direktivy inject: yes 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: yes

Závislost Service1 bude předána voláním metody inject*, závislost Service2 bude přiřazena do proměnné $service2:

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 \App\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

Od Nette verze 2.1 je možné presenter 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: yes.

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 config.neon:

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.