Event Listeners

By the end of this lesson, your site will increment an access counter for the number of times a post has been read, implemeneted using a custom event you crafted. Along the way, we’ll be:

  • Creating a code hook which allows for future functionality to be introduced without touching the code
  • Creating an event listener to automatically modify data objects

Follow along using the code available from this GIT repository.
Start - micro_services
End - event_listeners

Event Listeners

Event listeners are pieces of code that respond to milestones within the execution cycle. These milestones, to the code are known as event dispatchers and they are used in order to extend functionality without having to modify the original code.

In the Standard Edition, there are already a number of event listeners setup for you. Before continuuing, check what listeners you have setup by running the following commands.

  1. # Check what listeners are registered in the development environment
  2. php bin/console debug:event-dispatcher --env=dev
  3. # Check what listeners are registered in the production environment
  4. php bin/console debug:event-dispatcher --env=prod

Do note that it is likely that there are many more dispatchers that exist than what appear on screen, just that they are not being used. To the best of my current knowledge, there is unfortunately no easy method for getting a comprehensive list of all the available listeners so. The closest you can get is to inspect the container services and then investigate manually. Debugging the service container and showing only listeners can be done with the following command.

  1. php bin/console debug:container --show-private | grep -i "listener"

Creating a dispatcher

The first part of creating a dispatcher means creating a event class which defines the event name and some methods that will be available to all listeners when fired. It is worth mentioning that it isn’t 100% necessary to create an event class if you don’t need to pass any additional data to the listeners. You could use the default event class but, in this case, as we want access to the entity we will be using a custom class.

  1. <?php
  2. // src/AppBundle/Event/PostFetchedEvent.php
  3.  
  4. namespace AppBundle\Event;
  5.  
  6. /* The event we'll be extending */
  7. use Symfony\Component\EventDispatcher\Event;
  8. use AppBundle\Entity\Post;
  9.  
  10. class PostFetchedEvent extends Event
  11. {
  12.     /* This is the internal reference for the event name */
  13.     const NAME = 'post.fetched';
  14.  
  15.     /* Listeners will be extending this class so, we use protected */
  16.     protected $post;
  17.  
  18.     /* Cast the post to object property on initiation */
  19.     public function __construct(Post $post)
  20.     {
  21.         $this->post = $post;
  22.     }
  23.     /*
  24.     * Any methods contained within the event are available
  25.     * to the event listeners / subscribers
  26.     */
  27.     public function getPost()
  28.     {
  29.         return $this->post;
  30.     }
  31. }

The event class is now defined but, it isn’t being used; the next step is to create an event dispatcher. An event dispatcher is placed within the code at the point we want the plugged in code to fire.

Symfony provides an interface for communicating with the event dispatcher so, the first thing we want to do is inject it in to the blog manager we created earlier. Start, by editing the service definition and adding the Symfony event dispatcher service as an extra argument to the blog manager:

  1. services:
  2.   app.service.blog_manager:
  3.     class: AppBundle\Service\BlogManager
  4.     arguments:
  5.       - '@app.repository.post'
  6.       - '@event_dispatcher'

The next step is to edit the Blog manager to use the event dispatcher and for this we do three things.

  • Include the namespaces for both the eventdispatcher (in order to type enforce the constructor argument) and the event we just created, PostFetchedEvent
  • Modify the constructor function to set the event dispatcher to the object
  • Modify the fetchPost function to use the event.
  1. <?php
  2.  
  3. namespace AppBundle\Service;
  4.  
  5. use Doctrine\Common\Persistence\ObjectRepository;
  6.  
  7. /* Include namespaces for type casting */
  8. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  9.  
  10. /* Include the event definition */
  11. use AppBundle\Event\PostFetchedEvent;
  12.  
  13.  
  14. class BlogManager
  15. {
  16.   protected $postRepository;
  17.  
  18.   /* The event dispatcher interface */
  19.   protected $eventDispatcher;
  20.  
  21.   /* Notice the type forcing of the argument */
  22.   public function __construct(
  23.     ObjectRepository $postRepository,
  24.     EventDispatcherInterface $eventDispatcher)
  25.   {
  26.     $this->postRepository = $postRepository;
  27.  
  28.     /* Set the event dispatcher to an object property */
  29.     $this->eventDispatcher = $eventDispatcher;
  30.   }
  31.   public function fetchPosts()
  32.   {
  33.     return $this->postRepository->findAllOrderedByDate();
  34.   }
  35.   public function fetchPost($id)
  36.   {
  37.     if($post = $this->postRepository->find($id))
  38.     {
  39.       /* Inform the Symfony event listener that there is an event here */
  40.       $event = new PostFetchedEvent($post);
  41.       $this->eventDispatcher->dispatch(PostFetchedEvent::NAME, $event);
  42.       return $post;
  43.     }
  44.   }
  45. }

