Website development
PHP
May 2016 24

Dependency Injection контейнер от PHPixie

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.
+2
3.6k 28
Comments 21
Top of the day