Buy Access to Course
15.

Dispatching Custom Events

Share this awesome video!

|

Keep on Learning!

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

What if a user wants to change the behavior of our controller? Symfony does have a way to override controllers from a bundle... but not if that controller is registered as a service, like our controller. Well, ok, thanks to Symfony's incredible container, there is always a way to override a service. But let's not make our users do crazy things! If someone wants to tweak how our controller behaves, let's make it easy!

How? By dispatching a custom event. Ready for our new goal? I want to allow a user to change the data that we return from our API endpoint. Specifically, we're going to add a third key to the JSON array from our app.

Custom Event Class

The first step to dispatching an event is to create an event class. Create a new Event directory with a PHP class inside: call it FilterApiResponseEvent. I just made that up.

Make this extend a core Event class from Symfony. When you dispatch an event, you have the opportunity to pass an Event object to any listeners. To be as awesome as possible, you'll want to make sure that object contains as much useful information as you can.

Tip

Starting from Symfony 4.4, you should use the Event class from Symfony\Contracts\EventDispatcher: If you want to know more about this: https://github.com/symfony/event-dispatcher/blob/4.4/Event.php

// ... lines 1 - 6
class FilterApiResponseEvent extends Event
{
// ... lines 9 - 24
}

In this case, a listener might want to access the data that we're about to turn into JSON. Cool! Add public function __construct() with an array $data argument. I'll press Alt+Enter and choose "Initialize Fields" to create a data property and set it.

// ... lines 1 - 6
class FilterApiResponseEvent extends Event
{
private $data;
public function __construct(array $data)
{
$this->data = $data;
}
// ... lines 15 - 24
}

Then, we need a way for the listeners to access this. And, we also want any listeners to be able to set this. Go back to the Code -> Generate menu, or Command + N on a Mac, choose "Getter and Setters" and select data.

// ... lines 1 - 15
public function getData(): array
{
return $this->data;
}
public function setData(array $data)
{
$this->data = $data;
}
// ... lines 25 - 26

It's ready!

Dispatching the Event

Head to your controller: this is where we'll dispatch that event. First, set the data to a $data variable and then create the event object: $event = new FilterApiResponseEvent() passing it the data.

// ... lines 1 - 9
class IpsumApiController extends AbstractController
{
// ... lines 12 - 21
public function index()
{
// ... lines 24 - 28
$event = new FilterApiResponseEvent($data);
// ... lines 30 - 32
}
}

I'm not going to dispatch the event quite yet, but at the end, pass $event->getData() to the json method.

// ... lines 1 - 21
public function index()
{
// ... lines 24 - 31
return $this->json($event->getData());
}
// ... lines 34 - 35

To dispatch the event, we need... um... the event dispatcher! And of course, we're going to pass this in as an argument: EventDispatcherInterface $eventDispatcher. Press Alt+enter and select "Initialize Fields" to add that as a property and set it in the constructor.

// ... lines 1 - 13
private $eventDispatcher;
// ... line 15
public function __construct(KnpUIpsum $knpUIpsum, EventDispatcherInterface $eventDispatcher)
{
// ... line 18
$this->eventDispatcher = $eventDispatcher;
}
// ... lines 21 - 35

As soon as we do this, we need to also open services.xml and pass a second argument: type="service" and id="event_dispatcher".

// ... lines 1 - 6
<services>
// ... lines 8 - 13
<service id="knpu_lorem_ipsum.controller.ipsum_api_controller" class="KnpU\LoremIpsumBundle\Controller\IpsumApiController" public="true">
// ... line 15
<argument type="service" id="event_dispatcher" />
</service>
// ... lines 18 - 20
</services>
// ... lines 22 - 23

Back in the controller, right after you create the event, dispatch it: $this->eventDispatcher->dispatch(). The first argument is the event name and we can actually dream up whatever name we want. Let's use: knpu_lorem_ipsum.filter_api. For the second argument, pass the event.

// ... lines 1 - 9
class IpsumApiController extends AbstractController
{
// ... lines 12 - 21
public function index()
{
// ... lines 24 - 29
$this->eventDispatcher->dispatch('knpu_lorem_ipsum.filter_api', $event);
// ... lines 31 - 32
}
}

Tip

Starting in Symfony 4.4, you only need to pass the $event argument:

$this->eventDispatcher->dispatch($event);

Then, instead of knpu_lorem_ipsum.filter_api, the event name becomes the event class: in our case FilterApiResponseEvent::class.

And... yea, that's it! I mean, we haven't tested it yet, but this should work: our users have a new hook point.

Being Careful with Optional Dependencies

But actually there's a small surprise. Find your terminal and re-run all the tests:

./vendor/bin/simple-phpunit

They fail! Check this out: it says that our controller service has a dependency on a non-existent service event_dispatcher. But, the service id is event_dispatcher - that's not a typo! The problem is that the event_dispatcher service - like many services - comes from FrameworkBundle.

Open up the test that's failing: FunctionalTest. Inside, we're testing with a kernel that does not include FrameworkBundle! We did this on purpose: FrameworkBundle is an optional dependency.

Let me say it a different way: one of our services depends on another service that may or may not exist. Since we want our bundle to work without FrameworkBundle, we need to make the event_dispatcher service optional. To do that, add an on-invalid attribute set to null.

// ... lines 1 - 13
<service id="knpu_lorem_ipsum.controller.ipsum_api_controller" class="KnpU\LoremIpsumBundle\Controller\IpsumApiController" public="true">
// ... line 15
<argument type="service" id="event_dispatcher" on-invalid="null" />
</service>
// ... lines 18 - 23

Thanks to this, if the event_dispatcher service doesn't exist, instead of an error, it'll just pass null. That means, we need to make that argument optional, with = null, or by adding a ? before the type-hint.

// ... lines 1 - 9
class IpsumApiController extends AbstractController
{
// ... lines 12 - 15
public function __construct(KnpUIpsum $knpUIpsum, EventDispatcherInterface $eventDispatcher = null)
{
// ... lines 18 - 19
}
// ... lines 21 - 35
}

Inside the action, be sure to code defensively: if there is an event dispatcher, do our magic.

// ... lines 1 - 21
public function index()
{
// ... lines 24 - 29
if ($this->eventDispatcher) {
$this->eventDispatcher->dispatch('knpu_lorem_ipsum.filter_api', $event);
}
// ... lines 33 - 34
}
// ... lines 36 - 37

Try the tests again:

./vendor/bin/simple-phpunit

Aw yea! Next, let's make our event easier to use by documenting it with an event constants class. Then... let's make sure it works!