Symfony 4 Workshop

Dispatching and handling events

Theory

Some words about events.

So far we are storing the sales but we are not creating new tickets. Real world applications tend to have processes that can be modelled as a series of "when X happens, do Y". In our case, we might want to do something like:

  • When we receive a sale, we create tickets.
  • When the tickets are created, we send an email to the user.
  • When a sale is closed, we log it.
  • When the room is full, we close the sales.

And so on.

These kind of processes are well expressed with events. Let's see how to use them:

In our application we are only going to create the tickets and store a log message, but it should give you a taste of how to continue.

Let's do it first in the controller. Open our controller and change the section where we process the sales form so it looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
use App\Entity\Ticket;

//...
    if ($form->isSubmitted() && $form->isValid()) {
        $em = $this->getDoctrine()->getManager();
        $em->persist($sale);
        
        $numTickets = $form['numTickets']->getData();
        for ($i = 0; $i < $numTickets; $i++) {
            $ticket = new Ticket();
            $ticket->setMovie($movie);
            $ticket->setSale($sale);
            $ticket->setRow(1);
            $ticket->setSeat(1);
            $em->persist($ticket);
        }
        $em->flush();
    
        $this->addFlash(
            'notice',
            'Thank you for your sale!'
        );
    
        return $this->redirectToRoute('index');
    }

This should be pretty straightforward. We are extracting the form data for numTickets and using it to create that number of tickets.

This works (try it) and it is ok, but as we add more and more code when processing a sale, our controller will grow and grow and start looking like spaghetti.

So we can refactor it to use Events. We need to create two new classes, an Event, that holds the information about what just happened, and a EventListener that will react to the Event when it is dispatched.

Create two new directories: src/Event and src/EventListener.

Write this code in src/Event/SaleEvent.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?php

namespace App\Event;

use Symfony\Component\EventDispatcher\Event;
use App\Entity\Sale;
use App\Entity\Movie;

class SaleEvent extends Event
{
    const NAME = 'sale.created';

    protected $sale;

    public function __construct(Sale $sale, Movie $movie, int $numTickets)
    {
        $this->sale = $sale;
        $this->movie = $movie;
        $this->numTickets = $numTickets;
    }

    public function getSale()
    {
        return $this->sale;
    }

    public function getMovie()
    {
        return $this->movie;
    }

    public function getNumTickets()
    {
        return $this->numTickets;
    }
}

As you see, events are basically holders of parameters with not much logic in them.

The logic goes into the EventListener. Create this file in src/EventListener/SaleListener.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php

namespace App\EventListener;

use App\Event\SaleEvent;
use App\Entity\Ticket;
use Doctrine\ORM\EntityManagerInterface;

class SaleListener
{
    public function __construct(EntityManagerInterface $em)
    {
        $this->em = $em;
    }

    public function onSaleCreated(SaleEvent $event)
    {
        for ($i = 0; $i < $event->getNumTickets(); $i++) {
            $ticket = new Ticket();
            $ticket->setMovie($event->getMovie());
            $ticket->setSale($event->getSale());
            $ticket->setRow(1);
            $ticket->setSeat(1);
            $this->em->persist($ticket);
        }
    }
}

This is basically the same that we wrote in our controller. Let's rewrite the controller to use the event (lines 11-15):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
use App\Event\SaleEvent;

//...

if ($form->isSubmitted() && $form->isValid()) {
    $em = $this->getDoctrine()->getManager();
    $em->persist($sale);

    $numTickets = $form['numTickets']->getData();

    $this->get('event_dispatcher')
        ->dispatch(
            SaleEvent::NAME,
            new SaleEvent($sale, $movie, $numTickets)
        );

    $em->flush();

    $this->addFlash(
        'notice',
        'Thank you for your sale!'
    );

    return $this->redirectToRoute('index');
}

Dispatching events is easy. We "get" the event dispatcher (more on this later), and dispatch an event with a name and the data that is needed to create it.

To make it work, we need to do one more step. At this point we are dispatching our event, but how does Symfony know about our listener? We need to declare somehow that we want to use our listener.

We are approaching the concept that is in the core of Symfony.

For now, just open config/services.yaml and edit to add a new section just after the section that deals with controllers, like this:

1
2
3
4
5
6
7
8
9
10

App\Controller\:
    resource: '../src/Controller'
    tags: ['controller.service_arguments']

app.sale_listener:
    class: App\EventListener\SaleListener
    tags:
        - { name: kernel.event_listener, event: sale.created }
            

This works. We have registered our EventListener, and it is reacting to our Event (try your Sale form and check in your database that tickets are been created).

But at this point we may have some questions:

  • What is that file services.yaml?
  • How is that we are calling in our controller $this->get('event_dispatcher')? What kind of things can we get and where do they come from?
  • In our EventListener we have a constructor that expects to be called with the EntityManager, but we are not constructing EventListeners directly. How does Symfony know what to inject when it creates the object?

So many questions that we will answer in the next section.

Discuss

There is a particular type of events that is very interesting: Doctrine Events. Can you have a look at the documentation, and discuss when would they be useful?