Внедрение зависимости c Inversion

    Inversion это простой и функциональный контейнер внедрения зависимости для PHP 5.3. Поддерживает сервис-ориентированную архитектуру, ссылки, PRS-0, и Composer.



    Установить можно через packagist.org: granula/inversion либо скачав и добавив к PRS-0 совместимому загрузчику.

    $container = new Inversion\Container();
    $container['foo'] = 'My\Class\Foo';
    // ...
    $foo = $container('foo');
    


    В вышеприведённом примере показана базовая функциональность контейнера. Разберем что там происходит.
    В первой строчки создаем экземпляр контейнера. Во второй создаем ассоциацию между «foo» и сервисом создающим экземпляр класса «My\Class\Foo». Что по другому можно записать так:
    $container->addService(new Service('My\Class\Foo'), 'foo');
    

    Имя «foo» идёт вторым, т.к. его вообще можно опустить. Подробнее ниже.

    В третей строчке мы получаем экземпляр объекта. Что по другому можно записать так:
    $foo = $container('foo');
    // или
    $foo = $container->get('foo');
    // или
    $foo = $container['foo']->get();
    // или 
    $foo = $container->getService('foo')->get();
    

    Однако, рекомендую использовать сокращённый вариант, хотя все они допустимы.


    Описание зависимостей


    По умолчанию когда в контейнер передаётся строка она понимается как имя класса и подставляется в сервис Inversion\Servise.
    У данного сервиса есть несколько особенностей и функций.
    Первое это отложенная загрузка. Пока вы не будете использовать его, класс не будет загружен.
    Второе, вы можете указать зависимость от других сервисов и параметров. Объясню на примере.
    Пусть у нас есть класс Bar, который зависит от классов One и Two:
    namespace My\Space;
    class One {}
    class Two {}
    class Bar
    {
        public function __construct(One $one, Two $two) 
        {
        }
    }
    

    Опишем эту зависимость в Inversion:
    use Inversion\Service;
    //...
    $container['one'] = 'My\Space\One';
    $container['two'] = 'My\Space\Two';
    $container['bar'] = new Service('My\Space\Bar', array($container['one'], $container['two']));
    

    Теперь при вызове «bar», они будут созданы и подставлены в конструктор. На самом деле можно ещё проще. Если вместо «one» и «two» указать их имена классов:
    $container['My\Space\One'] = 'My\Space\One';
    $container['My\Space\Two'] = 'My\Space\Two';
    $container['My\Space\Bar'] = new Service('My\Space\Bar'); // "new Service" можно опустить 
    

    Это удобный способ описывать зависимости при использовании интерфейсов:
    namespace My\Space;
    class One implements OneInterface {}
    class Two implements TwoInterface  {}
    class Bar implements BarInterface 
    {
        public function __construct(OneInterface $one, TwoInterface $two) 
        {
        }
    }
    


    $container['My\Space\OneInterface'] = 'My\Space\One';
    $container['My\Space\TwoInterface'] = 'My\Space\Two';
    $container['My\Space\BarInterface'] = 'My\Space\Bar'; 
    

    Вообще имена интерфейсов, можно опустить. Они будут автоматически получены из классов:
    $container[] = 'My\Space\One';
    $container[] = 'My\Space\Two';
    $container[] = 'My\Space\Bar'; 
    

    Вот так вот просто.
    Однако, нужно понимать что в таком случае классы будут сразу же загружены чтобы получить список интерфейсов через рефлексию. Поэтому лучше указывать имя интерфейса вручную.


    Другие виды сервисов


    В библиотеке идет несколько сервисов, однако вы можете создать свой имплементировав Inversion\ServiceInterface.

    Closure


    Класс: Inversion\Service\Closure
    Использование:
    $container['closure'] = function () use ($container) {
        return new My\Class();
    };
    

    Можно также указать зависимости:
    $container['closure'] = function (One $foo, Two $foo) use ($container) {
        return new My\Class();
    };
    

    Так же как и с Inversion\Service можно указать их явно:
    $container['closure'] = new Closure(function (One $foo, Two $foo) use ($container) {
        return new My\Class();
    }, array($container['one'], $container['two']));
    


    Factory


    Класс: Inversion\Service\Factory
    Использование:
    $container['factory'] = new Factory('My\ClassFactory', 'create');
    

    Так же можно указать зависимости для конструктора явно третьим параметром.

    Object


    Класс: Inversion\Service\Object
    Использование:
    $container['object'] = new My\Class();
    

    или
    $container['object'] = new Object(new My\Class());
    


    Prototype


    Класс: Inversion\Service\Prototype
    Использование:
    $container['prototype'] = new Prototype($object);
    

    При каждом вызове будет создана новая копия: clone $object.

    Data


    Класс: Inversion\Service\Data
    Использование:
    $container['data'] = new Data('what you want');
    

    По умолчанию все массивы преобразуется в Data сервисы.
    $container['data'] = array(...);
    

    Эквивалентно:
    $container['data'] = new Data(array(...));
    

    Ссылки на сервисы


    Inversion поддерживает ссылки. Что бы получить ссылку обратитесь к контейнеру как к массиву:
    $container['foo'] = new Service(...);
    
    $ref = $container['foo']; // Ссылка на сервис.
    

    Таким образом можно создать алиас к любому сервису:
    $container['My\Class\FooInterface'] = new Service('My\Class\Foo');
    $container['foo'] = $container['My\Class\FooInterface']; 
    //...
    $foo = $container('foo');
    

    Теперь если кто-нибудь перезапишет «My\Class\FooInterface», то «foo» будет по прежнему ссылаться на этот сервис:
    //...
    $container['My\Class\FooInterface'] = new Service('Another\FooImpl');
    //...
    $foo = $container('foo'); // $foo instanseof Another\FooImpl
    

    Можно даже создавать ссылки на ссылки:
    $container['foo'] = 'My\Class\Foo';
    $container['ref'] = $container['foo']; 
    $container['ref2'] = $container['ref']; 
    $container['ref3'] = $container['ref2'];
    //...
    $foo = $container('ref3'); // $foo instanseof My\Class\Foo
    $name = $container->getRealName('ref3'); // $name == 'foo'
    

    Расширение сервисов


    Например если мы хотим расширить какой-нибудь сервис, то такой способ не подойдет т.к. он перезапишет первый:
    $container['My\Class\FooInterface'] = 'My\Class\Foo';
    //...
    $container['My\Class\FooInterface'] = function (FooInterface $foo) {
        $foo->extendSome(...);
        return $foo;
    };
    

    В результате будет зацикливание, что бы этого избежать, для расширения используйте следующую функцию:
    $container['My\Class\FooInterface'] = 'My\Class\Foo';
    //...
    $container->extend('My\Class\FooInterface', function (FooInterface $foo) {
        return new FooDecorator($foo);
    });
    

    Тесты


    Библиотека Inversion полностью тестирована. Тесты находятся в отдельном репозитории (granula/test) для уменьшения размера библиотеки.

    Как Singleton


    Inversion спроектирована полностью без использования статических методов и синглетонов, однако редко бывает полезно иметь контейнер как синглетон:
    $container = Inversion\Container::getInstanse();
    


    Другие реализации


    • Symfony Dependency Injection — мощная и тяжёлая библиотека внедрения зависимости. Имеет хорошую документацию.
    • Pimple — простой и очень лёгкий (всего один файл) «контейнер» от создателя Symfony.

    Комментарии 18

      0
      Конечно, к сервис ориентированной архитектуре (SOA) это имеет достаточно отдаленное отношение.

      В чем преимущество перед указанными «соперниками»? В чем тяжесть симфоневской реализации? Почти уверен что при при дампинге в класс sf di выиграет достаточно много.

      Прототипы через клонирование не лучший вариант. Как отдельная фишка — мб, но в основе должен быть вариант простого пересоздание сервиса.

      В общем, пока не понятно «зачем».
        +1
        Мне в моём проекте нужен был DI, я взял естественно то с чем уже знаком из Symfony, но он мне не подходил, после работал с Pimple, но в нём не хватало функциональности. Я решил создать что-то среднее между ними. Вот «зачем» :)

        PS Конечно же я мог оставить эту библиотеку только для себя, однако могут оказаться люди для которых пригодиться моя реализация.
          +1
          Чем именно для вас эта библиотека оказалась лучше чем Pimple?
          И почему не подходил компонент из Symfony?

          Эти мелочи могут стать важным звеном при оценке библиотек.
        +5
        Долго смотрел на картинку и не мог понять, что же в ней не так :)
          0
          не понимаю в чем преимущество поддерживать такую конфигурацию, вместо того, чтобы все эти сервисы прописать вручную.

          class DIC {
          
          function getService1()
          {
              return new Service1();
          }
          
          function getService2()
          {
              return Serivce2($this->getService1());
          }
          }
          


          Плюсы реализации: никакой магии, всегда знаем что в контейнере, никаких плясок с бубном вокруг конфигурации (почему оно незаинжектилось), можно явно удалять-добавлять сервисы.

          Минусы: чуть сложнее реализовать LazyLoading и… Дописывайте в комментариях )
            +1
            Начинают все с простой реализации, затем добавляют LazyLoading и… через несколько интераций приходят к своему DI. :)
              +1
              Ну просто ваш DIC для меня принципиально ни чем не отличается от обоих DIC, что сделал Фабьен.
              Чего бы лично мне хотелось от DIC: меньше всякой магии. Ибо такие контейнеры подобны фокусникам, что вытаскивают кролика из шляпы.
              0
              Потому что у вас не DI-контейнер, а Service Locator. И это антипаттерн.
                0
                > Потому что у вас не DI-контейнер, а Service Locator. И это антипаттерн.

                Покажите, пожалуйста, недостатки подхода, который предложил Davert, которые отсутствуют при использовании библиотеки, которую, например, предоставил Elfet.
                  0
                  Пусть у меня где-то в каком-то классе идёт следующий код:
                  $dic = new DIC();
                  $service1 = $dir->getService1();
                  

                  Кроме как заменить new DIC() на свой класс унаследованный от DIC не получиться.
                    0
                    Не-не-не, не надо такого кода, это тот же самый Service Locator. Это ничем не отличается от просто фабрики или даже вызовы конструктора и здесь нет инверсии зависимости.

                    blog.ploeh.dk/2010/02/03/ServiceLocatorIsAnAntiPattern.aspx

                    А по поводу кода Davert`a — хороший DI-контейнер еще позволяет управлять жизненным циклом создаваемых объектов и задавать все зависимости в одном месте.
                      0
                      В одном классе = в одном месте. Ну можно расширить класс. А ещё модно всяких смешных трейтов навесить.
                      Всё лучше чем по конфигурациям лазить.
                      0
                      Не совсем понял в чем проблема и что должно было получиться.
                    0
                    Исходя из логики в статье
                    blog.ploeh.dk/2010/02/03/ServiceLocatorIsAnAntiPattern.aspx

                    with Service Locator is that it hides a class’ dependencies, causing run-time errors instead of compile-time errors, as well as making the code more difficult to maintain because it becomes unclear when you would be introducing a breaking change.


                    DIC ничем не лучше.
                      0
                      C DIC у вас зависимости прописаны в конструкторах. В языках со статической типизацией падать будет в compile time.
                        0
                        Стоп, а сервисы всё равно вытаскиваются из контейнера через get.
                        И вот в этот момент get'а будет происходить магия и падение.
                          0
                          Нет. get типизован под конкретный тип. Как именно типизован — зависит от конкретного языка. Но тип есть и его проверка произойдет при компиляции.

                          (я еще раз зачему, что это верно для языков со статической типизацией).
                            +1
                            В Symfony2 get типизирован под… вообщем никак он не типизирован. То же и в Pimple.
                            В чем собственно и печалька.

                            Потому я считаю, что так строить контейнеры нельзя.

                            Из всех вариантов я вижу 2:
                            — статически строить контейнер и дампить в файл. Но чтобы метода доступа к сервисам всегда были публичными.
                            — строить контейнер ручками.

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое