Nette - jak zapisovat služby (extension / neon) 13/10/2015

Jaké jsou možnosti registrovat služby do dependency injection containeru. Porovnání zápisů přes Config a přes Extension.

Nette\DI a Nette\DI\Container jsou, jak už všichni víme, skvělé třídy, které nám ušetří spoustu a spoustu času. Jako fakt. Denně do DIC registrujeme služby, načítáme configy, přidáváme parametry. Přes DIC vytváříme služby, továrničky na služby, továrničky na cokoli…

Jaké jsou možnosti

Existují 2 základní postupy. Přes Config, zpravidla config.neon nebo přes Extension.

Pro jednoduché prototypování a přepisování již existujících služeb se více hodí registrovat přes Config.

Pro samostatné standalone rozšíření, komponenty, addony apod. je ideální použít vlastní Extension

Příklad můžeme vidět hned u Nette, které je rozdělené do samostatných balíčků jako například:

Většinou je dobré se držet principu konvence před konfigurací.

Zápisy

Pro účely snadného porovnání zápisů jsem vytvořil balíček FriendsOfNette / DI syntax.

Najdete tam zápisy přes neon a totožné přes extension. Můžete tak snadno porovnat, jestli děláte vše správně, případně narazit na postup, který neznáte (hlavně začátečníci).

Nástroj vytváří 2 containery a přes Nette\Tester je porovnává.

Neon

Neon je velmi důmyslný nástroj, ve kterém lze snadno definovat vše co potřebujeme.

Potřebujeme službu?

services:
  - App\MyService

Potřebujeme službě předat argumenty?

services:
  - App\MyService(path/to/folder)
services:
  my:
    class: App\MyService
    arguments: [path/to/folder]

Potřebujeme službu vytvořit s parametry?

services:
  my:
    class: App\MyService
    parameters: [a]
    arguments: [%a%]
services:
  my:
    class: App\MyService(%a%)
    parameters: [a]

Extension

Extension nebo-li CompilerExtension nám poskytuje API, přes které dokážeme nadefinovat věškerou funkčnost.

Hlavním nástrojem je zde ContainerBuilder. V extension ho získáme přes $this->getContainerBuilder().

Třída obsahuje 3 základní metody:

  • loadConfiguration
    • volá se v 1. kroku
    • co dělat: registrovat a nastavovat svoje služby
  • beforeCompile
    • volá se ve 2. kroku
    • co dělat: upravovat již zaregistrované služby (pokud víte co děláte!!!)
  • afterCompile
    • volá se ve 3. kroku
    • co dělat: vkládat dekorující části PHP kódu, zejména do metody initialize (pokud víte co děláte!)

Tipy a zajímavosti

Jak vytvořit container

Nejjednoduše container vytvoříte přes ContainerLoader takto:

$loader = new ContainerLoader('path/to/temp', $autoRebuild = TRUE);
$class = $loader->load('mycontainer', function (Compiler $compiler) {
    $compiler->addExtension('my', new MyExtension());
    //
    $compiler->loadConfig('my.config.neon');
});

// Container_998a370549
$container = new $class;

Kde se bere název pro container

Může za to metoda ContainerLoader::getClassName($key), což v předchozím případě vrátí Container_998a370549.

$key = 'mycontainer';
$name = 'Container_' . substr(md5(serialize($key)), 0, 10);
// Container_998a370549

Kde se berou defaultní extensions

Nette definuje všechny defaultní extension v Configuratoru.

