How to use events listeners in Symfony

How event listening works in Symfony

 

The concept of events and listeners is implemented in many frameworks and programming languages. There is a big chance you already have been using it in JavaScript when a user clicks a button and then some code is executed. This is quite similar in Symfony but instead of listening to the user interactions the idea is to listen to the Symfony itself or it's underlying components.

 

This post is focused on Symfony and custom events that you can implement for your own services. Those events are crucial but do not cover all possibilities because there are plenty of events that come with other components and bundles like Doctrine, Console, Forms and many more. Nonetheless if you grasp the process of the event mechanism presented here you will be fine to adopt the rest.

 

The Symfony framework has a specific data flow related to the Request and Response process. Currently there are 8 built-in events related to this flow where you can hook up your custom functions. Without event listeners you could basically execute your code only in controllers. But before the request get to the controller or after it gives the response there are 8 steps you can utilize to change the request itself, terminate its execution, change the controller, change the response, call external API, send email, log something, intercept exceptions and so on. And still it's the basics because your services can produce custom events that you can hook up to not to mention other components and bundles.

 

At first events can be overwhelming but hopefully there is a great tool to analyze their occurrence in the application. Go to the performance tab in the Symfony web profiler. Change threshold to 0 and you will be able to see at least a few built-on events in the timeline for any request. All default HTTP events are related with the kernel (HttpKernel component) that's why their ids start with kernel (i.e. kernel.request).

 

Performance tab profiler in Symfony

 

Additionally, events can pass to your hookup functions specific objects related to events so you can easily apply your changes to the data in the flow. There are two ways to implement listeners in Symfony: Event Listeners and Even Subscribers. Let's take a closer look at how they differ and how to use them.

 

Event listeners vs event subscribers

 

They serve the same purpose but have different implementations and you can use them interchangeably in most cases. Both trigger some functions in the specific time of processing data by Symfony.

 

Event subscriber

 

If it's possible I personally prefer to use event subscribers because they are easier to implement. To make a new subscriber just use the console.

 

symfony console make:subscriber

 

 

You will be asked to give it a name and pass the listening event. Hopefully names of basic events will be suggested, let's focus on kernel ones and choose the kernel.request for starters. You should get the following template.

 

<?php

namespace App\EventSubscriber;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;

class MyFirstSubscriber implements EventSubscriberInterface
{
    public function onKernelRequest(RequestEvent $event)
    {
        // ...
    }

    public static function getSubscribedEvents()
    {
        return [
            'kernel.request' => 'onKernelRequest',
        ];
    }
}

 

 

If you followed my advice of checking the performance tab in the profiler you already know that kernel.request is the first event in the flow you can hook up to. The generated code is separated into two functions. The static one is responsible for registering and mapping functions with events. You can have as many functions as you wish even hooked to the same event. The onKernelRequest is the function that will be executed on the kernel.request event and you can name it differently if you wish to but remember to change mapping accordingly.

Let's start with echoing something on the kernel request event occurrence.

 

public function onKernelRequest(RequestEvent $event)
{
    echo 'I am echo from kernel.request event';
}

 

 

When you hit any route the output from the onKernelRequest function should be visible on the top of the page. It's not very fascinating at the moment but you get the idea how event listeners work. Next let's checkout what RequestEvent objects have to offer.

 

public function onKernelRequest(RequestEvent $event)
{
    dd(event);
}

 

 

After dumping the object you will notice that you have access to a full Request that can be modified, checked or even terminated accordingly to your needs before it passes further to the flow. For example you can block requests that don't originate from a specific IP address.

 

public function onKernelRequest(RequestEvent $event) {
        if ($event->getRequest()->server->get('REMOTE_ADDR') != '127.0.0.1') {
            $event->setResponse(new Response('You are not allowed to enter!'));
        }
}

 

 

Event listener

 

There is no console command that can help generate a listener template so you have to make it manually. First create an EventListener folder in src. Then create a new class preferably that ends with Listener i.e MyFirstListener.

 

namespace App\EventListener;


use Symfony\Component\HttpKernel\Event\RequestEvent;

