Keep Constant, and Dispatch Events from Services


Towards the end of the previous video we had created our first custom Event, and used the Symfony event dispatcher service to send this event off to our configured Symfony event listener.

Before we go any further, let's cover one thing that you will likely want to do when using this setup in a real project:

Constants.

Here's what we had before:

$eventDispatcher->dispatch(
    'some.event.name',
    new FunEvent()
);

The problem area is some.event.name.

Let's imagine that we have another controller action, or an entirely separate Symfony service that also dispatches some FunEvents.

What we don't want to do is sprinkle the string some.event.name throughout our code base. It's error prone, and a pain to change.

It's better to use a Constant.

For the purposes of this series I am going to define a new class: Events, which I will then reference throughout the code as needed. You can change this setup to suit your needs.

One thing I have tried, but have come to not like is the way the Symfony docs suggest, which is to use a constant per class, e.g:

namespace Acme\Store\Event;

use Symfony\Component\EventDispatcher\Event;
use Acme\Store\Order;

/**
 * The order.placed event is dispatched each time an order is created
 * in the system.
 */
class OrderPlacedEvent extends Event
{
    const NAME = 'order.placed';

This is an example taken from the official docs.

Your mileage may vary of course, but for me I prefer to centralise and de-couple (for want of a better word) my event names from their implementations. It's a tiny point of contention, but as ever, do whatever feels right for you.

Here's my new class:

<?php

// src/AppBundle/Event/Events.php

namespace AppBundle\Event;

class Events
{
    # PHP 7.1+
    # https://codereviewvideos.com/course/php-7-1/video/class-constant-visibility
    public const SOME_EVENT_NAME = 'some.event.name';

    # PHP 7 or earlier
    const SOME_EVENT_NAME = 'some.event.name';
}

And now, we can replace the hardcoded strings with a shiny constant:

use AppBundle\Event\Events;

// ...

$eventDispatcher->dispatch(
    Events::SOME_EVENT_NAME,
    new FunEvent()
);

It seems like a minor thing, but on a real project this practice is a good one, in my experience.

Now, another thing is that typically the event name, and the event class name tend to match up. They don't here. And as we saw in the previous video that's not the end of the world, we can customise the method name and so on. In reality, it's easier to just keep things the same, so lets:

<?php

// src/AppBundle/Event/Events.php

namespace AppBundle\Event;

class Events
{
    public const FUN_EVENT = 'fun.event';
}

And don't forget to update the dispatch call:

use AppBundle\Event\Events;

// ...

$eventDispatcher->dispatch(
    Events::FUN_EVENT,
    new FunEvent()
);

Ok, that's the housekeeping out of the way.

Dispatching Events From Services

Before we go further into event listeners and subscribers, let's cover one area I often find developers who are new to Symfony can get confused with.

Our examples so far have shown how we can get access to the Event Dispatcher from a Symfony controller.

As we have seen, in Symfony 3.3 onwards we can inject the required service directly into the controller action in which the service is needed.

Prior to Symfony 3.3, in Symfony controller actions we could get services from the container.

Where many developers came unstuck is in how to access services in other services.

Calling $this->get('some_service_id_here') would result in errors - as neither get, not the container are available by default. Likewise, the answer is not to simply inject the full container into every service :)

Instead we need to inject specific services into other services.

Don't over think this.

Here is a video that explains this process further.

Here's an example of some config which allows this:

# app/config/services.yml

services:

    # possible Symfony 3.3 approach
    _defaults:
        autowire: true
        autoconfigure: true
        public: false

    AppBundle\:
        resource: '../../src/AppBundle/*'
        exclude: '../../src/AppBundle/{Document,Entity,Features,Repository}'

    # Prior to Symfony 3.3
    crv.order:
        class: AppBundle\Service\OrderService
        arguments:
            - "@logger"
            - "@event_dispatcher"
            - "@crv.order_factory"

    crv.order_factory:
        class: AppBundle\Factory\OrderFactory

In Symfony 3.3, with autowiring / autoconfig, the way this process works is masked from you, the developer. This has both upsides, and downsides.

On the upside you don't need to worry about manually configuring your services, for the most part.

On the downside, if you don't understand how your services are configured then when things go wrong, fixing these problems will be difficult / confusing.

As you can see from the Symfony 3.2 and earlier approach, the dependency setup is explicit / immediately obvious:

crv.order_factory is a standalone service. It takes no arguments / has no injected dependencies.

crv.order is itself a standalone service. It takes three arguments. Two of which are pre-configured services that come with the Symfony framework. The third argument is our own crv.order_factory service.

In answer the question: How do I dispatch an event from my own, custom service? We simply inject the pre-configured / already available Event Dispatcher service.

In this case our OrderService may look like this:

<?php

// src/AppBundle/Service/OrderService.php

namespace AppBundle\Service;

use AppBundle\Event\Events;
use AppBundle\Event\FunEvent;
use AppBundle\Factory\OrderFactory;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

class OrderService
{
    /**
     * @var LoggerInterface
     */
    private $logger;

    /**
     * @var EventDispatcherInterface
     */
    private $eventDispatcher;

    public function __construct(
        LoggerInterface $logger,
        EventDispatcherInterface $eventDispatcher,
        OrderFactory $factory
    )
    {
        $this->logger = $logger;
        $this->eventDispatcher = $eventDispatcher;
        // etc
    }

    public function processOrder($data)
    {
        // ... some process

        $this->eventDispatcher->dispatch(
            Events::SOME_EVENT_NAME,
            new FunEvent()
        );
    }
}

In my experience this process is only obvious if you have seen it in action. I have seen many developers come unstuck trying to figure this out for themselves.

The good news is, now you know :)

Episodes

# Title Duration
1 An Introduction to Symfony Events 06:54
2 Keep Constant, and Dispatch Events from Services 03:32
3 Symfony Event Subscriber Tutorial 07:28
4 Symfony Event Subscriber in a JSON API Example 03:20
5 Symfony Events - The Gotchas 08:07