📜 ⬆️ ⬇️

Symfony2 subscribe to dynamic events

Good afternoon

Not so long ago I ran into a not very standard task, I would like to share a variant of its solution, as well as learn clever ideas on this topic. Who cares, welcome under cat.

A few words about the project configuration: using symfony 2.3 + doctrine 2

Formulation of the problem

Implement an event handler whose name is not known in advance. The system contains a register of all events in the database.
')
Using the documentation, there are 3 options for subscribing to events in the standard implementation of the symfony event dispatcher.
  1. Add a service with a kernel.event_listener tag to a DI container
  2. Add a DI container service with a kernel.event_subscriber tag that implements EventSubscriberInterface
  3. Add calls to addListener methods during application dispatching


The first method does not suit us because we do not know the names of events in advance. The second way would seem to be exactly what is needed, but the getSubscribedEvents method should be static, which means it cannot interact with injected services.

The third method seemed to me the most logical, but I would not like to add 1 request to each request to the database, so I started looking for a more elegant solution with caching the list of event names.

I got the idea to use the compiler pass, so she solved this problem. During the creation of an instance of the bundle object, we can register a class that will participate in the compilation of the DI container. One important question that remains is how to reach the doctrine, and here we can come to the rescue by compiler pass'ov. There are 5 steps to compile a container:


In the early stages, the service definitions are compiled; in the later stages, unused services and private aliases are removed (from the documentation).

Implementation

We are the last stage, as all services at this stage are ready to use. Let's announce our compiler:
/** * 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); } 


and the code of the compiler (the code responsible for retrieving data from the database is hidden in the repository class)
  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')) ); } } 

as you can see, we select all event names and register our service as an event data handler. The only thing that is left unlighted is the isSchemaSynced method, let's start with its implementation:
  protected function isSchemaSynced(EntityManager $em, $className) { $tables = $em->getConnection()->getSchemaManager()->listTableNames(); $table = $em->getClassMetadata($className)->getTableName(); return array_search($table, $tables); } 

it checks whether tables with event names are created. The thing is that the first time the container is compiled when you call the doctrine: schema: create utility command, it can cause a DBException.

Thank you for your attention, I will be glad to hear the opinions of those who are faced with a similar task.
Strictly do not judge this is my first post about programming in general.

Source: https://habr.com/ru/post/187178/


All Articles