Добрый день читатель.
Не так давно столкнулся с не особо стандартной задачей, хотел бы поделиться вариантом ее решения, а так же узнать умные мыли по данной теме. Кому интересно, добро пожаловать под кат.
Пару слов о конфигурации проекта: используется Symfony 2.3 + doctrine 2
Постановка задачи
Реализовать обработчик событий имя которых не известно заранее. Система содержит некий реестр всех событий в базе данных.
Пользуясь документацией, можно выделить 3 варианта подписки на события в стандартной реализации symfony event dispatcher'а.
- Добавить в DI контейнер сервис с тегом kernel.event_listener
- Добавить в DI контейнер сервис с тегом kernel.event_subscriber реализующий EventSubscriberInterface
- Добавить вызовы методов addListener во время диспетчеризации приложения
Первый способ нам не подходит так как мы заранее не знаем имена событий. Второй способ казалось бы именно то что нужно, но метод getSubscribedEvents должен быть статичен, а значит взаимодействовать с injected сервисами он не может.
Третий способ показался мне самым логичным, но не хотелось бы добавлять на каждый request еще 1 запрос в базу, поэтому начал искать вариант более изящного решения с кэшированием списка имен событий.
Пришла идея использовать compiler pass вот она то и решила данную задачу. Во время создания экземпляра объекта бандла, мы можем зарегистрировать класс который будет участвовать в компиляции DI контейнера. Остается один важный вопрос как достучаться до доктрины, и тут нам на помощь приходит сортировка compiler pass'ов. существует 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);
}
он проверяет созданы ли таблицы с именами событий. Все дело в том, что первый раз контейнер компилируется при вызове служебной команды doctrine:schema:create, и может вызвать DBException.
Спасибо за внимание, буду рад выслушать мнения тех кто сталкивался с подобной задачей.
Строго не судите это мой первый пост о программировании в целом.
Автор: to0n1