DI: konfigurace služeb

Dependency Injection (DI) kontejner se snadno konfiguruje pomocí souborů NEON. Řekneme si:

  • jak používat parametry
  • jak přidat a nastavit služby
  • jak vložit více konfiguračních souborů

Samotná konfigurace se obykle zapisuje v NEON souboru. Se syntaxí se můžete seznámit na hřišti.

Parametery

V konfiguraci můžete definovat parametry, které lze pak použít jako součást definic služeb. Čímž můžete zpřehlednit konfiguraci nebo sjednodit a vyčlenit hodnoty, které se budou měnit.

Pro nastavení parametrů použijte sekci parameters konfiguračního souboru:

parameters:
    dsn: 'mysql:host=127.0.0.1;dbname=test'
    user: root
    password: secret

Na parametr dsn se odkážeme kdekoliv v konfiguraci zápisem %dsn%. Lze je používat i uvnitř řetězců jako '%wwwDir%/images'.

Parametry nemusí být jen řetězce nebo čísla, mohou také obsahovat pole:

parameters:
    mailer:
        host: smtp.example.com
        secure: ssl
        user: franta@gmail.com
    languages: [cs, en, de]

Na konkrétní klíč se odkážeme jako %mailer.user%.

Chcete-li zapsat řetězec začínající znakem @ nebo obsahující %, musíte znak escapovat zdvojením na @@ nebo %%.

Služby

Konfigurační soubor je místem, kam umísťujeme definice vlastních služeb. Slouží k tomu sekce services. Například takto může vypadat definice služby pojmenované database, což bude instance PDO:

services:
    # v jednom řádku
    database: PDO(%dsn%, %user%, %password%)

    # nebo ve dvou řádcích:
    database:
        factory: PDO(%dsn%, %user%, %password%)

    # s názvy parametrů (na pořadí nezáleží)
    database:
        factory: PDO(dsn: %dsn%, username: %user%)

    # nebo ve třech řádcích :-)
    database:
        factory: PDO
        arguments: [%dsn%, %user%, %password%]

Což vygeneruje tovární metodu v DI kontejneru:

function createServiceDatabase()
{
    $service = new PDO('mysql:host=127.0.0.1;dbname=test', 'root', 'secret');
    return $service;
}

Kromě vytvoření instance třídy lze volat i metodu:

services:
    database: Database::create(root, secret)

Vygenerovaný kód:

function createServiceDatabase()
{
    $service = Database::create('root', 'secret');
    return $service;
}

V tomto případě je nutné, aby metoda Database::create() měla definovaný návratový typ buď pomocí anotace @return nebo type hintů v PHP 7.

Službu získáme z DI kontejneru metodou getService():

$database = $container->getService('database');

Setup

Nad vytvořenou službou můžeme volat metody nebo nastavovat hodnoty proměnným a statickým proměnným:

services:
    database:
        factory: PDO(%dsn%, %user%, %password%)
        setup:
            - setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION)
            - $mode = 123

    myService:
        factory: MyService
        setup:
            - MyService::$foo = 2

Vygeneruje:

public function createServiceDatabase()
{
    $service = new PDO('mysql:host=127.0.0.1;dbname=test', 'root', 'secret');
    $service->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    $service->mode = 123;
    return $service;
}

public function createServiceMyService()
{
    $service = new MyService;
    MyService::$foo = 2;
    return $service;
}

Parametry metod mohou být kromě řetězců a čísel i pole, lze vytvářet i nové objekty nebo volat metody:

services:
    analyser: My\Analyser(
        FilesystemIterator(%appDir%)
        [dryrun: true, verbose: false]
        DateTime::createFromFormat('Y-m-d')
    )

Vygeneruje:

public function createServiceAnalyser()
{
    return new My\Analyser(
        new FilesystemIterator('...'),
        ['dryrun' => true, 'verbose' => false],
        DateTime::createFromFormat('Y-m-d')
    );
}

Anonymní služby

Pojmenování služeb je vhodné hlavně ve chvíli, kdy na ně chceme odkazovat z jiných částí konfiguračního souboru. Pokud na službu není odkazováno názvem, není ji potřeba pojmenovávat. Pro anonymní služby použijte následující syntax:

services:
    - PDO('sqlite::memory:')

    -
        factory: Model\ArticleRepository
        setup:
            - setCacheStorage

Službu získáme z DI kontejneru metodou getByType():

$database = $container->getByType(PDO::class);
$repository = $container->getByType(Model\ArticleRepository::class);

Propojení služeb

Na jednotlivé služby se odkazujeme pomocí zavináče a názvu služby, takže například @database:

services:
    database: PDO(%dsn%, %user%, %password%)
    articles:
        factory: Model\ArticleRepository(@database)
        setup:
            - setCacheStorage(@cache.storage)   # cache.storage je systémová služba

Vygeneruje:

