Jak funguje Dependency Injection v Symfony a v Nette
Petr OlišarV 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í:
- DI slouží k předávání závislostí konstuktorem
- 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\AbstractController;
final class TestController extends AbstractController
{
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.
- Musíme prohledat celou třídu a najít všechny řádky s
- 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\AbstractController;
final class TestController extends AbstractController
{
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
- Nette - Dependency injection
- Symfony - Service container
- domnikl/DesignPatternsPHP - Dependency Injection
- Symplify/ControllerAutowire