Я не люблю 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)
L0NGMAN
25.05.2016 00:08+1Мне очен нравится ларавеловская импелементация Illuminate/Container
Не вижу никаких плюсов по сравнение с ним
MetaDone
25.05.2016 08:54+1Посмотрите на https://github.com/auraphp/Aura.Di — много полезных фишек можно взять на вооружение. С кучей зависимостей приложение есть не более 2мб памяти из-за ленивой загрузки, можно настроить авто-разруливание зависимостей через контроль типов и не париться. В случае Pimple style — контейнеров не имеет значения, нужен Вам сервис сейчас или нет, он все равно будет инициализирован и будет висеть и есть ресурсы.
jigpuzzled
25.05.2016 09:18В PHPixie DI и так все работает как lazy в Aura, даже ->instance(). А вот авто разруливание сомнительная фича имхо, к тому же в случае если конструкторы принимвют интерфейсы, то настараивать какой сервис использовать вместо интерфейса все равно надо вручную. К тому же аура использует подход с ArrayAccess как и Pimpl.
На самом деле все это дело вкуса, так как самое важное это насколько вам нравится работа с контйнером. Я думаю phpixie di достаточно отличается своим интерфейсом от других чтобы не быть велосипедом, а вот какой интерфейс работы нравится больше уже решать вам.Fesor
25.05.2016 21:34А вот авто разруливание сомнительная фича имхо, к тому же в случае если конструкторы принимвют интерфейсы, то настараивать какой сервис использовать вместо интерфейса все равно надо вручную
Так сложно задать мэпу соответствий интерфейс => реализация, прям уж никак. Что до автосвязывания — это на самом деле очень мощная штука, в том плане что разработчики обычно ленивы, а так вся эта "магия" не будет им мешать делать больше объектов вместо пары жирных "что бы не конфигурить ничего".
К тому же аура использует подход с ArrayAccess как и Pimpl.
мы точно про dependency injection? ну мол я к тому что какая разница если в нашем коде контейнер использоваться не должен.
jigpuzzled
25.05.2016 23:05Ну у меня тут явно комплексный вариант, как хочешь так и используй, хоть как и статический локатор. Автоваиринг еще не нравится тем что можно инжекнуть как я писал в начале статьи: совсем далекое из совсем другого концы системы. Именно поэтому у меня фабрики, каждой доступно только то к чему у нее есть доступ, то есть нельзя так просто взять и всунуть в класс валидации какой-то сервис с другого бандла. Как по мне тогда нарушается модулярность кода. То есть если вот у меня есть ОРМкак то только ее или ее репозитории можно куда-то инжектить, а не какой-то внутренний ее сервис.
Если делать автоваиринг, то ваш разработчик может смело получить доступ к сервису, к которому скорее всего ему совсем не нужно обращаться. И как раз фабрики позволяют лучше это организовать.Fesor
26.05.2016 10:31Если делать автоваиринг, то ваш разработчик может смело получить доступ к сервису, к которому скорее всего ему совсем не нужно обращаться.
А если не делать этот же разработчик будет меньше дробить систему, потому что лень делать фабрики. Проходили.
Я эту проблему решил при помощи deptrac который просто не даст закоммитить такой код.
rsvasilyev
25.05.2016 09:23+1jigpuzzled, вы еще долго собираетесь пиарить свой кривой велосипед?
Вас уличили в накрутке инсталлов на 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
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 (по крайней мере я так это использую)jigpuzzled
25.05.2016 12:41На самом деле во всех своих проектах я использую фабрики, так что мне все равно приходится руками описывать как строить контроллеры. Автоваиринг конечно удобно, но тогда надо еще рефлексию парсить и как-то кешировать метаданные. Если будете использовать ->instance() метод, то это только одна строчка конфигурации на контроллер, что не так уж и много, чтобы ради этого тянуть парсер и кеш. К тому же этот кеш еще надо читать и парсить на каждом запросе, а такой контейнер как в PHPixie DI закешируеться самим opcache PHP.
Но как я писал, все это дело вкуса, это лишь еще одна альтернатива. И поскольку он опциональный, то вы смело можете использовать любой другой DI компонент с PHPixie фреймворком.Fesor
25.05.2016 21:40На самом деле во всех своих проектах я использую фабрики
А зачем если эти фабрики можно сгенерировать (так собственно симфони и делает). Ну то есть я понимаю, все явно, все круто, но мне это не интересно как потенциальному пользователю.
но тогда надо еще рефлексию парсить и как-то кешировать метаданные.
Никаких проблем, опциональный кэш, или кодогенерация и тогда все будет делать за нас opcache. И ставить ничего дополнительно не нужно.
Fesor
25.05.2016 21:36С auto-wiring остаточно описать контроллер в контейнере.
https://github.com/Symplify/ControllerAutowire — прошу, можно и контроллеры не описывать. Нужно будет только для интерфейсов имплементации обозначить и если декорацией увликаетесь.
andrewnester
26.05.2016 00:16ну библиотека ведь всё равно вместо нас их описывает ;) https://github.com/Symplify/ControllerAutowire/blob/master/src/DependencyInjection/Compiler/RegisterControllersPass.php#L40
в Laravel 5 такие контроллеры например из коробки идут.
суть в общем в том была, что так или иначе контроллеры описать придётся при такой схеме, а вот всё остальное уже «автовайриться» будет
OnYourLips
Посмотрел код и увидел, что вы автор этой библиотеки. Поэтому задам пару вопросов лично.
1. Чем не угодили популярные DI-библиоткеи? Зачем изобретать очередной велосипед? Тем более такой примитивный (кода меньше, чем объем этой статьи).
2.Почему этот материал написан не в виде части документации, а в виде отельной статьи? Пользователям фреймворка так будет гораздо удобнее его найти.
3. Попробуйте CodeSniffer. Очень полезная утилита, вам поможет. Без неё код выглядит небрежно из-за мелких недочётов.
jigpuzzled
1) Объясню на примере Pimple, поскольку он довольно популярный:
Если сдается примитивным, пишите которые фичи вам надо, может и появляться =)
2. Я всегда сначала пишу статью на хабр, смотрю фидбек, перевожу на англ и тогда уже ставлю в доку. Так что статья это бета тест доков ))
3. Спасибо, на самом деле у меня PHPStorm, но вот как раз эту либу писал в Атоме
OnYourLips
> Объясню на примере Pimple
А почему не на примере symfony/dependency-injection? ИМХО самое красивое и качественное решение в современном PHP.
Оно не только надёжное и проверенное временем, но ещё и крайне гибкое, что позволяет конфигурировать его даже из yml или xml конфигов.
> Настраивается извне, что как я уже писал выше мне не нравится. Я хочу чтобы контейнер инкапсулировал свою логику
Поясните, что вы конкретно имеете ввиду, используя устоявшуюся терминологию.
> Нет статического интерфейса. Как по мне, кому-то то это тоже понадобится
Это ухудшает взаимодействие и опускает DI до уровня Service Locator или синглтонофасадов Laravel.
Остальные проблемы свойственны только pimple, а не компоненту symfony.
jigpuzzled
> А почему не на примере symfony/dependency-injection?
как я писал в самом начале статьи, совсем не люблю конфигурацию зависимостей в конфиг файлах, а настройка его через PHP более громоздка, e.g.:
> Поясните, что вы конкретно имеете ввиду, используя устоявшуюся терминологию.
Настраивать PHPixie DI можно только изнутри самого класса во время создания екземпляра через protected методы. То нельзя просто так взять и добавить новое значение где-то потом в рантайме, как с Pimple:
> Это ухудшает взаимодействие и опускает DI до уровня Service Locator или синглтонофасадов Laravel.
Как я уже писал вначале статьи, я считаю что контейнеры сильно проигрывают стандартным классам фабрик, и этот компонент написан на случай «если кому-то очень хочется контейнер». Если разработчик сам вправе решать использовать контейнер или нет, он также должен иметь право использовать синглофасады если захочет. Конечно же никто не мешает просто их не использовать если не хочется.
Я не спорю конечно что компонент симфони в совсем другой лиге. PHPixie DI больше создает конкуренцию легковесным контейнерам типа Пимпл и Аура
OnYourLips
> как я писал в самом начале статьи, совсем не люблю конфигурацию зависимостей в конфиг файлах
Вас никто не заставляет это делать. Это просто один из вариантов для тех, кто предпочитает конфиги. А те, кто предпочитает код, пишут в коде.
> а настройка его через PHP более громоздка
Только вы допустили ошибку. Если строка начинается с @, то ваш код перестанет работать.
И эта «громоздкость» необходима как раз для защиты от подобных ситуаций.
> Настраивать PHPixie DI можно только изнутри самого класса во время создания екземпляра через protected методы. То нельзя просто так взять и добавить новое значение где-то потом в рантайме, как с Pimple:
Аналогично и с symfony DI. Ваша ошибка в том, что вы сравниваете свой продукт не с продакшн-решением (symfony DI), а с простейшей библиотекой для понятия теории новичками (pimple).
> Как я уже писал вначале статьи, я считаю что контейнеры сильно проигрывают стандартным классам фабрик, и этот компонент написан на случай «если кому-то очень хочется контейнер»
Не проигрывают, дело в принципах применения. Когда надо что-то грязно и быстро заговнокодить, то используют SL. DI для длительных проектов, которые будут поддерживать десятки человеколет.
Big_Shark
Лучше рассматривать PHP-Di как пример чем symfony/dependency-injection.
jigpuzzled
он то как раз слишком много умеет, ему нужен кеш, он допускает возможность создавать новый инстанс сервисов через make(), в то время как в пикси это конфигурация решает будет ли возвращаться новый инстанс. Сам по себе PHPixie DI более строгий. Как я уже писал в другом комменте, выбор контейнера дело вкуса, этот DI достаточно отличается от других чтобы не быть клоном, а уже каким пользоваться зависит только от того какой стиль конфигурации и интерфейса вам больше нравится.