Jak funguje Dependency Injection v Symfony a v Nette

4 min | by Petr Olišar


V tomto článku si ukážeme základy Dependency Injection – jaký je rozdíl mezi Nette presenterem a Symfony controllerem. A jak přenést trochu chování Nette do Symfony.

Dependency Injection (DI) + Container

DI a container už bude asi většina čtenářů znát, takže jen rychlovka pro připomenutí:

  1. DI slouží k předávání závislostí konstuktorem
  2. Container je služba, která vytváří a poskytuje objekty

Spojením DI a containeru získáme tyto výhody:

  • Závislosti se kontrolují již při sestavení containeru
  • Hned při pohledu na konstruktor je jasné, na čem třída závisí
  • Eliminace skrytých závislostí
// Presenter/Controller
final class ...
{
    public function actionDefault()
    {
        $this->myClass->send();
    }
}
// MyClass
class MyClass
{
    public function __construct(Mailer $mailer)
    {
        $this->mailer = $mailer;
    }

    public function send()
    {
        $this->mailer->send()
    }
}

Jak to funguje v Nette?

Presenter je v Nette registrovaný jako služba. Takže i on je sestavovaný containerem a mohou mu být vloženy závislosti do konstruktoru. Pak by řetězec závislostí Presenter > MyClass > Mailer mohl vypadat nějak takhle:

// Presenter
use Nette\Application\UI\Presenter;

final class TestPresenter Extends Presenter
{
    public function __construct(MyClass $myClass)
    {
        $this->myClass = $myClass;
    }

    public function actionDefault()
    {
        $this->myClass->send();
    }
}
// MyClass
class MyClass
{
    public function __construct(Mailer $mailer)
    {
        $this->mailer = $mailer;
    }

    public function send()
    {
        // Some logic
        $this->mailer->send();
    }
}

Každá třída má svou závislost a nepůjde získat z containeru aniž by svou závislost dostala. Všechny závislosti jsou přehledně vidět v konstruktoru a nikde není žádná skrytá závislost.

Výsledek

  • Závislosti se kontrolují již při sestavení containeru - ANO
  • Hned při pohledu na konstruktor je jasné, na čem třída závisí - ANO
  • Eliminace skrytých závislostí - ANO

Jak to funguje v Symfony

Controller v Symfony jako služba registrovaný není, a tak mu není možné vložit jinou závislost konstruktorem. Místo toho existuje traita ContainerAwareTrait, která předává controlleru celý container. Pokud bychom měli stejnou situaci jako v předchozí části, pak by vypadala následovně:

// Controller
use Symfony\Bundle\FrameworkBundle\Controller\Controller;

final class TestController extends Controller
{
    public function indexAction()
    {
        $this->myClass = $this->container->get('myClass');
        $this->myClass->send();
    }
}
// MyClass
class MyClass
{
    public function __construct(Mailer $mailer)
    {
        $this->mailer = $mailer;
    }

    public function send()
    {
        // Some magic
        $this->mailer->send();
    }
}

Základní rozdíl je tento řádek:

$this->myClass = $this->container->get('myClass');

Na začátku jsme si definovali 3 úkoly, které chceme po spojení DI a containeru. Podívejme se, co jsme splnili:

Výsledek

  • Závislosti se kontrolují již při sestavení containeru - NE
    • Controller není službou v containeru, takže se jeho závislosti nekontrolují.
  • Hned při pohledu na konstruktor je jasné, na čem třída závisí - NE
    • Musíme prohledat celou třídu a najít všechny řádky s $this->container->get('whatever'); abychom našli všechny závislosti.
  • Eliminace skrytých závislostí - NE
    • Existují závislosti na něčem co je potřeba, ale při vytvoření instance to ještě potřeba není.

Controller jako služba

Naštěstí existuje řešení! Bundle Symplify/ControllerAutowire, který automaticky registruje controller jako službu do containeru. Po instalaci se bude controller chovat stejně jako presenter v předchozí ukázce.

Bude mu možné předat závislost konstruktorem a získáme tím všechny přednosti, které jsme chtěli po spojení DI a containeru. Navíc při použití traity ControllerAwareTrait fungují i všechny pomocné metody z FrameworkBundle.

// Controller
use Symfony\Bundle\FrameworkBundle\Controller\Controller;

final class TestController extends Controller
{
    public function __construct(MyClass $myClass)
    {
        $this->myClass = $myClass;
    }

    public function indexAction()
    {
        $this->myClass->send();
    }
}
// MyClass
class MyClass
{
    public function __construct(Mailer $mailer)
    {
        $this->mailer = $mailer;
    }

    public function send()
    {
        // Some logic
        $this->mailer->send();
    }
}

Výsledek

  • Závislosti se kontrolují již při sestavení containeru - ANO
  • Hned při pohledu na konstruktor je jasné, na čem třída závisí - ANO
  • Eliminace skrytých závislostí - ANO

Zdroje