Kiedy przychodzi zarządzać coraz większą i większą ilością obiektów (pomyśl o frameworkach czy CMS-ach) bardzo szybko możemy zaobserwować pewną niedoskonałość wstrzykiwania zależności z zewnątrz. Musimy za każdym razem inicjować obiekt i zadbać o część jego funkcjonalności. Gdy zaczynamy się z tym borykać w wielu miejscach aplikacji czas pomyśleć o kolejnym kroku – Dependency Injection Container.
Container to obiekt, który przejmie odpowiedzialność za tworzenie naszych komponentów, zarządzanie nimi i ich powiązaniami. Zaletą takiego rozwiązania jest przede wszystkim mała inwazyjność: zarządzany kod nie wie i w żaden sposób nie musi zostać przystosowany, by być obsługiwanym przez DI Container (pod warunkiem, że został napisany zgodnie z zasadami opisanymi w poprzednim poście). Nie przeszkadza także w testowaniu, gdyż każdy element możemy bardzo łatwo wymienić na makietę, którą łatwiej będzie nam przetestować cały komponent. Wreszcie to także scentralizowany punkt zarządzania przepływem sterowania w naszej aplikacji (jeszcze raz przypomnij sobie o dużych projektach), gdzie w jednym miejscu dostajemy możliwość konfiguracji praktycznie wszystkiego (co nie znaczy, że będziemy to miejsce często wykorzystywać).
Na przykładzie biblioteki Zend_Mail spróbujmy skonstruować prosty pojemnik, który uprości nam tworzenie obiektu do jednej linijki $c->getMailer();
class Container {
static protected $shared = array();
protected $parameters = array();
public function __construct(array $parameters = array()) {
$this->parameters = $parameters;
}
public function getMailTransport() {
return new Zend_Mail_Transport_Smtp('smtp.gmail.com', array(
'auth' => 'login',
'username' => $this->parameters['mailer.username'],
'password' => $this->parameters['mailer.password'],
'ssl' => 'ssl',
'port' => 465,
));
}
public function getMailer() {
if (!isset(self::$shared['mailer'])) {
$class = $this->parameters['mailer.class'];
$mailer = new $class();
$mailer->setDefaultTransport($this->getMailTransport());
self::$shared['mailer'] = $mailer;
}
return self::$shared['mailer'];
}
}
Obsługa w tym momencie jest bardzo prosta. Podczas tworzenia Containera przekazujemy mu ustawienia wszelkich komponentów:
$sc = new Container(array(
'mailer.username' => 'foo',
'mailer.password' => 'bar',
'mailer.class' => 'Zend_Mail',
));
Aby pobrać nasz gotowy obiekt Zend_Mail wystarczy w dowolnym miejscu aplikacji wpisać:
$mailer = $sc->getMailer();
Warto w tym miejscu zwrócić uwagę, że mamy do czynienia z dwoma rodzajami usług (obiekty zarządzanie przez DI Container). Te, tworzone za każdym razem, jak na przykład Zend_Mail_Transport_Smtp jak i współdzielone na całą aplikację (za każdym razem, gdy będziemy potrzebować mailera dostaniemy ten sam obiekt – bez użycia Singletonu, który jest dla mnie antywzorcem numer 1).
Takie coś w zasadzie możemy uznać już za pełnoprawny DI Container. Dlaczego by jednak nie pójść krok dalej – skorzystać z gotowej biblioteki która ułatwi nam cały proces. W ostatnim czasie zaroiło się tego trochę, ja za najbardziej stabilny i godny zaufania uważam Dependency Injection Container autorstwa Fabiena Potenciera.
Tworzenie obiektu Container
require_once '/PATH/TO/sfServiceContainerAutoloader.php';
sfServiceContainerAutoloader::register();
$sc = new sfServiceContainerBuilder();
Tworzenie usług
$sc->
register('mail.transport', 'Zend_Mail_Transport_Smtp')->
addArgument('smtp.gmail.com')->
addArgument(array(
'auth' => 'login',
'username' => '%mailer.username%',
'password' => '%mailer.password%',
'ssl' => 'ssl',
'port' => 465,
))->
setShared(false)
;
$sc->
register('mailer', '%mailer.class%')->
addMethodCall(
'setDefaultTransport',
array(new sfServiceReference('mail.transport'))
);
Metoda register($name, $class) przyjmuje podstawowe dane na temat tworzonej usługi (nazwę usługi i klasy), po czym zwraca obiekt klasy sfServiceDefiniton. addArgument() dodaje argument do przekazania konstruktorowi naszego komponentu. Możemy wywołać ją kilkukrotnie, bądź wywołać od razu setArguments() i przekazać wszystkie wymagane informacje. addMethodCall() mówi natomiast jaką metodę wykonać zaraz po utworzeniu naszej usługi. Drugi jej argument to tablica z parametrami do przekazania. Domyślnie tworzone obiekty będą shared, czyli współdzielone w całej aplikacji. Jeżeli chcemy inicjować obiekt za każdym wywołaniem $sc->getService('name') musimy wyłączyć dzielenie się za pomocą setShared(false).
Gdyby ktoś uznał tworzenie definicji w PHP za nieco uciążliwe, twór Fabiena Potenciera pozwoli nam zapisać je także w plikach XML czy YAML. Tak, jak to jest opisane w dokumentacji.
Zależności między obiektami
Najważniejsza rzecz, wszakże mówimy o zależnościach (ang. dependencies). Służy nam do tego klasa sfServiceReference która za argument konstruktora przyjmuje nazwę usługi, którą zamierzamy wstrzyknąć.
Tak więc część
addMethodCall(
'setDefaultTransport',
array(new sfServiceReference('mail.transport'))
)
po utworzeniu obiektu Zend_Mail wywoła setDefaultTransport() wstrzykując obiekt klasy Zend_Mail_Transport_Smtp, czyli naszej usługi o nazwie mail.transport.
Parametry tworzenia usług
Używając Dependency Injection Containera dostajemy nowy poziom zarządzania naszymi obiektami. Z centralnego punktu możemy zarządzać wszystkimi parametrami. Aby podczas tworzenia definicji usługi (nasz sfServiceDefinition) wskazać, że dane będą pochodziły z konfiguracji Containera używamy znaku % na początku i końcu nazwy parametru. register('mailer', '%mailer.class%') utworzy więc usługę o nazwie mailer będącą instancją klasy podanej w parametrze mailer.class.
W naszym wypadku Container oczekuje trzech wartości.
mailer.class
mailer.username
mailer.password
Przekazując je raz
$sc->addParameters(array(
'mailer.username' => 'foo',
'mailer.password' => 'bar',
'mailer.class' => 'Zend_Mail',
));
Możemy wykorzystywać wielokrotnie
$mailer = $sc->mailer;
Mały bonus – Graphviz
To nie koniec możliwości gotowego Containera. Posiadając w systemie bibliotekę Graphviz (do zdobycia tutaj) jesteśmy w stanie w bardzo łatwy sposób wygenerować diagram obiektów i klas ukazujący zależności między nimi.
Tworzenie pliku dla Graphviza ogranicza się do dwóch linijek.
$dumper = new sfServiceContainerDumperGraphviz($sc);
file_put_contents('/somewhere/container.dot', $dumper->dump());
Wystarczy uruchomić skrypt, który zapisze nam plik /somewhere/container.dot a następnie w konsoli wywołać;
$ dot -Tpng /somewhere/container.dot > /somewhere/container.png
Rezultat może być na przykład taki.
Na zakończenie dodam, że scena DI Containers dla PHP jest bardzo rozległa i całkiem prawdopodobnym jest, że gdzieś zalega coś nowego, coś ciekawszego. Jeżeli znajdziecie – dajcie znać. Tak samo jak mile widziane są wszelkie opinie i doświadczenia na temat wdrożonych bibliotek tego typu. Ja – przyznam szczerze – na szerszą skalę nie próbowałem. Ale brakuje mi tego w wielu frameworkach (kto wie, może Symfony 2 mnie przekona).
Świetnie że ktoś zajął się naprawdę przydatnymi narzędziami dla PHP, zamiast deliberować o wyższości jednego frameworka nad drugim.
Z IoC w PHP przez długi czas było kiepsko, jednak ostatnio zaczyna się coś ruszać. Z projektów o którycm wiem:
- Stubbles (www.stubbles.org) używa IoC wzorowanego na Google Guice
- FLOW3 (flow3.typo3.org) jest bardzo enterprise, porównać go można do Spring Framework (http://php.dzone.com/news/flow3-phps-answer-javas-spring)
- Bucket (http://www.sitepoint.com/blogs/2009/05/11/bucket-is-a-minimal-dependency-injection-container-for-php/) będzie pewnie kontenerem we frameworku Konstrukt (http://konstrukt.dk/)
- propozycja Zend_Di upadła niestety (http://framework.zend.com/wiki/display/ZFPROP/Zend_Di+-+Federico+Cargnelutti)
- port PicoContainer do PHP od dawna nie jest rozwijany (co ciekawe napisali go Polacy) http://www.sitepoint.com/forums/showthread.php?t=232030
Swoją drogą ten Twój Dependency Injection Container funkcjonuje podobnie jak singleton, przechowuje instancje klas w zmiennej statycznej, plusy są takie, że możemy w tych klasach tworzyć normalne konstruktory, przekazywać parametry do nich.