public function createServiceArticles()
{
    $service = new Model\ArticleRepository($this->getService('database'));
    $service->setCacheStorage($this->getService('cache.storage'));
    return $service;
}

I na anonymní služby lze odkazovat přes zavináč, jen místo názvu uvedeme jejich typ (třídu nebo rozhraní). Tohle ovšem obvykle není potřeba dělat díky autowiringu.

services:
    articles:
        factory: Model\ArticleRepository(@Nette\Database\Connection)  # nebo @\PDO

Pokročilá syntaxe

Formát NEON nám dává mimořádně silné výrazové prostředky, pomocí kterých můžete zapsat téměř cokoliv.

Nad odkazovanou službou lze volat metody, nicméně pro jednoduchost místo -> používáme ::.

services:
    routerFactory: App\Router\Factory
    router: @routerFactory::create()

Vygeneruje:

public function createServiceRouterFactory()
{
    return new App\Router\Factory;
}

public function createServiceRouter()
{
    return $this->getService('routerFactory')->create();
}

Volání method lze zřetězit za sebe:

services:
    foo: FooFactory::build()::get()

Vygeneruje:

public function createServiceFoo()
{
    return FooFactory::build()->get();
}

Volání metod lze použít i v parametrech. Krom metod je lze volat i globální funkce, před jejich názvem dáme čtyřtečku:

services:
    routerFactory: App\Router\Factory( Foo::bar() )   # volání statické metody
    setup:
        - setIp( @http.request::getRemoteAddress() )  # http.request je systémová služba
        - setMode( ::getenv(NETTE_MODE) )             # global function getenv

Vygeneruje:

public function createServiceRouterFactory()
{
    $service = new App\Router\Factory( Foo::bar() );
    $service->setIp( $this->getService('http.request')->getRemoteAddress() );
    $service->setMode( getenv('NETTE_MODE') );
    return $service;
}

Zápis ve tvaru NázevTřídy([parametry, ...]), který používáme obvykle v položce factory a který znamená vytvoření objektu, vlastně odpovídá zápisu v PHP jen s tím rozdílem, že vynecháváme operátor new. Tento zápis můžeme používat i kdekoliv jinde, třeba jako parametr:

services:
    articles:
        factory: Model\ArticleRepository( PDO(%dsn%, %user%, %password%) )
        setup:
            - setCacheStorage( Nette\Caching\Storages\DevNullStorage() )

Vygeneruje:

public function createServiceArticles()
{
    $service = new Model\ArticleRepository( new PDO('mysql:host=127.0.0.1;dbname=test', 'root', 'secret') );
    $service->setCacheStorage( new Nette\Caching\Storages\DevNullStorage );
    return $service;
}

Dokonce nad vytvořeným objektem můžeme rovnou volat metody:

services:
    router: App\Router\Factory()::create()
    # pozor, neplést s App\Router\Factory::create()

Vygeneruje:

public function createServiceRouter()
{
    return (new App\Router\Factory())->create();
}

Autowiring

Autowiring je skvělá vlastnost, která umí automaticky předávat do konstruktoru a dalších metod požadované služby, takže je nemusíme vůbec psát. Ušetří vám spoustu času.

Výše uvedený příklad služby articles tak můžeme takto zjednodušit:

services:
    articles:
        factory: Model\ArticleRepository
        setup:
            - setCacheStorage

Autowiring se řídí podle typehintů, takže aby fungoval, musí být třída ArticleRepository definována asi takto:

namespace Model;

class ArticleRepository
{
    public function __construct(\PDO $db)
    {}

    public function setCacheStorage(\Nette\Caching\IStorage $storage)
    {}
}

Aby bylo možné použit autowiring, musí pro každý typ být v kontejneru právě jedna služba. Pokud by jich bylo víc, autowiring by nevěděl, kterou z nich předat a vyhodil by výjimku:

services:
    mainDb: PDO(%dsn%, %user%, %password%)
    tempDb: PDO('sqlite::memory:')
    articles: Model\ArticleRepository  # VYHODÍ VÝJIMKU, vyhovuje mainDb i tempDb

Řešením by bylo buď autowiring obejít a explicitně uvést název služby (tj articles: Model\ArticleRepository(@mainDb)). Nebo jej u jedné ze služeb můžeme vypnout. Autowiring pak bude fungovat a automaticky bude předávat službu druhou:

services:
    mainDb: PDO(%dsn%, %user%, %password%)

    tempDb:
        factory: PDO('sqlite::memory:')
        autowired: false               # služba tempDb je vyřazena z autowiringu

    articles: Model\ArticleRepository  # tudíž předá do kontruktoru mainDb

Zúžení autowiringu

Jednotlivým službám lze autowiring zúžit jen na určité třídy nebo rozhraní. Služba se pak předá jen do parametrů (např. konstruktoru), jejichž typehint krom toho, že odpovídá typu služby, odpovídá i typům uvedeným v nastavení.

Ukážeme si to na příkladu:

class ParentClass
{}

class ChildClass extends ParentClass
{}

class ParentDependent
{
    function __construct(ParentClass $obj)
    {}
}

class ChildDependent
{
    function __construct(ChildClass $obj)
    {}
}

Pokud bychom je všechny zaregistrovali jako služby, tak by autowiring selhal:

services:
    parent: ParentClass
    child: ChildClass
    parentDep: ParentDependent  # VYHODÍ VÝJIMKU, vyhovují služby parent i child
    childDep: ChildDependent    # autowiring předá do konstruktoru službu child

Služba parentDep vyhodí výjimku Multiple services of type ParentClass found: parent, child, protože do jejího kontruktoru pasují obě služby parent i child, a autowiring nemůže rozhodnout, kterou z nich zvolit.

U služby child můžeme proto zúžit její autowirování na typ ChildClass:

services:
    parent: ParentClass
    child:
        factory: ChildClass
        autowired: ChildClass   # lze napsat i 'autowired: self'

    parentDep: ParentDependent  # autowiring předá do konstruktoru službu parent
    childDep: ChildDependent    # autowiring předá do konstruktoru službu child

Nyní se do kontruktoru služby parentDep předá služba parent, protože teď je to jediný vyhovující objekt. Službu child už tam autowiring nepředá. Typehintu, kterým je ParentClass, sice služba odpovídá (tj. child je typu ParentClass), a už neplatí, že ParentClass je typu ChildClass (vazba is_a).

U služby child by bylo možné autowired: ChildClass zapsat také jako autowired: self, jelikož self je zástupné označení pro třídu aktuální služby.

V klíči autowired je možné uvést i několik tříd nebo interfaců jako pole:

autowired: [BarClass, FooInterface]

Zkusme příklad doplnit ještě o rozhraní:

interface FooInterface
{}

interface BarInterface
{}

class ParentClass implements FooInterface
{}

class ChildClass extends ParentClass implements BarInterface
{}

class FooDependent
{
    function __construct(FooInterface $obj)
    {}
}

class BarDependent
{
    function __construct(BarInterface $obj)
    {}
}

class ParentDependent
{
    function __construct(ParentClass $obj)
    {}
}

class ChildDependent
{
    function __construct(ChildClass $obj)
    {}
}

Když službu child nijak neomezíme, bude pasovat do konstruktorů všech tříd FooDependent, BarDependent, ParentDependent i ChildDependent a autowiring ji tam předá.

Pokud její autowiring ale zúžíme na ChildClass pomocí autowired: ChildClass (nebo self), předá ji autowiring pouze do konstruktoru ChildDependent, protože jen tento typehint je typu ChildClass.

Pokud jej omezíme na ParentClass pomocí autowired: ParentClass, předá ji autowiring navíc i do konstruktoru ParentDependent, protože typehint bude taktéž vyhovovat podmínce.

Pokud jej omezíme na FooInterface, bude autowirovaná do konstruktoru FooDependent a stále i do ParentDependent (typehint ParentClass je typu FooInterface) a ChildDependent, ale nikoliv BarDependent, neboť typehint BarInterface není typu FooInterface.

services:
    child:
        factory: ChildClass
        autowired: FooInterface

    fooDep: FooDependent        # autowiring předá do konstruktoru child
    barDep: BarDependent        # VYHODÍ VÝJIMKU, žádná služba nevyhovuje
    parentDep: ParentDependent  # autowiring předá do konstruktoru child
    childDep: ChildDependent    # autowiring předá do konstruktoru child

Preference autowiringu

Pokud máme více služeb stejného typu a u jedné z nich uvedeme volbu autowired, stává se tato služba preferovanou:

services:
    mainDb:
        factory: PDO(%dsn%, %user%, %password%)
        autowired: PDO    # stává se preferovanou

    tempDb:
        factory: PDO('sqlite::memory:')

    articles: Model\ArticleRepository

Proto služba articles nevyhodí výjimku, že existují dvě vyhovující služby typu PDO (tj. mainDb a tempDb), které lze do konstruktoru předat, ale použije preferovanou službu, tedy mainDb.

Více konfiguračních souborů

V případě, že chceme pro nastavení používat více propojených konfiguračních souborů, můžeme je uvést v sekci includes.

includes:
    - parameters.php
    - services.neon
    - presenters.neon

Pokud se v konfiguračních souborech objeví prvky se stejnými klíči, budou přepsány, nebo v případě polí sloučeny. Později vkládaný soubor má vyšší prioritu než předchozí. Soubor se sekcí includes má vyšší prioritu, než v něm uvedené soubory.

config1.neon config2.neon výsledek
items:
    - 1
    - 2
items:
    - 3
items:
    - 1
    - 2
    - 3

U polí lze zabránit slučování uvedením vykřičníku za názvem klíče:

config1.neon config2.neon výsledek
items:
    - 1
    - 2
items!:
    - 3
items:
    - 3