class MyFirstListener
{

    public function onKernelRequest(RequestEvent $event) {
        \\
    }
    
     public function onKernelRequestBlocker(RequestEvent $event) {
        \\
    }
    
     public function onKernelException(ExceptionEvent $event) {
        \\
    }

}

 

 

Listeners also have to be configured in services.yaml file with tags mapping their events. You can add many functions triggered by different event but if you want to add many functions per one event you will have to specify method in tags as well.

 

\\ services.yaml

App\EventListener\DummyListener:
    tags:
        - { name: kernel.event_listener, event: kernel.request }
        - { name: kernel.event_listener, method: onKernelRequestBlocker,  event: kernel.request }
        - { name: kernel.event_listener, event: kernel.exception }

 

 

Custom events

 

Custom events can be used when you want to listen to your own services. The idea behind custom events is to extend functionality of services without need to extend the class itself. This can be truly helpful when inheritance becomes an awkward solution or simply because the new functionality shouldn't be the responsibility of the service.

For the purpose of understanding custom events let's create a pseudo service SentenceChecker responsible for simple string processing. It has only one method parsing string that excludes not allowed words.

 

<?php

namespace App\Service;

class SentenceChecker
{
    public const NOT_ALLOWED_WORDS = [
        '/uncensored word 1/',
        '/uncensored word 2/',
        '/uncensored word 3/'
    ];
  
    public function parse(string $str): string
    {
        return preg_replace(self::NOT_ALLOWED_WORDS, '***', $str);
    }

}

 

 

If a string contains not allowed words you also want to log it and send it by an email. Instead of making the service too heavy or putting too much into the controller you can register and dispatch a new custom event if the string will have any of the unwanted words.

To dispatch a custom event you have to create it first, preferably in the Event folder.

 

<?php


namespace App\Event;


use Symfony\Contracts\EventDispatcher\Event;

class RestrictedWordEvent extends Event
{

    public const NAME = 'restricted.word';

    protected $str;

    public function __construct($str)
    {
        $this->str = $str;
    }

    public function getStr(): string {
        return $this->str;
    }

}

 

 

Back to the service. In order to dispatch the newly created event, inject EventDispacher in the service and change the parse function accordingly.

 

<?php

namespace App\Service;

use App\Event\RestrictedWordEvent;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

class SentenceChecker
{
    public const NOT_ALLOWED_WORDS = [
        '/uncensored word 1/',
        '/uncensored word 2/',
        '/uncensored word 3/'
    ];

    private $eventDispatcher;

    public function __construct(EventDispatcherInterface $eventDispatcher)
    {

        $this->eventDispatcher = $eventDispatcher;
    }

    public function parse(string $str): string
    {
        $count = 0;
        $str = preg_replace(self::NOT_ALLOWED_WORDS, '***', $str, -1, $count);
        
        if ($count) {
            $event = new RestrictedWordEvent($str);
            $this->eventDispatcher->dispatch($event, RestrictedWordEvent::NAME);
        }
        return $str;
    }

}

 

 

The last thing will be listening to the event. Add a new event to the previously created subscriber and map it with your call back function. That's it! Your custom event is ready.

 

<?php

namespace App\EventSubscriber;

use App\Event\RestrictedWordEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;

class MyFirstSubscriber implements EventSubscriberInterface
{
    public function onKernelRequest(RequestEvent $event)
    {
    // ...
    }

    public function onRestrictedWord($string)
    {
        // log and send email there
    }

    public static function getSubscribedEvents(): array
    {
        return [
            'kernel.request' => 'onKernelRequest',
            RestrictedWordEvent::NAME => 'onRestrictedWord'
        ];
    }
}

 

 

Finally inject the service into a controller.

 

<?php

namespace App\Controller;

use App\Service\SentenceChecker;
use App\Service\WordsChecker;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class MainController extends AbstractController
{
    /**
     * @Route("/{str}", name="main")
     */
    public function index($str, SentenceChecker $checker): Response
    {
        $checker->parse($str);
        // other logic
        return $this->render('main/index.html.twig');
    }
}

 

 

BlowStack 2023