Tvorba komponent s využitím autowiringu

Jak vytvářet složité komponenty s mnoha závislostmi, aniž bychom si zaneřádili presentery, které je budou používat?

Článek je psán pro Nette 2.1.

Díky chytrým vlastnostem Dependency Injection kontejneru, kterým Nette disponuje, lze stejně jako u psaní klasických služeb nechat většinu práce na frameworku. Vezměme si jako příklad registrační formulář.

Tato komponenta bude muset umět vícenásobně komunikovat s modelovou vrstvou:

  1. Zjistit, jestli zadaný email jěště není registrován.
  2. Provést samotnou registraci.
  3. Odeslat uživateli email s odkazem k potvrzení.
  4. Pracovat s SDK Facebooku, abychom umožnili registraci přes tuto sociální síť.

Ukázková registrační komponenta

K řešení takového registračního procesu by šlo pochopitelně přistoupit různými způsoby, ale pro účely našeho příkladu předpokládejme, že každý z těchto bodů znamená jednu službu, kterou je třeba formulářové komponentě předat jako závislost.

class RegistrationControl extends Nette\Application\UI\Control
{
    private $emailValidator, $registrator, $mailer, $facebook;

    public function __construct(EmailValidator $emailValidator, Registrator $registrator, Nette\Mail\IMailer $mailer, Facebook $facebook)
    {
        $this->emailValidator = $emailValidator;
        // etc.
    }
}

Jak předat komponentě závislosti?

Pokud bychom psali klasickou službu, nebylo by co řešit, o předání všech závislostí by se neviditelně postaral DI kontejner. Jenže s komponentami obvykle zacházíme tak, že jejich novou instanci vytváříme přímo v presenteru v jejich tzv. továrničce (metodě createComponent...).

Logickou otázkou je, proč prostě nezaregistrujeme komponentu jako klasickou službu, a tu jen z továrničky nevracíme. Takový přístup je ale krajně nevhodný, když chceme komponentu například vytvářet vícenásobně pomocí Nette\Application\UI\Multiplier.

Zdlouhavou a těžkopádnou variantou je předat si všechny 4 závislosti do presenteru a v továrničce je předat komponentě jako argumenty konstruktoru. Budeme však v presenteru muset zavést 4 privátní proměnné, které mimo továrničku nenajdou žádné uplatnění. Kromě toho jen při pomýšlení na množství napsaného kódu se nám může dělat nevolno…

Architektonicky správným řešením, které neznečistí presenter zbytečnými proměnnými, je napsat pro komponentu její tovární službu. Tato třída bude umět pouze to, že vytvoří instanci komponenty. Mohla by vypadat takto:

class RegistrationControlFactory
{
    private $emailValidator, $registrator, $mailer, $facebook;

    public function __construct(EmailValidator $emailValidator, Registrator $registrator, Nette\Mail\IMailer $mailer, Facebook $facebook)
    {
        $this->emailValidator = $emailValidator;
        $this->registrator = $registrator;
        $this->mailer = $mailer;
        $this->facebook = $facebook;
    }

    public function create()
    {
        return new RegistrationControl($this->emailValidator, $this->registrator, $this->mailer, $this->facebook);
    }
}

Takovou tovární třídu pak stačí zaregistrovat jako službu v config.neon

services:
    - RegistrationControlFactory

…předat ji presenteru jako závislost…

private $registrationControlFactory;

public function injectRegistrationControlFactory(RegistrationControlFactory $factory)
{
    $this->registrationControlFactory = $factory;
}

…a v továrničce zavolat metodu create(). Takovou metodu můžeme bezpečně volat i vícekrát při použití Nette\Application\UI\Multiplier.

protected function createComponentRegistration()
{
    return $this->registrationControlFactory->create();
}

Jak si ušetřit psaní

Asi si teď klepete na čelo, jak jsme si to vlastně pomohli. Množství kódu zůstalo, jen se z presenteru přesunulo do zvláštní třídy. Framework má však skryté eso v rukávu. Konceptu tovární třídy totiž rozumí, a dokáže takovou službu dokonce napsat za nás!

Klíčem k využití této užitečné feature je interface. Pokud napíšeme interface s metodou create(), a dáme DI kontejneru vědět, že jej má chápat jako tovární třídu, o napsání implementace se postará za nás.

Interface pro naši registrační komponentu by mohl vypadat takto:

interface IRegistrationControlFactory
{
    /** @return RegistrationControl */
    function create();
}

Metoda create() je konvencí. Interface musí obsahovat pouze ji. Přípustnou alternativou je get().

Registraci továrny jako služby upravíme následovně:

services:
    - IRegistrationControlFactory

Všimněte si anotace @return u deklarace metody create – podle té Nette pozná, jakou instanci má továrna vytvořit.

Dokonce není potřeba uvádět FQN, Nette v souboru s továrnou rozezná namespace i use klauzule.

V presenteru pak pouze upravíme typehint v injectRegistrationControlFactory metodě z RegistrationControlFactory na IRegistrationControlFactory, a je to. Kódu, kde se jen předávají závislosti, jsme se úspěšně zbavili – o to se postará autowiring. A zároveň můžeme vytvářet nové instance tam, kde jsou potřeba.