blowstack logo

How to use events listeners in Symfony

Last update

10 min.
  1. How event listening works in Symfony
  2. Event listeners vs event subscribers.
  3. Custom events.

 

 

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 click 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 component and bundles like Doctrine, Console, Forms and many more. Nontheless if you grasp the process how work the event mechanism presented here you will be fine to adpot the rest.

 

Symfony framework has a specific data flow related to 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 listensers you could basically execute your code only in controllers. But before the request get to the controller or after it give the response there are 8 steps you can utilize to change the request itself, terminate it execution, change the controller, change the response, call external API, send email, log something, intercept exeptions and so on. And still it's the basics because your services can produce custom events that your can hook up to not to mention other components and bundles.

 

At first events can be overwhelming but hopefully there is great tool to analyze their occurence in the application. Go to the performace tab in the Symfony web profiler. Change threshold to 0 and you will be able to see at least 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 event can pass to your hook up 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 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 their are easier to implement. To make a new subscriber just use console.

symfony console make:subscriber

 

You will be asked to give it a name and pass the listening event. Hopefully names of basic event will be suggested, let's focus on kernel ones and choose the kenel.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 performace 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 responsilbe for registrering 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 occurence.

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 object has to offer.

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

 

After dumping the object you will notice that you have access to full Request that can be modified, checked or even termianted accordingly to your needs before it's pass further to the flow. For example you can block requests that don't originate from 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 listener template so you have to make it manually. First create EventListener folder in src. Then create 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 specifiy 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 become an awkward solution or simply because the new functionality shouldn't be the responsibility of the service.

For the purpopse of understanding custom events let's create a pseduo service SentenceChecker responsible for simple string processing. It has only one method parsing string that exludes 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 to heavy or putting to 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 = 'rerstricted.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 new event to the previously created subscriber and map it with your call back function. That's it! Yout 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');
    }
}

 

 

Recent posts

BlowStack 2021
Portfolio Cheat
sheets