Creating an event listener

If you followed the above steps, you now have an event dispatcher ready to use. If you were to run the debug:event-dispatcher command mentioned earlier, it wouldn’t show up though. This is because we are not using the event yet.

Code to handle events can be setup using two methods; either an event listener or an event subscriber. The difference between the two is that the definition of which event is being targeted is located within the class when using subscribers (which also means multiple events can be subscribed to) whereas Symfony configuration is used to define the targeted event when using listeners.

The code for the actual event listener is simple, the legwork is mostly carried out by the dispatcher we created earlier so let’s create the class. Notice the method name ends in “ed”; this is quite important internally to Symfony and it will not work if you don’t name it accordingly as an event is supposed to represent an event that just happened.

  1. <?php
  2. // src/AppBundle/Event/Listener/PostAccessCountIncrementListener.php
  3.  
  4. namespace AppBundle\Event\Listener;
  5.  
  6. /* Include namespaces for type enforcement */
  7. use Doctrine\ORM\EntityManager;
  8. use AppBundle\Event\PostFetchedEvent;
  9.  
  10. class PostAccessCountIncrementListener
  11. {
  12.     private $entityManager;
  13.  
  14.     /* Set the entity manager to object */
  15.     public function __construct(EntityManager $entityManager)
  16.     {
  17.       $this->entityManager = $entityManager;
  18.     }
  19.  
  20.     /* The method that will be called */
  21.     public function onPostFetched(PostFetchedEvent $event)
  22.     {
  23.  
  24.         /* Get the post from the event */
  25.         $post = $event->getPost();
  26.  
  27.         /* Increment the access counter */
  28.         $newCountValue = $post->getAccessCounter() + 1;
  29.         $post->setAccessCounter($newCountValue);
  30.  
  31.         /* Save the changes */
  32.         $this->entityManager->flush();
  33.     }
  34. }

The last step is to now define the configuration options and tie the event in to the kernel. The important line in the configuration below is the tag named "kernel.event_listener". Tags imply that a service is targeting something and the we want to target the Symfony event listener.

  1. services:
  2.   app.event.listener.post_access_count_increment:
  3.     class: AppBundle\Event\Listener\PostAccessCountIncrementListener
  4.     tags:
  5.       - { name: kernel.event_listener, event: post.fetched, event: onPostFetch }
  6.     arguments:
  7.       - '@doctrine.orm.entity_manager'

Now, try running the debug:event-dispatcher code earlier and you should see your newly created event registered as shown below.

  1. "post.fetched" event
  2. --------------------
  3.  
  4. ------- ---------------------------------------------------------------------------- ----------
  5.  Order   Callable                                                                     Priority
  6. ------- ---------------------------------------------------------------------------- ----------
  7.  #1      AppBundleEventListenerPostAccessCountIncrementListener::onPostFetched()      0
  8. ------- ---------------------------------------------------------------------------- ----------

Now each time a post is accessed, the access count will increment by 1 automatically.

Exercises

  • Modify your templates to show the accessCounter property and then try reloading the page multiple times to see it count up
  • Convert the event listener in to an event subscriber. Whether people choose to use listeners or subscribers in the long run is really a matter of preference. A listener was used in this post as it only subscribes to a single event. If needing to respond to multiple events,you will need to use subscribers so they are worth learning.
  • Read up on the other types of event dispatcher to get a better idea of what is happening. A better understanding always helps

Communication, our greatest tool

Whether you want to connect, have a problem I can help with or simply want to drop a line, I welcome your words!