public $defaultExtensions = [
  'php' => Nette\DI\Extensions\PhpExtension::class,
  'constants' => Nette\DI\Extensions\ConstantsExtension::class,
  'extensions' => Nette\DI\Extensions\ExtensionsExtension::class,
  'application' => [Nette\Bridges\ApplicationDI\ApplicationExtension::class, ['%debugMode%', ['%appDir%'], '%tempDir%/cache']],
  'decorator' => Nette\DI\Extensions\DecoratorExtension::class,
  'cache' => [Nette\Bridges\CacheDI\CacheExtension::class, ['%tempDir%']],
  'database' => [Nette\Bridges\DatabaseDI\DatabaseExtension::class, ['%debugMode%']],
  'di' => [Nette\DI\Extensions\DIExtension::class, ['%debugMode%']],
  'forms' => Nette\Bridges\FormsDI\FormsExtension::class,
  'http' => Nette\Bridges\HttpDI\HttpExtension::class,
  'latte' => [Nette\Bridges\ApplicationDI\LatteExtension::class, ['%tempDir%/cache/latte', '%debugMode%']],
  'mail' => Nette\Bridges\MailDI\MailExtension::class,
  'reflection' => [Nette\Bridges\ReflectionDI\ReflectionExtension::class, ['%debugMode%']],
  'routing' => [Nette\Bridges\ApplicationDI\RoutingExtension::class, ['%debugMode%']],
  'security' => [Nette\Bridges\SecurityDI\SecurityExtension::class, ['%debugMode%']],
  'session' => [Nette\Bridges\HttpDI\SessionExtension::class, ['%debugMode%']],
  'tracy' => [Tracy\Bridges\Nette\TracyExtension::class, ['%debugMode%']],
  'inject' => Nette\DI\Extensions\InjectExtension::class,
];

Kdy se přegeneruje container

To záleží na konkrétním použití. Generování ovlivňuje název containeru a příznak autoRebuild.

K přegenerování dojde když:

Při vytváření containeru se vytvoří i soubor *.meta, který nese informaci o použitých souborech.

Pokud se nějaký soubor v průběhu změní, kontrolní součet nebude sedět a dojde k přegenerování.

Pokud používáte Nette\Configurator, tak ten vytváří název containeru podle parametrů a souborů (configů).

Neznámý počet parametrů

Tuto vlastnost využijeme hlavně v metodě CompilerExtension::afterCompile.

Uvažujme tento kód:

$initialize = $class->getMethod('initialize');

$initialize->addBody('My\\Tracy\\Bar::init(?);', [1, 2]);
$initialize->addBody('My\\Tracy\\Bar::init(?*);', [[1, 2]]);

Výstup bude vypadat takto:

public function initialize()
{
  My\Tracy\Bar::init(1);
  My\Tracy\Bar::init(1, 2);
}

Rozdíl je v placeholderu ?*, který nám parametry expanduje jako pole argumentů.
Dalo by se to přirovnat k call_user_func_array.

Přehled

Kompletní přehled naleznete na GitHubu v repozitáři FriendsOfNette / DI syntax.

Footprint (k 18.10.2015)

Simple

Config (code)

services:
    a1: TestClass

    a2:
        class: TestClass

    a3:
        create: TestClass

Extension (code)

$builder = $this->getContainerBuilder();

$builder->addDefinition('a1')
    ->setClass('TestClass');

$builder->addDefinition('a2')
    ->setClass('TestClass');

$builder->addDefinition('a3')
    ->setFactory('TestClass');

Compiled result

Options

Config (code)

services:
    b1:
        class: TestClass
        autowired: off

    b2:
        class: TestClass
        inject: on

Extension (code)

$builder = $this->getContainerBuilder();

$builder->addDefinition('b1')
    ->setClass('TestClass')
    ->setAutowired(FALSE);

$builder->addDefinition('b2')
    ->setClass('TestClass')
    ->setInject(TRUE);

Compiled result

Arguments

Config (code)

services:
    c1a: TestClass2(1, 2)

    c1b:
        class: TestClass2
        arguments: [1, 2]

    c2a: TestClass2(1)

    c2b:
        class: TestClass2
        arguments: [a: 1]

    c3a: TestClass2(b: 2)

    c3b:
        class: TestClass2
        arguments: [b: 2]

Extension (code)

$builder = $this->getContainerBuilder();

