Пользователь
0,0
рейтинг
28 октября 2013 в 13:01

Разработка → Pimple? Не… Не слышал из песочницы

Удивительно, что на Хабре всё ещё нет статей об этом гениальном DI контейнере для PHP.
Почему гениальном? Потому, что весь код этого творения укладывается в 80 строк – маленький объект с большими возможностями.
Контейнер представляет из себя один класс, и его подключение в проект выглядит следующим образом:

require_once '/path/to/Pimple.php';

Создание контейнера так же просто:

$container = new Pimple();

Как и многие другие DI контейнеры, Pimple поддерживает два вида данных: сервисы и параметры.


Объявление параметров


Объявить параметры в Pimple очень просто: используем контейнер как простой массив:

// Объявляем параметр
$container['cookie_name'] = 'SESSION_ID';
$container['session_storage_class'] = 'SessionStorage';


Объявление сервисов


Сервис — некий объект, часть системы, которая выполняет свою конкретную задачу.
Например, сервисами могут являться: объект, предоставляющий соединение с базой данных, отвечающий за отправку почты, шаблонизацию выводимых данных и т.д.
В Pimple сервисы определяются как анонимные функции, возвращающие объект сервиса:

// Объявление сервисов
$container['session_storage'] = function ($c) {
  return new $c['session_storage_class']($c['cookie_name']);
};
$container['session'] = function ($c) {
  return new Session($c['session_storage']);
};

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

// Получение объекта сервиса
$session = $container['session'];
// Предыдущая строка равносильна следующему коду
// $storage = new SessionStorage('SESSION_ID');
// $session = new Session($storage);


Объявление сервисов «Синглтонов»


По умолчанию при каждом вызове Pimple возвращает новый объект сервиса. Если же требуется один экземпляр на всё приложение, всё, что вам необходимо сделать – обернуть объявление в метод share():

$container['session'] = $container->share(function ($c) {
  return new Session($c['session_storage']);
});


Объявление функций


Так как Pimple рассматривает все анонимные функции как объявление сервисов, то для объявления именно функций в контейнере необходимо лишь обернуть всё это дело в метод protect():

$container['random'] = $container->protect(function () { return rand(); });


Изменение сервисов после их объявления


В некоторых случаях может понадобиться изменение поведения уже объявленного сервиса. Тогда можно использовать метод extend() для регистрации дополнительного кода, который будет выполнен сразу же после создания сервиса:

$container['mail'] = function ($c) {
  return new \Zend_Mail();
};
$container['mail'] = $container->extend('mail', function($mail, $c) {
  $mail->setFrom($c['mail.default_from']);
  return $mail;
});

Первым параметром в данную функцию передается имя сервиса, которое нужно дополнить, а вторым – функция, принимающая в качестве аргументов объект сервиса и текущий контейнер. В итоге при обращении к сервису получается объект, возвращаемый данной функцией.
Если же сервис был «Синглтоном», необходимо повторно обернуть код дополнения сервиса методом share(), иначе дополнения будут вызываться каждый раз при обращении к сервису:

$container['twig'] = $container->share(function ($c) {
  return new Twig_Environment($c['twig.loader'], $c['twig.options']);
});
$container['twig'] = $container->share($container->extend('twig', function ($twig, $c) {
  $twig->addExtension(new MyTwigExtension());
  return $twig;
}));


Доступ к функции, возвращающей сервис


Каждый раз, когда вы обращаетесь к сервису, Pimple автоматически вызывает функцию его объявления. Если же требуется получить прямой доступ именно к функции объявления, можно использовать метод raw():

$container['session'] = $container->share(function ($c) {
  return new Session($c['session_storage']);
});
$sessionFunction = $container->raw('session');


Повторное использование готового контейнера


Если вы от проекта к проекту используете одни и те же библиотеки, вы можете создать готовые контейнеры для повторного использования. Всё, что нужно сделать – это расширить класс Pimple:

class SomeContainer extends Pimple
{
  public function __construct()
  {
    $this['parameter'] = 'foo';
    $this['object'] = function () { return stdClass(); };
  }
}

И вы можете с лёгкостью использовать данный готовый контейнер внутри другого контейнера:

