Dependency Injection Container

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).

Ten wpis został opublikowany w kategorii Biblioteki, Dobre nawyki, PHP, Projektowanie, Teoria, Wzorce projektowe. Dodaj zakładkę do bezpośredniego odnośnika.

2 odpowiedzi na „Dependency Injection Container

  1. Adam Brodziak pisze:

    Ś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

  2. Luneth pisze:

    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.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *

*

Możesz użyć następujących tagów oraz atrybutów HTML-a: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>