$builder->addDefinition('c1a')
    ->setClass('TestClass2')
    ->setArguments([1, 2]);

$builder->addDefinition('c1b')
    ->setClass('TestClass2', [1, 2]);

$builder->addDefinition('c2a')
    ->setClass('TestClass2')
    ->setArguments([1]);

$builder->addDefinition('c2b')
    ->setClass('TestClass2', [1]);

$builder->addDefinition('c3a')
    ->setClass('TestClass2')
    ->setArguments(['b' => 2]);

$builder->addDefinition('c3b')
    ->setClass('TestClass2', ['b' => 2]);

Compiled result

Tags (code)

Config

services:
    d1:
        class: TestClass
        tags: [t1]
    d2:
        class: TestClass
        tags: [t1: foobar]

Extension (code)

$builder = $this->getContainerBuilder();

$builder->addDefinition('d1')
    ->setClass('TestClass')
    ->addTag('t1');

$builder->addDefinition('d2')
    ->setClass('TestClass')
    ->setTags(['t1' => 'foobar']);

Compiled result

Arguments + parameters

Config (code)

services:
    e1:
        class: TestClass2
        parameters: [a]
        arguments: [%a%]

    e2:
        class: TestClass2
        parameters: [a: NULL, b: 1]
        arguments: [%a%, %b%]

    e3:
        class: TestClass2(%a%)
        parameters: [a]

    e4:
        class: TestClass2(b: %a%)
        parameters: [a]

Extension (code)

$builder = $this->getContainerBuilder();

// $->setClass()->setArguments() <==> $->setFactory()

$builder->addDefinition('e1')
    ->setClass('TestClass2')
    ->setArguments([$builder->literal('$a')])
    ->setParameters(['a']);

$builder->addDefinition('e2')
    ->setClass('TestClass2', [$builder->literal('$a'), $builder->literal('$b')])
    ->setParameters(['a' => NULL, 'b' => 1]);

$builder->addDefinition('e3')
    ->setClass('TestClass2')
    ->setArguments([$builder->literal('$a')])
    ->setParameters(['a']);

$builder->addDefinition('e4')
    ->setClass('TestClass2')
    ->setArguments([NULL, $builder->literal('$a')])
    ->setParameters(['a']);

Compiled result

Implements (interfaces)

Config (code)

services:
    f1:
        implement: ITestInterface

    f2:
        class: stdClass
        implement: ITestInterface

    f3a:
        implement: ITestInterface2
        arguments: [1, 2]

    f3b:
        implement: ITestInterface2
        arguments: [b: 2]

    f4a:
        implement: ITestInterface3
        parameters: [c]
        arguments: [%c%]

    f4b:
        implement: ITestInterface3
        parameters: [c]
        arguments: [1]

    f5s: TestClass2
    f5:
        factory: @f5s
        implement: ITestInterfaceGet

    f6s:
        class: TestClass
    f6:
        factory: @f6s
        implement: ITestInterface

    f7s:
        class: TestClass2
    f7:
        factory: @f7s
        implement: ITestInterface3
        parameters: [c: 1]
        arguments: [%c%]

Extension (code)

$builder = $this->getContainerBuilder();

$builder->addDefinition('f1')
    ->setImplement('ITestInterface');

$builder->addDefinition('f2')
    ->setClass('stdClass')
    ->setImplement('ITestInterface');

$builder->addDefinition('f3a')
    ->setImplement('ITestInterface2')
    ->setArguments([1, 2]);

$builder->addDefinition('f3b')
    ->setImplement('ITestInterface2')
    ->setArguments(['b' => 2]);

$builder->addDefinition('f4a')
    ->setImplement('ITestInterface3')
    ->setArguments([$builder->literal('$c')])
    ->setParameters(['c']);

$builder->addDefinition('f4b')
    ->setImplement('ITestInterface3')
    ->setArguments([1])
    ->setParameters(['c']);

$builder->addDefinition('f5s')
    ->setClass('TestClass2');

$builder->addDefinition('f5')
    ->setFactory('@f5s')
    ->setImplement('ITestInterfaceGet');

$builder->addDefinition('f6s')
    ->setClass('TestClass');

$builder->addDefinition('f6')
    ->setFactory('@f6s')
    ->setImplement('ITestInterface');

$builder->addDefinition('f7s')
    ->setClass('TestClass2');

$builder->addDefinition('f7')
    ->setFactory('@f7s')
    ->setImplement('ITestInterface3')
    ->setArguments([$builder->literal('$c')])
    ->setParameters(['c' => 1]);

Compiled result

References

Config (code)

services:
    g1:
        class: TestClass2
        parameters: [a: NULL, b: NULL]
        arguments: [%a%, %b%]

    g2: @g1

    g3:
        factory: @g1
        arguments: [1]

    g4:
        factory: @g1
        parameters: [b]
        arguments: [b: %b%]

    g5a:
        class: stdClass
        factory: @g1::foo()

    g5b:
        class: stdClass
        factory: @g1::foo
        parameters: [bar]
        arguments: [%bar%]

    g5c:
        class: stdClass
        factory: @g1::foo(%bar%)
        parameters: [bar]

    g5d:
        class: stdClass
        factory: @g1(%bar1%)::foo(%bar2%)
        parameters: [bar1, bar2]

Extension (code)

$builder = $this->getContainerBuilder();

$builder->addDefinition('g1')
    ->setClass('TestClass2')
    ->setArguments([$builder->literal('$a'), $builder->literal('$b')])
    ->setParameters(['a' => NULL, 'b' => NULL]);

$builder->addDefinition('g2')
    ->setFactory('@g1');

$builder->addDefinition('g3')
    ->setFactory('@g1')
    ->setArguments([1]);

$builder->addDefinition('g4')
    ->setFactory('@g1')
    ->setArguments(['b' => $builder->literal('$b')])
    ->setParameters(['b']);

$builder->addDefinition('g5a')
    ->setClass('stdClass')
    ->setFactory('@g1::foo');

$builder->addDefinition('g5b')
    ->setClass('stdClass')
    ->setFactory('@g1::foo')
    ->setArguments([$builder->literal('$bar')])
    ->setParameters(['bar']);

$builder->addDefinition('g5c')
    ->setClass('stdClass')
    ->setFactory('@g1::foo', [$builder->literal('$bar')])
    ->setParameters(['bar']);

$builder->addDefinition('g5d')
    ->setClass('stdClass')
    ->setFactory(new Statement([
            new Statement('@g1', [$builder->literal('$bar1')]),
            'foo'
        ], [$builder->literal('$bar2')])
    )->setParameters(['bar1', 'bar2']);

Compiled result

Setup

Config (code)

services:
    h1:
        class: stdClass
        setup:
            - $a(1)
            - [@self, $a](1)
            - @self::$a(1)
            - foo(1)
            - [@self, foo](1)
            - @self::foo(1)

    h2:
        class: stdClass
        setup:
            - "$service->hello(?)"(@h1)
            - "$service->hi(?)"(@container)
            - "My\\Tracy\\Bar::init(?)"(@self)

Extension (code)

$builder = $this->getContainerBuilder();

$builder->addDefinition('h1')
    ->setClass('stdClass')
    ->addSetup('$a', [1])
    ->addSetup(new Statement(['@self', '$a'], [1]))
    ->addSetup('@self::$a', [1])
    ->addSetup('foo', [1])
    ->addSetup(new Statement(['@self', 'foo'], [1]))
    ->addSetup('@self::foo', [1]);

$builder->addDefinition('h2')
    ->setClass('stdClass')
    ->addSetup(new Statement('$service->hello(?)', ['@h1']))
    ->addSetup(new Statement('$service->hi(?)', ['@container']))
    ->addSetup(new Statement('My\\Tracy\\Bar::init(?)', ['@self']));
}

Compiled result