Подписка На Динамические События Symfony2

Добрый день, хабражитель.

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

Кому интересно, добро пожаловать под кат. Несколько слов о конфигурации проекта: используется Symfony 2.3 + Doctor 2.



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

Система содержит некий реестр всех событий в базе данных.

Используя документацию, мы можем выделить 3 варианта подписки на события в стандартной реализации диспетчера событий symfony.

  1. Добавьте сервис с тегом kernel.event_listener в контейнер DI.
  2. Добавьте в контейнер DI службу с тегом kernel.event_subscriber, реализующую EventSubscriberInterface.
  3. Добавьте вызовы методов addListener во время отправки приложения.

Первый способ нам не подходит, так как мы заранее не знаем названия событий.

Казалось бы, второй метод — именно то, что нужно, но метод getSubscribedEvents должен быть статическим, а значит, не может взаимодействовать с внедренными сервисами.

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

Пришла идея использовать проход компилятора и это решило эту проблему.

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

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

Существует 5 этапов компиляции контейнера:

  • PassConfig::TYPE_BEFORE_OPTIMIZATION
  • PassConfig::TYPE_OPTIMIZE
  • PassConfig::TYPE_BEFORE_REMOVING
  • PassConfig::TYPE_REMOVE
  • PassConfig::TYPE_AFTER_REMOVING
На ранних этапах компилируются определения сервисов, на более поздних удаляются неиспользуемые сервисы и приватные псевдонимы (из документации).



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

Давайте объявим наш компилятор:

  
  
   

/** * Builds the bundle. * * It is only ever called once when the cache is empty. * * This method can be overridden to register compilation passes, * other extensions, .

* * @param ContainerBuilder $container A ContainerBuilder instance */ public function build(ContainerBuilder $container) { parent::build($container); $container->addCompilerPass(new EventsCompilerPass(), PassConfig::TYPE_AFTER_REMOVING); }

и сам код компилятора (код, отвечающий за выборку данных из базы, скрыт в классе репозитория)

public function process(ContainerBuilder $container) { if (!$container->hasDefinition(self::SERVICE_KEY)) { return; } $eventClassName = $container->getParameter(self::EVENT_ENTITY_CLASS_PARAM); $dispatcher = $container->getDefinition(self::DISPATCHER_KEY); $em = $container->get('doctrine.orm.entity_manager'); $eventNames = array(); if ($this->isSchemaSynced($em, $eventClassName) !== false) { $eventNames = $em->getRepository($eventClassName) ->getEventNames(); } foreach ($eventNames as $eventName) { $dispatcher->addMethodCall( 'addListenerService', array($eventName['name'], array(self::SERVICE_KEY, 'process')) ); } }

Как видите, мы выбираем все имена событий и регистрируем наш сервис в качестве обработчика этих событий.

Единственное, что осталось нераскрытым, — это метод isSchemaSynced. Начнем с его реализации:

protected function isSchemaSynced(EntityManager $em, $className) { $tables = $em->getConnection()->getSchemaManager()->listTableNames(); $table = $em->getClassMetadata($className)->getTableName(); return array_search($table, $tables); }

он проверяет, созданы ли таблицы с именами событий.

Дело в том, что при первой компиляции контейнера при вызове сервисной команды Doctor:schema:create он может выдать исключение DBException. Спасибо за внимание, буду рад услышать мнение тех, кто столкнулся с подобной проблемой.

Не судите строго, это мой первый пост о программировании вообще.

Теги: #symfony2 #php #symfony

Вместе с данным постом часто просматривают: