image
Я не люблю DI контейнеры. Да, они удобны, но со временем с ними возникает куча проблем, поэтому PHPixie использует классический подход с паттерном Factory. Возможность получить любой сервис из контейнера иногда ломает логическую цепочку программы, когда например какой-то валидатор тянет к себе сервис из совсем другого бандла в Symfony2. Еще хуже когда он используется как Service Locator где все зависимости получаются через вызов в стиле Locator::get('doctrine.entityManager'). К тому же различны имплементации контейнеров поощряют хранение конфигурации в YML и XML файлах, что иногда утрудняет отладку. Но недавно я вспомнил фразу «Не думай что разработчик дурак», то есть не стоить навязывать свою точку зрения при разработке архитектуры. К тому же трудно поспорить с тем, что маленькие проекты намного проще строить используя контейнер и/или локатор зависимостей.

Встречайте PHPixie DI.

Сразу стоить отметить что в самом фреймворке он не используется, хоть и поставляется с дефолтным бандлом как полностью опциональный компонент. Зависимостей нет, так что использовать можно полностью отдельно от PHPixie.

Конфигруация

Для постройки контейнера надо расширить базовый класс и переопределить в нем метод configure, например:

class Container extends \PHPixie\DI\Container\Root
{
    public function configure()
   {
       // простое значение по ключу
       $this->value('apiToken', '1234567890');

       // динамическое определение метода
       $this->callback('addFive', function($a, $b) {
           return $a + $b;
       });

       // эта функция будет вызвана только раз,
       // что подходит для построения сервисов
       $this->build('twitterService', function() {
           return new TwitterService($this->apiToken());
           // или
           return new TwitterService($this->get('apiToken'));
       });

       // также сервисы можно строить так:
       $this->instance('twitterService', TwitterService::class, ['@apiToken']);
       // заметьте что параметры начинающиеся с '@' будут заменены на их значения в контейнере

      // добавим группу
      $this->group('user', function() {
          $this->instance('repository', UserRepository::class, ['@twitterService']);
      });
   }
}

// строим контейнер
$container = new Container();

// Получение из контейнера
$container->get('apiToken');
$container->apiToken();

// Статические методы доступны только
// после вызова конструктора
Container::apiToken();
Container::get('apiToken');

// Вызов метода
// Все это также работает через статические методы

$container->add(6, 7); // 13
$container->call('add', [6, 7]);
$callable = $container->get('add');
$callable(6, 7);

// Обращение к подгрупам
$container->get('user.repository');

$userGroup = $container->user();
$userGroup->repository();

Container::user()->repository();
// итд...


Как дополнительный бонус используя контейнер можно доступится к методам классов, например допустим в классе TwitterService существует метод getTweets, тогда можно сделать вот так:

$container->get('twitterService.getTweets'); // $container->twitterService()->getTweets();

// или даже
$container->call('twitterService.getTweets.first.delete', [true]); // $container->twitterService()->getTweets()->first()->delete(true);

// Работает также через статический вызов


Кстати все методы value, callback, build и instance обьявлены как protected. Так что после построение контейнера к нему ничего нельзя будет добавить или изменить, что защитит вас от возможности выстрелить себе в ногу изменение контейнера на лету (отладка тогда очень неприятная). Но если все таки надо будет конфигурировать его извне, всегда можно сделать из публичными. Кстати «конфигурация только изнутри» одна из моих любимых фич.

Подсказки в IDE
Тут стоить упомянуть что есть возможность добавить в класс аннотации которые позволят большинству IDE подсказывать вам имена методов:

/**
 * @method TwitterService twitterService()
 * @method static TwitterService twitterService()
 */
class Container
{
    //...
}


Использование с PHPixie

Создаете в вашем бандле класс контейнера:

namespace Project\App;

// Расширяем другой базовый класс, в котором уже
//  зарегистрированы несколько полезных вещей
class Container extends \PHPixie\DefaultBundle\Container
{
    public function configure()
    {
          //....
          parent::configure();
    }
}


И добавляем его в Builder:

namespace Project\App;

class Builder extends \PHPixie\DefaultBundle\Builder
{
    protected function buildContainer()
    {
         return new Container($this);
    }

}


Builder автоматически создаст инстанс контейнера, так что можно смело сразу использовать статические методы. Ну и несколько примеров:

$container = $builder->container();

$container->get('components.orm');
$query = $container->call('components.orm.query', ['user']);

$builder = Container::builder();
$frameworkBuilder = Container::frameworkBuilder();


Надеюсь новый компонент вам понравится и вы добавите «phpixie/di»: "~3.0" в свой composer.json.
Поделиться с друзьями
-->

Комментарии (21)


  1. OnYourLips
    24.05.2016 21:36
    +2

    Посмотрел код и увидел, что вы автор этой библиотеки. Поэтому задам пару вопросов лично.

    1. Чем не угодили популярные DI-библиоткеи? Зачем изобретать очередной велосипед? Тем более такой примитивный (кода меньше, чем объем этой статьи).
    2.Почему этот материал написан не в виде части документации, а в виде отельной статьи? Пользователям фреймворка так будет гораздо удобнее его найти.
    3. Попробуйте CodeSniffer. Очень полезная утилита, вам поможет. Без неё код выглядит небрежно из-за мелких недочётов.


    1. jigpuzzled
      24.05.2016 22:46

      1) Объясню на примере Pimple, поскольку он довольно популярный:

      • Интерфейс ArrayAccess, прост в использование но выглядит криво в ООПшном коде.
      • Настраивается извне, что как я уже писал выше мне не нравится. Я хочу чтобы контейнер инкапсулировал свою логику
      • Нет статического интерфейса. Как по мне, кому-то то это тоже понадобится
      • Нет доступа к глубоким значением по точке, что выглядит не так красиво как с ней
      • Поскольку для вложенности надо создавать несколько контейнеров, то в случае если сервиса нет, вы получите только имя последнего ключа (например просто repository) вместо полного (user.repository).
      • Нет аналога $this->instance()


      Если сдается примитивным, пишите которые фичи вам надо, может и появляться =)

      2. Я всегда сначала пишу статью на хабр, смотрю фидбек, перевожу на англ и тогда уже ставлю в доку. Так что статья это бета тест доков ))

      3. Спасибо, на самом деле у меня PHPStorm, но вот как раз эту либу писал в Атоме


      1. OnYourLips
        25.05.2016 00:30
        +1

        > Объясню на примере Pimple
        А почему не на примере symfony/dependency-injection? ИМХО самое красивое и качественное решение в современном PHP.
        Оно не только надёжное и проверенное временем, но ещё и крайне гибкое, что позволяет конфигурировать его даже из yml или xml конфигов.

        > Настраивается извне, что как я уже писал выше мне не нравится. Я хочу чтобы контейнер инкапсулировал свою логику
        Поясните, что вы конкретно имеете ввиду, используя устоявшуюся терминологию.

        > Нет статического интерфейса. Как по мне, кому-то то это тоже понадобится
        Это ухудшает взаимодействие и опускает DI до уровня Service Locator или синглтонофасадов Laravel.

        Остальные проблемы свойственны только pimple, а не компоненту symfony.


        1. jigpuzzled
          25.05.2016 01:12

          > А почему не на примере symfony/dependency-injection?

          как я писал в самом начале статьи, совсем не люблю конфигурацию зависимостей в конфиг файлах, а настройка его через PHP более громоздка, e.g.:

          $container
              ->register('newsletter_manager', 'NewsletterManager')
              ->addArgument(new Reference('mailer'));
          
          // вместо
          $this->instance('newsletter_manager', NesletterManager::class, ['@mailer']);
          


          > Поясните, что вы конкретно имеете ввиду, используя устоявшуюся терминологию.

          Настраивать PHPixie DI можно только изнутри самого класса во время создания екземпляра через protected методы. То нельзя просто так взять и добавить новое значение где-то потом в рантайме, как с Pimple:

          // может случится где угодно
          $container['session_storage'] = function ($c) {
              return new SessionStorage('SESSION_ID');
          };
          


          > Это ухудшает взаимодействие и опускает DI до уровня Service Locator или синглтонофасадов Laravel.

          Как я уже писал вначале статьи, я считаю что контейнеры сильно проигрывают стандартным классам фабрик, и этот компонент написан на случай «если кому-то очень хочется контейнер». Если разработчик сам вправе решать использовать контейнер или нет, он также должен иметь право использовать синглофасады если захочет. Конечно же никто не мешает просто их не использовать если не хочется.

          Я не спорю конечно что компонент симфони в совсем другой лиге. PHPixie DI больше создает конкуренцию легковесным контейнерам типа Пимпл и Аура


          1. OnYourLips
            25.05.2016 02:04
            +1

            > как я писал в самом начале статьи, совсем не люблю конфигурацию зависимостей в конфиг файлах

            Вас никто не заставляет это делать. Это просто один из вариантов для тех, кто предпочитает конфиги. А те, кто предпочитает код, пишут в коде.

            > а настройка его через PHP более громоздка

            Только вы допустили ошибку. Если строка начинается с @, то ваш код перестанет работать.
            И эта «громоздкость» необходима как раз для защиты от подобных ситуаций.

            > Настраивать PHPixie DI можно только изнутри самого класса во время создания екземпляра через protected методы. То нельзя просто так взять и добавить новое значение где-то потом в рантайме, как с Pimple:

            Аналогично и с symfony DI. Ваша ошибка в том, что вы сравниваете свой продукт не с продакшн-решением (symfony DI), а с простейшей библиотекой для понятия теории новичками (pimple).

            > Как я уже писал вначале статьи, я считаю что контейнеры сильно проигрывают стандартным классам фабрик, и этот компонент написан на случай «если кому-то очень хочется контейнер»

            Не проигрывают, дело в принципах применения. Когда надо что-то грязно и быстро заговнокодить, то используют SL. DI для длительных проектов, которые будут поддерживать десятки человеколет.


        1. Big_Shark
          25.05.2016 05:59
          +2

          Лучше рассматривать PHP-Di как пример чем symfony/dependency-injection.


          1. jigpuzzled
            25.05.2016 11:11

            он то как раз слишком много умеет, ему нужен кеш, он допускает возможность создавать новый инстанс сервисов через make(), в то время как в пикси это конфигурация решает будет ли возвращаться новый инстанс. Сам по себе PHPixie DI более строгий. Как я уже писал в другом комменте, выбор контейнера дело вкуса, этот DI достаточно отличается от других чтобы не быть клоном, а уже каким пользоваться зависит только от того какой стиль конфигурации и интерфейса вам больше нравится.


  1. L0NGMAN
    25.05.2016 00:08
    +1

    Мне очен нравится ларавеловская импелементация Illuminate/Container
    Не вижу никаких плюсов по сравнение с ним


  1. MetaDone
    25.05.2016 08:54
    +1

    Посмотрите на https://github.com/auraphp/Aura.Di — много полезных фишек можно взять на вооружение. С кучей зависимостей приложение есть не более 2мб памяти из-за ленивой загрузки, можно настроить авто-разруливание зависимостей через контроль типов и не париться. В случае Pimple style — контейнеров не имеет значения, нужен Вам сервис сейчас или нет, он все равно будет инициализирован и будет висеть и есть ресурсы.


    1. jigpuzzled
      25.05.2016 09:18

      В PHPixie DI и так все работает как lazy в Aura, даже ->instance(). А вот авто разруливание сомнительная фича имхо, к тому же в случае если конструкторы принимвют интерфейсы, то настараивать какой сервис использовать вместо интерфейса все равно надо вручную. К тому же аура использует подход с ArrayAccess как и Pimpl.

      На самом деле все это дело вкуса, так как самое важное это насколько вам нравится работа с контйнером. Я думаю phpixie di достаточно отличается своим интерфейсом от других чтобы не быть велосипедом, а вот какой интерфейс работы нравится больше уже решать вам.


      1. Fesor
        25.05.2016 21:34

        А вот авто разруливание сомнительная фича имхо, к тому же в случае если конструкторы принимвют интерфейсы, то настараивать какой сервис использовать вместо интерфейса все равно надо вручную

        Так сложно задать мэпу соответствий интерфейс => реализация, прям уж никак. Что до автосвязывания — это на самом деле очень мощная штука, в том плане что разработчики обычно ленивы, а так вся эта "магия" не будет им мешать делать больше объектов вместо пары жирных "что бы не конфигурить ничего".


        К тому же аура использует подход с ArrayAccess как и Pimpl.

        мы точно про dependency injection? ну мол я к тому что какая разница если в нашем коде контейнер использоваться не должен.


        1. jigpuzzled
          25.05.2016 23:05

          Ну у меня тут явно комплексный вариант, как хочешь так и используй, хоть как и статический локатор. Автоваиринг еще не нравится тем что можно инжекнуть как я писал в начале статьи: совсем далекое из совсем другого концы системы. Именно поэтому у меня фабрики, каждой доступно только то к чему у нее есть доступ, то есть нельзя так просто взять и всунуть в класс валидации какой-то сервис с другого бандла. Как по мне тогда нарушается модулярность кода. То есть если вот у меня есть ОРМкак то только ее или ее репозитории можно куда-то инжектить, а не какой-то внутренний ее сервис.

          Если делать автоваиринг, то ваш разработчик может смело получить доступ к сервису, к которому скорее всего ему совсем не нужно обращаться. И как раз фабрики позволяют лучше это организовать.


          1. Fesor
            26.05.2016 10:31

            Если делать автоваиринг, то ваш разработчик может смело получить доступ к сервису, к которому скорее всего ему совсем не нужно обращаться.

            А если не делать этот же разработчик будет меньше дробить систему, потому что лень делать фабрики. Проходили.


            Я эту проблему решил при помощи deptrac который просто не даст закоммитить такой код.


            1. L0NGMAN
              26.05.2016 14:37

              За deptrac спасибо, не знал такое :)


  1. rsvasilyev
    25.05.2016 09:23
    +1

    jigpuzzled, вы еще долго собираетесь пиарить свой кривой велосипед?
    Вас уличили в накрутке инсталлов на packagist, в создании фейковых аккаунтов на reddit, вы внесли смуту в PHP-FIG. Остановитесь уже, а?

    https://www.reddit.com/r/PHP/comments/4iju97/95_of_phpixie_installs_on_packagist_are_fraudulent/
    https://twitter.com/hashtag/phpixie


  1. andrewnester
    25.05.2016 12:12
    +2

    В целом контейнер неплохой, идея конфигурации только изнутри мне тоже нравится, за это плюс.

    Но как мне кажется, подойдёт для не очень больших проектов, расскажу почему.

    Условно в большом проекте у меня 50 классов с бизнес-логикой, в которых там что-то инжекститься.
    Сами же эти сервисы инжектятся в контроллер, что-то в духе

    class SomeController
    {
    
        /**
         * @var SomeLogicService $service
         */
        private $service;
        
        public function __construct(SomeLogicService $service)
        {
            $this->service = $service;
        }
    }
    
    class SomeLogicService
    {
        /**
         * @var AnotherLogicService $service
         */
        private $service;
    
        public function __construct(AnotherLogicService $service)
        {
            $this->service = $service;
        }
    }
    


    Без auto-wiring мне придётся все 50 классов описывать в контейнере, в том числе и контроллер. А это несколько неудобно.
    С auto-wiring остаточно описать контроллер в контейнере.
    Так это работает в Laravel и Symfony (по крайней мере я так это использую)


    1. jigpuzzled
      25.05.2016 12:41

      На самом деле во всех своих проектах я использую фабрики, так что мне все равно приходится руками описывать как строить контроллеры. Автоваиринг конечно удобно, но тогда надо еще рефлексию парсить и как-то кешировать метаданные. Если будете использовать ->instance() метод, то это только одна строчка конфигурации на контроллер, что не так уж и много, чтобы ради этого тянуть парсер и кеш. К тому же этот кеш еще надо читать и парсить на каждом запросе, а такой контейнер как в PHPixie DI закешируеться самим opcache PHP.

      Но как я писал, все это дело вкуса, это лишь еще одна альтернатива. И поскольку он опциональный, то вы смело можете использовать любой другой DI компонент с PHPixie фреймворком.


      1. Fesor
        25.05.2016 21:40

        На самом деле во всех своих проектах я использую фабрики

        А зачем если эти фабрики можно сгенерировать (так собственно симфони и делает). Ну то есть я понимаю, все явно, все круто, но мне это не интересно как потенциальному пользователю.


        но тогда надо еще рефлексию парсить и как-то кешировать метаданные.

        Никаких проблем, опциональный кэш, или кодогенерация и тогда все будет делать за нас opcache. И ставить ничего дополнительно не нужно.


        1. jigpuzzled
          25.05.2016 22:58

          Ну если кодогенерация то да, с ней спорить трудно


    1. Fesor
      25.05.2016 21:36

      С auto-wiring остаточно описать контроллер в контейнере.

      https://github.com/Symplify/ControllerAutowire — прошу, можно и контроллеры не описывать. Нужно будет только для интерфейсов имплементации обозначить и если декорацией увликаетесь.


      1. andrewnester
        26.05.2016 00:16

        ну библиотека ведь всё равно вместо нас их описывает ;) https://github.com/Symplify/ControllerAutowire/blob/master/src/DependencyInjection/Compiler/RegisterControllersPass.php#L40

        в Laravel 5 такие контроллеры например из коробки идут.
        суть в общем в том была, что так или иначе контроллеры описать придётся при такой схеме, а вот всё остальное уже «автовайриться» будет