$container = new Pimple();
// Объявление сервисов и параметров основного контейнера
// ...
// Вставка другого контейнера
$container['embedded'] = $container->share(function () { return new SomeContainer(); });
// Конфигурация встроенного контейнера
$container['embedded']['parameter'] = 'bar';
// И его использование
$container['embedded']['object']->...;


Заключение


Управление зависимостями — одна из важнейших и в то же время трудных задач в разработке веб-приложений. Большинство фреймворков предлагают собственные решения данной проблемы. Однако в случае использования фреймворков без менеджера зависимостей или проектирования архитектуры приложения без фреймворков, в качестве простого и маленького DI контейнера я бы однозначно выбрал Pimple.

P.S. Примеры использования — перевод официального readme Pimple.
@MoonGrate
карма
5,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

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

  • +3
    Про Pimple было уже, как минимум, в контексте Silex + упоминалось в описании Inversion. Кроме последнего еще стоит вспомнить Chernozem.
  • 0
    Или я чего-то не понимаю, или получение сервисов через container['service'] — это паттерн под названием Service Locator, но никак не DI.
    • +1
      Мы можем инъецировать объекты и параметры во время конструирования сервиса.

      В том же примере:

      // Объявление сервисов
      $container['session_storage'] = function ($c) {
        return new $c['session_storage_class']($c['cookie_name']);
      };
      $container['session'] = function ($c) {
        return new Session($c['session_storage']);
      };
      


      сервис session_storage передается в виде аргумента в конструктор класса Session.
      • 0
        Ну вы это вручную делаете на этапе конфигурации. То есть классический Service Locator. DI-контейнер же должен сам разбираться, кому какие аргументы конструктора вбить.
        • 0
          Как раз таки нет. Возьмем DiC Symfony — мы так же в конфигурации сервиса указываем каким аргументом что подавать.

          Для того что бы использовать Pimple полноценно как DiC, а не как Service Locator, просто необходимо и контроллеры приложения обернуть как сервис — появится возможность передавать как аргументы другие сервисы, или назначать их через setter-ы.

          $container['user_controller'] = $container->share(function ($c) {
              return new UserController(
                  $c['session_storage']
              );
          });
          
          • 0
            Ну вот если посмотреть на DI из мира языков с поддержкой reflection, то можно заметить, что они ощутимо иначе работают.

            Предположим, у нас есть вот такой набор классов:
            class FooImpl : IFoo
            {
                Foo(IBar dependency){}
            }
            class BarImpl : IBar
            {
            }
            class Target
            {
                Target(IFoo dependency)
            }
            


            В случае с DI мы делаем примерно так
            //Тут привязки в произвольном порядке
            container.Bind<IFoo>().To<FooImpl>();
            container.Bind<IBar>().To<BarImpl>().InSingletonScope();
            //Просим контейнер создать экземпляр класса, имеющего зависимости.
            container.Resolve<Target>();
            

            Теперь посмотрим на код с использованием паттерна Service Locator:
            locator.Set<IBar>(new BarImpl());
            locator.Set<IFoo>(()=> new FooImpl(locator.Get<IBar>());
            new Target(locator.Get<IFoo>());
            

            Очень похож на ваш, правда?
            У нас появилась необходимость вручную обращаться к локатору за зависимостями при конструировании объектов, а так же необходимость знать, какие зависимости нужны каждому сервису. Это отличительные черты паттерна Service Locator (которому для работы не нужна поддержка вывода информации о типах со стороны языка), из чего я делаю вывод, что в вашем коде используется именно он.
            • –2
              Речь идет о PHP, и у данного языка нет возможности реализовать приведенный вами пример. Service Locator — один из способов реализации DI.

              Согласен, в моем последнем примере $cointainer сам себе играет в роли Service Locator. Конфигурируемые же им объекты получают зависимости либо через конструктор, либо через специально отведенный для этого метод при создании, и в остальном коде нам нет необходимости обращаться к Pimple как с Service Locator.
              • +1
                Ну я не спорю с тем, что применяется именно DI (при «деревянном» использовании локатора классы сами его пинают из конструктора, а локатор представляет из себя статический класс/лежит в глобальной переменной), просто указанный контейнер не является DI-контейнером в полной мере.

                Я глянул доку по PHP, там вроде есть какой-то Reflection. Его точно никак нельзя применить для реализации полноценного DI-контейнера?
                • 0
                  Вполне можно узнать какого типа (класса, интерфейса) аргументы у метода-конструктора через рефлексию и создать по этим названиям интерфейсов объекты. Я реализовывал такой DI на php.
                • 0
                  Да, про невозможность реализовать ваш пример на PHP — был не прав. Через Reflection API такое возможно.

                  Все же я считаю что это только еще один способ реализации DI, и не стал бы их делить на полноценные и неполноценные :)
                  Просто в данном случае необходимые зависимости конфигурируются в самом классе. Удобнее.
                  • +2
                    В моём понимании «полноценный DI-контейнер» — это когда мы говорим контейнеру какому интерфейсу что соответствует, просим создать нужный объект, а со всем остальным он думает без нашего участия. А тут надо во-первых дублировать кучу кода (когда вам надо что-то заинжектить в несколько разных классов), во-вторых помнить, где какие параметры у конструкторов. Неудобно, многословно, тратит время.
                    • 0
                      Конфигурацию контейнера никто не отменял, и вам всеравно придется описывать какому интерфейсу что соответствует. Делать это в отдельных файлах конфигураций или же задавать через api предоставляемое компонентом — дело личное.

                      К слову, разве основным отличием di от service locator не в том, что при использовании service locator мы завязываемся на конкретную имплементацию сервиса? в этом плане pimple все же реализует паттерн dependency injection за счет возможности переопределять сервисы (класс использующий сервис все-равно должен быть завязан только на интерфейс). Хотя я Фаулера не читал, могу и ошибаться. Поправьте если что.
                      • 0
                        что при использовании service locator мы завязываемся на конкретную имплементацию сервиса
                        Нет, не завязываемся, лишь на «интерфейс», который в php достигается утиной типизацией. Строго говоря в статье Pimple используется для Dependency Injection (зависимости всё же приходят через конструктор, а не заполняются в нём через $locator['service']), но сам по себе реализует именно Service Locator, так как представляет собой банальный реестр и не способен самостоятельно создавать инстансы.
                • 0
                  Есть в пхп по крайне мере один нормальный DI — Dice
                • 0
                  По вашим критериям даже спринговый контейнер не является «полноценным». То, что вы описываете — лишь доп фича контейнера, которую несложно реализовать, в том числе в php. Она не может являться базовой так как не покрывает всех вариантов связей (несколько реализаций интерфейса, коллекции и т.п.)
                  • 0
                    Ну не знаю, Windsor, Autofac и Ninject это в состоянии делать, с другими DI-контейнерами, я, увы, не работал.
                    • +1
                      Вопрос не в том умеют или нет, вопрос в том, что эта фича не является фундаментальной для DI-контейнера. Его задача из декларативной конфигурации уметь создавать объект и инжектить разные типы зависимостей. Данный контейнер вполне в состоянии это делать.
                      А Spring я привел просто как самый классический пример реализации контейнера.
                      • 0
                        Я не вижу тут декларативной конфигурации, я вижу тут кучу new и запихиваний в контейнер делегатов (или как в php зовут «указатель на функцию»).
                        • +1
                          Декларативная она по тому, что вы говорите как создать объект, когда он понадобится. Я не вижу принципиальной разницы между
                          $container['session'] = function ($c) {
                            return new Session($c['sessionStorage']);
                          };
                          

                          <component id="session" type="Session">
                            <sessionStorage>${sessionStorage}</emailFormatter>
                          </component>
                          

                          Есть у такой реализации через замыкания (они так в пхп называются) свои минусы, но в целом для легковесного контейнера вполне применимо.
                          • 0
                            Если вы поменяете набор параметров конструктора, то придётся снова лезть в конфигурацию, иначе всё развалится, причём о том, что всё развалилось, вы не узнаете до момента, когда попробуете создать этот объект (если у вас есть интеграционные тесты, они это выловят, но всё же). Вся прелесть нормального DI-контейнера в том, что он такие вещи разруливает и достаточно сообразителен, чтобы создать экземпляр любого нужного класса самостоятельно.
                • 0
                  В Zend Framework 2 пытались сделать полноценный DI с Reflection. Получилось настолько медленно, что сами создатели рекоммендуют использовать его только для экспериментов.
                  • +4
                    Это легко решается «кэшированием», что, например, делает Symfony2 DI под названием «скомпилированный контейнер»
                  • 0
                    А что мешало кэш соорудить? Если один раз собрать необходимую информацию, а потом из него брать, должно летать, нет?
                • 0
                  Посмотрите реализацию DI для Magento 2 github.com/magento/magento2/tree/master/lib/Magento/ObjectManager
                  Он соответствует вашим критериям DI, а именно в конфигурации DI просто говорится какому интерфейсу соответствует какая реализация, а клиентский код напрямую с DI контейнером вообще не работает, просто указывает все сови зависимости в конструкторе. DI контейнер же, узнает о всех зависимостях через декларацию конструкторов и создает объекты либо на лету, либо по разанее «скомпилированной» декларации. Как-то так github.com/magento/magento2/blob/master/lib/Magento/ObjectManager/Factory/Factory.php
              • 0
                В php есть и рефлексии и тайп хинтинг для объектов, так что можно реализовать без проблем. Другой вопрос зачем. Суть pimple в том, что бы быть максимально простым, влазить в минимум строк кода так сказать. Добавление рефлексий при отсутствии системы кеширования сделают его крайне медлительным и увеличат время инициализации сервисов. Добавление рефлекций и кеширование этого всего превратит pimple в symfony/dependency-injection или любую другую здоровую имплементацию.
                • +2
                  Компонент должен быть простым в использовании, а не в реализации, нет?
              • +1
                Строго говоря, и DI, и SL — это способы реализации более общего паттерна IoC, Inversion of Control. Pimple, как тут уже сказали, ближе к SL, с минимальной обвязкой для создания «провайдеров» в виде анонимных функций. Зависимости, как видно в статье, всё равно придётся разрешать вручную. Полноценный DI же разрешает зависимости автоматически, как правило, с учётом полифорфизма подтипов (как раз те привязки интерфейсов к реализациям). Не могу сказать, насколько это реализуемо в PHP, поскольку я его не знаю, но в языках вроде Java для этого приходится использовать отражение. Языки с более продвинутой системой типов дают дополнительные средства, вроде self-type annotations в Scala.
                • +1
                  В PHP тоже есть Reflection и он с успехом используется для написания DI контейнеров. Например, в ZF2, Symfony 2, Magento 2
  • 0
    Не спорю, что упоминалось, и были показаны некоторые возможности, но считаю что Pimple достоин отдельной статьи.
  • 0
    Мне одному кажется, что в DI зависимости нужно привязывать к интерфейсу, т.е. интерфейс -> класс реализация, а не к строковому значению, и инстанцировать объекты через конструктор, указывая в нем названия интерфейсов в параметрах.
    • 0
      Можете пояснить? В конструкторе/сеттере обычно прописывается интерфейс (тайп хинтинг поддерживается с версии 5,3), да. То есть нету смысла в di если вы все-равно привязываетесь к конкретной реализации. Или вы о чем-то другом?
      • 0
        Смысл в DI есть, потому что привязки интерфейса к реализации централизуются в одном месте — конфигурации DI-контейнера, где их очень легко и просто менять, а не размазываются по всему коду.
      • 0
        Реализуете один интерфейс и классы, которые реализуют его. Далее в настройках указываете, какой класс для интерфейса использовать. Этот класс легко можно сменить, не меняя названия интерфейса во всех местах в коде. Это и есть основная идея DI. Просто в PHP есть только такие средства это организовать. И кстати, по-моему тайп-хинтинг для классов поддерживался в PHP еще до 5.3.
        • 0
          Это то я понимаю, просто не сразу понял о каких строковых значениях идет речь.
    • 0
      Проблема в том, что у вас не всегда 1 к 1 связь, поэтому в самом общем случае вам придется вводить уникальные айди. А вещи типа @Autowired делаются уже поверх для упрощения.

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