A real-world implementation of the Symfony events system

profile picture

Jan Engineer

2 Mar 2018  ·  7 min read


With some basic knowledge in hand, let’s dive into a practical use case to learn the ropes of Symfony events.

In our previous post, you learned more about how the Symfony event system works – more specifically how to set up dispatching and listening for events.

In this one, we’ll provide you with a real-world use case and an approach to a minimal implementation. You’re about to be introduced to one of our clients’ requirements and learn how to solve this with events.

The use case

One of our clients asked us to develop a support ticketing system for them. Fair question, but there was a catch: they needed a system that worked both ways. The user had to be able to create a ticket using a form on the web platform; and the client had to able to integrate it in their system to push a support ticket to us (for example, when they had a support call with an end-customer). This last particular case is the one we’re going to discuss in this post.

For this implementation, we had to take in account that both system should work independently. At the customer’s side, minimal changes should be introduced, while on our side, we should reuse as much business logic as possible.

By using queues and the event system in this situation, we were able to ensure that synchronisation was consistent between both systems. After all, the creation of a support ticket is a business-critical process, meaning that no data should ever be lost! By using events, the creation logic contains the same business logic on both implementations (on their backend and in the existing support ticket form on the portal system).

The initial setup

The In-Queue contains messages for database updates at our side. This means that every change the client makes in their backend system is pushed towards us.

The Messages

The most important part of this integration are the messages. These are the core of the synchronisation between the datasources.

1) The incoming message life-cycle

The support ticket gets created in the client’s backend system, which triggers a command that puts the newly created ticket (converted to a message) on the queue. We listen for new incoming message on that queue. When there is one, our queue listener picks it up and dispatches it to the correct listener / subscriber that hold the business logic.

2) The incoming message structure

{
    "event": {
        "event_name": "november_five.queue.ticket.created",
        "event_timestamp": "2017-07-02T12:00:00+00:00",
        "payload": {
            "id": 123,
            "user": 12,
            "subject": "Hello world!",
            "body": "Hello there, this is a test.",
            "status": "open"
        }
    }
}

Here you can see that the message contains a event_name and a payload element, these two fields in the payload are our key elements to work with in a further stadium.

Now let’s start writing code …

Prerequisites

When you want to build a requirement like this you should spend some time on finding out if there are bundles that can take some of the logic out of your hands. (You don’t have to reinvent the wheel.)

For this example we’ll use the AWS SDK for PHP and the JMSSerializerBundle.

Please note that this isn’t the full implementation we provided to the client. The full implementation contains a lot more data validation. If you want to use validation, we would recommend that you use the build-in Symfony validation tools; to validate the json we recommend json-schema.

A limited developer’s approach

In this example we’ll receive a incoming ‘november_five.queue.ticket.created’ event. By exploring the following code you’ll learn how to fetch a queue message from the broker, create a event, dispatch that event and store the provided data into the database.

For the outgoing messages, we recommend using a similar approach. Only this time, you’ll dispatch your events when you are storing your entity in the database.

1) The queue event

<?php

namespace NovemberFive\QueueBundle\Event;

...

class QueueEvent extends Event
{
    /** @var string **/
    protected $eventName;
    /** @var array **/
    protected $payload = array();

    /** ... */
    public function setEventName($eventName)
    {
        $this->eventName = $eventName;
    }

    /** ... */
    public function getEventName()
    {
        return $this->eventName;
    }

    /** ... */
    public function setPayload(array $payload = [])
    {
        $this->payload = $payload;
    }

    /** ... */
    public function getPayload()
    {
        return $this->payload;
    }

}

2) The basic queue listener

<?php

namespace NovemberFive\QueueBundle\Command;

...

class QueueListenerCommand extends ContainerAwareCommand
{
    /** ... */
    protected function configure()
    {
        $this
            ->setName('november-five:queue:listener')
            ->setDescription('Run the queue listener');
    }

    /** ... */
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $container = $this->getContainer();
        $sqsManager = ...
        $eventDispatcher = $container->get('event_dispatcher');
        $queueUrl = "https://....";
        $client = $sqsManager->getClient();

        while (true) { // Keep listening

            $result = $client->receiveMessage(array(
                'QueueUrl' => $queueUrl,
            ));

            if ($result['Messages'] === null) { // if the queue does not contain a message
                continue;
            }

            // Getting the message
            $messages = $result['Messages'];
            $message = reset($messages);
            $message = json_decode($message, true);

            // ... Payload validation

            // Creating the event
            $event = new QueueEvent();
            $event->setEventName($message['event']['event_name']);
            $event->setPayload($message['event']['payload']);

            // Dispatching the event
            $eventDispatcher->dispatch($eventName, $event);

            // cleaning up the message
            $client->deleteMessage(array(
                'QueueUrl' => $queueUrl,
                'ReceiptHandle' => $originalMessage['ReceiptHandle'],
            ));
        }
    }
}

3) The Support ticket entity

We need to set up this entity first, because it’s used to serialise the incoming message.

<?php

namespace NovemberFive\SupportBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as Serializer;
use Symfony\Component\Validator\Constraints as Assert;

/**
 * Class SupportTicket.
 *
 * @ORM\Table("nove_support_ticket")
 * @ORM\Entity()
 **/
class SupportTicket
{
   /**
    * @var int
    *
    * @ORM\Id
    * @ORM\Column(type="integer")
    * @ORM\GeneratedValue(strategy="AUTO")
    *
    * @Serializer\SerializedName("id")
    * @Serializer\Type("integer")
    * @Serializer\Expose()
    * @Serializer\Groups({"ticket-list", "ticket-detail"})
    *
    * @Assert\NotNull()
    **/
    protected $id;

   /**
    * @var int
    *
    * @ORM\Column(type="integer")
    *
    * @Serializer\SerializedName("user_id")
    * @Serializer\Type("integer")
    *
    * @Assert\NotNull()
    **/
    protected $user;

   /**
    * @var string
    *
    * @ORM\Column(type="string", length=255)
    *
    * @Serializer\SerializedName("subject")
    * @Serializer\Type("string")
    * @Serializer\Expose()
    * @Serializer\Groups({"ticket-list", "ticket-detail"})
    *
    * @Assert\NotNull()
    **/
    protected $subject;

   /**
    * @var string
    *
    * @ORM\Column(type="text")
    *
    * @Serializer\SerializedName("body")
    * @Serializer\Type("string")
    * @Serializer\Expose()
    * @Serializer\Groups({"ticket-detail"})
    *
    * @Assert\NotNull()
    **/
    protected $body;

    /** ... */
    public function getId()
    {
        return $this->id;
    }

    /** ... */
    public function setId($id)
    {
        $this->id = $id;
    }

    /** ... */
    public function getUser()
    {
        return $this->user;
    }

    /** ... */
    public function setUser($user)
    {
        $this->user = $user;
    }

    /** ... */
    public function getSubject()
    {
        return $this->subject;
    }

    /** ... */
    public function setSubject($subject)
    {
        $this->subject = $subject;
    }

    /** ... */
    public function getBody()
    {
        return $this->body;
    }

    /** ... */
    public function setBody($body)
    {
        $this->body = $body;
    }
}

4) The EventSubscriber

First, we look at services.yml:

november_five.support.event_subscriber.support_ticket_created:
    class: 'NovemberFive\SupportBundle\EventSubscribe\SupportTicketCreatedListener'
    arguments:
        - "@november_five_support_ticket.storage.support_ticket"
        - "@jms_serializer"
    tags:
        - { name: kernel.event_subscriber }

Then, we move onto the SupportTicketCreatedListener:

<?php

namespace NovemberFive\SupportBundle\EventSubscriber;

use JMS\Serializer\SerializerInterface;
use NovemberFive\QueueBundle\Event\QueueEvent;
use NovemberFive\SupportBundle\Entity\SupportTicket;
use NovemberFive\SupportBundle\Storage\SupportTicketStorage;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

/**
 * Class SupportTicketCreateListener.
 **/
class SupportTicketCreateListener implements EventSubscriberInterface
{
    /** @var SupportTicketStorage */
    private $storage;
    /** @var SerializerInterface */
    private $serializer;

    /** ... */
    public function __construct(SupportTicketStorage $storage, SerializerInterface $serializer)
    {
        $this->storage = $storage;
        $this->validator = $validator;
        $this->serializer = $serializer;
    }

    /** ... */
    public static function getSubscribedEvents()
    {
        return [
        'november_five.queue.ticket.created' => 'onTicketCreated'
        ];
    }

    public function onTicketCreated(QueueEvent $event)
    {
        // First we'll try to serialize the payload to the entity
        $entity = $this->serializer->deserialize($event->getPayload(), SupportTicket::class, 'json');

        // Store the data to the database
        $this->storage->store($entity);
    }
  }

And for a useful bonus: SupportTicketStorage.

<?php

namespace NovemberFive\SupportBundle\Storage;

use Doctrine\ORM\EntityManagerInterface;
use NovemberFive\SupportBundle\Entity\SupportTicket;
use Symfony\Component\Validator\Validator\ValidatorInterface;

class SupportTicketStorage
{
    /** @var EntityManagerInterface */
    private $entityManager;
    /** @var ValidatorInterface */
    private $validator;

    /** ... */
    public function __construct(EntityManagerInterface $entityManager, ValidatorInterface $validator)
    {

        $this->entityManager = $entityManager;
        $this->validator = $validator;
    }

    public function store(SupportTicket $supportTicket)
    {
        $validation = $this->validator->validate($supportTicket);

        if ($validation->count() > 0) {
            // Throw validation error!
        }

        // Other checks ...

        $this->entityManager->persist($supportTicket);
        $this->entityManager->flush();
    }

}

5) Some minor improvements

Overall, this is a working minimal implementation. You can improve this by:

  • Adding a reference to the freshly created entity on the QueueEvent (this way you can access the freshly created entity from every following dispatched event and manipulate the date from there.)
  • Implementing the json-schema validator on both the incoming message and the body payload.
  • Implementing logging and a notification system on the entire flow.

Conclusion

After reading this post, you should have more insight into how powerful a tool the event ecosystem really is.

When you are implementing a solution like this, the main improvement is that you can extend the event system easily by hooking in more listeners at a later time. And most importantly, you can reuse the business logic to create the ticket (both from the event, and when a user creates a ticket from a webform on the platform).

Side note: when you are implementing this in a real-world example, the key concepts are validation and error handling/reporting. These are important because you want the correct data to be synced at both sides – and of course, you want to know if somethings goes wrong with either the message structure or the data payload!

Are you dispatching events and reusing code yet?