Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Event Constants & @Event Docs

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $8.00

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

Login Subscribe

There's one way we can make this better, and all high quality bundles do this: set the event name as a constant, instead of just having this random string. It's even a bit cooler than it sounds.

In the Event directory, create a new class: KnpULoremIpsumEvents. If your bundle dispatches events, you should typically have one class that has a constant for each event. It's a one-stop place to find all the event hook points.

... lines 1 - 4
final class KnpULoremIpsumEvents
{
... lines 7 - 14
}

Make this class final... which isn't too important... but in general, you should considering making any class in a shareable library final, unless you do want people to be able to sub-class it. Using final is always a safe bet and can be removed later.

Anyways, add const FILTER_API = '', go copy the event name and paste it here.

... lines 1 - 13
const FILTER_API = 'knpu_lorem_ipsum.filter_api';
... lines 15 - 16

Now, of course, replace that string in the controller with KnpULoremIpsumEvents::FILTER_API.

... lines 1 - 10
class IpsumApiController extends AbstractController
{
... lines 13 - 22
public function index()
{
... lines 25 - 30
if ($this->eventDispatcher) {
$this->eventDispatcher->dispatch(KnpULoremIpsumEvents::FILTER_API, $event);
}
... lines 34 - 35
}
}

So, this is nice! Though, the reason I really like this is that it gives us a proper place to document the purpose of this event: why you would listen to it and the types of things you can do.

The Special @Event Documentation

But the coolest part is this: add @Event(), and then inside double quotes, put the full class name of the event that listeners will receive. In other words, copy the namespace from the event class, paste it here and add \FilterApiResponseEvent.

... lines 1 - 4
final class KnpULoremIpsumEvents
{
/**
* Called directly before the Lorem Ipsum API data is returned.
*
* Listeners have the opportunity to change that data.
*
* @Event("KnpU\LoremIpsumBundle\Event\FilterApiResponseEvent")
*/
const FILTER_API = 'knpu_lorem_ipsum.filter_api';
}

What the heck does this do? On a technical level, absolutely nothing! This is purely documentation. But! Some systems - like PhpStorm - know to parse this and use it to help us when we're building event subscribers. We'll see exactly what I'm talking about in a minute. But, it's at least good documentation: if you listen to this event, this is the event object you should expect.

Creating an EventSubscriber

And... we're done! I'm not going to write a test for this, but I do at least want to make sure it works in my project. Move back over to the application code. Inside src/, create a new directory called EventSubscriber. Then, a new class called AddMessageToIpsumApiSubscriber.

... lines 1 - 8
class AddMessageToIpsumApiSubscriber implements EventSubscriberInterface
{
... lines 11 - 23
}

Like all subscribers, this needs to implement EventSubscriberInterface. Then I'll go to the Code -> Generate menu, or Command + N on a Mac, select Implement Methods, and add getSubscribedEvents.

... lines 1 - 10
public static function getSubscribedEvents()
{
... lines 13 - 15
}
... lines 17 - 25

Before we fill this in, I want to make sure that PhpStorm is fully synchronized with how our bundle looks - sometimes the symlink gets stale. Right click on the vendor/knpuniversity/lorem-ipsum-bundle directory, and click "Synchronize".

Cool: now it will definitely see the new event classes. When it's done indexing, return an array with KnpULoremIpsumEvents::FILTER_API set to, how about, onFilterApi.

... lines 1 - 10
public static function getSubscribedEvents()
{
return [
KnpULoremIpsumEvents::FILTER_API => 'onFilterApi',
];
}
... lines 17 - 25

Ready for the magic? Thanks to the Symfony plugin, we can hover over the method name, press Alt + Enter and select "Create Method". Woh! It added the onFilterApi method for me and type-hinted the first argument with FilterApiResponseEvent! But, how did it know that this was the right event class?

... lines 1 - 17
public function onFilterApi(FilterApiResponseEvent $event)
{
... lines 20 - 22
}
... lines 24 - 25

It knew that thanks to the @Event() documentation we added earlier.

Inside the method, let's say $data = $event->getData() and then add a new key called message set to, the very important, "Have a magical day". Finally, set that data back on the event with $event->setData($data).

... lines 1 - 17
public function onFilterApi(FilterApiResponseEvent $event)
{
$data = $event->getData();
$data['message'] = 'Have a magical day!';
$event->setData($data);
}
... lines 24 - 25

That is it! Thanks to Symfony's service auto-configuration, this is already a service and it will already be an event subscriber. In other words, go refresh the API endpoint. It, just, works! Our controller is now extensible, without the user needing to override it. Dispatching events is most commonly done in controllers, but you could dispatch them in any service.

Next, let's improve our word provider setup by making it a true plugin system with dependency injection tags and compiler passes. Woh.

Leave a comment!

17
Login or Register to join the conversation
Yaroslav Y. Avatar
Yaroslav Y. Avatar Yaroslav Y. | posted 4 years ago | edited

May I ask you why do you use a separate storage for event names? Why using Event::class, or at least, Event::getName() or Event::EVEN_NAME is not the preferred approach? I understand, it's seducing to keep the list of the Bundle Event Names in one, separate place, but, by doing this you

1) make the code less straight-forward, just compare this:

<blockquote>return [

return KnpULoremIpsumEvents::FILTER_API => 'onFilterApi',

];</blockquote>

and this

`return [

return FilterApiResponseEvent::class => 'onFilterApi',

];`

2) actually double the code by adding Event's class namespace and description to @Event annotation. Like, if you alter the Event's functionality, besides the Event doc, you will also have to update the @Event annotation - one change, but double places.

3 Reply

Hey Yaroslav Y.

That's a great observation, and the reason is that this was not done regionally in Symfony because the ClassName::class keyword didn’t exist back then, but these days, yes, we should start making the event name just the class name. Probably someday Symfony will change it.

Cheers!

Reply
triemli Avatar
triemli Avatar triemli | posted 2 years ago | edited

But how we assign AddMessageToIpsumApiSubscriber on event dispatcher?

I have my Subscriber class implemented with EventSubscriberInterface

Then create

` $event = new FilterApiResponseEvent($data);

    if($this->eventDispatcher) {
        $this->eventDispatcher->dispatch($event, FactorioEvents::FILTER_API);
    }`

I have <b>eventDispatcher</b>, I have $<b>event</b> but it ignore my subscriber. Method <b>getSubscribedEvents()</b> don't call.

Reply

Hello, I had the same issue and that was in my FilterApiResponseEvent I was extending Symfony\Contracts\EventDispatcher\Event instead of

Symfony\Component\EventDispatcher\Event;

I also clear my var/cache in my Bundle
I write it for future users

Reply

Hey triemli!

Excellent question! Normally (in your application) all you need to do is create a class that implements EventSubscriberInterface and... boom! Symfony magically sees it and registers it with the event dispatcher. That is thanks to "autoconfigure: true", which we don't normally use in bundles (since we prefer to configure everything explicitly.

So, if you're registering an event subscriber in your bundle, you'll need to give it a tag: kernel.event_subscriber - something like:


&lt;service id="whatever_your_service_id_is" class="Acme\Whatever\Your\EventSubscriber\ClassNameIs"&gt;
    &lt;!-- whatever arguments here --&gt;
    &lt;tag name="kernel.event_subscriber" /&gt;
&lt;/service&gt;

Cheers!

Reply
triemli Avatar

Hah after register in service.xml it works. But on you we didn't register, isn't?

Reply

Hey triemli

You're right. Ryan didn't configure the event subscriber inside the bundle and it works because the main application has autoconfiguration enabled.
Cheers!

Reply
triemli Avatar

Hm. THat's weird, because I had autoconfiguration is true too

Reply

Interesting... what Symfony version are you using? Maybe that "recently" changed

Reply
triemli Avatar

I just download Course code and made composer install.

Reply

Hmm, so you're on Symfony4.0

I'm not sure now but it's recommended to configure your bundle's services explicitly, assuming that the main application won't have autoconfigure enabled

Reply

Hello there,

Full disclosure I might be a little OCD and I HATE deprecation warnings. So... I tried to make the bundle as backward compatible as possible using Symfony 4.4.

So I extended my FilterApiResponseEvent with Event from Symfony\Contracts\EventDispatcher\Event. But now, my Spacebar is crashing since this class doesn't exists in 4.0. My question is, what would be the proper way to make it backward compatible but with no deprecation warnings when using 4.4?

Eg. for my config I used that nice little trick.

` $treeBuilder = new TreeBuilder('knpu_lorem_ipsum');

    $rootNode    = method_exists($treeBuilder, 'getRootNode')
        ? $treeBuilder->getRootNode()
        : $treeBuilder->root('knpu_lorem_ipsum');`

But I don't how to go about my current Event problem.

Thank you!

Julien

Reply

Hey julien_bonnier!

Full disclosure I might be a little OCD and I HATE deprecation warnings. So... I tried to make the bundle as backward compatible as possible using Symfony 4.4.

Ha! No problem. Actually, you should probably be a little OCD about deprecations in a bundle: if your bundles triggers deprecations, you will hear about it from your users ;).

About the events, there may be a more clever solution than this, but here is what comes to mind:


use Symfony\Contracts\EventDispatcher\Event as ContractEvent;
use Symfony\Component\EventDispatcher\Event as ComponentEvent;


if (class_exists(ContractEvent::class)) {
    class FilterApiResponseEvent extends ContractEvent
    {
        // the inside of the class
    }
} else {
    class FilterApiResponseEvent extends ComponentEvent
    {
        // the same inside of the class
    }
}

That should do it. You will have to duplicate the inside of the event, but hopefully that's not too much (there may also be a clever way around that that I'm not thinking about). Supporting multiple versions of Symfony like this is super hard - it's tricky for me, even though I have a fair amount of experience. And it often requires you to write weird code that would never otherwise be "ok" ;). This is also why it's important to have your test suite run against your normal dependencies but also against your "low" dependencies (e.g. https://github.com/SymfonyCasts/reset-password-bundle/blob/96391ea830a24bc2dcfc4437edd6648cc55ebcfd/.github/workflows/ci.yml#L184 ).

Let me know if it helps!

Cheers!

Reply

I see, I didn't think of it, and I don't like to duplicate code, but I understand that sometimes, when you want your code to support different versions of a framework you have to do some acrobatics. Any way thanks for the answer.

Cheers

Reply

Haha, indeed! When it comes to writing reusable code... and supporting different versions of Symfony or PHP... things can get INSANE. The rules are quite different :). And that's kind of the goal of many open source authors: they do a lot of the weird & hard work, so that application developers have an easy life. Welcome to the dark side ;)

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial is built using Symfony 4, but most of the concepts apply fine to Symfony 5!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-iconv": "*",
        "doctrine/annotations": "^1.8", // v1.8.0
        "knplabs/knp-markdown-bundle": "^1.7", // 1.7.0
        "knpuniversity/lorem-ipsum-bundle": "*@dev", // dev-master
        "nexylan/slack-bundle": "^2.0,<2.2", // v2.0.1
        "php-http/guzzle6-adapter": "^1.1", // v1.1.1
        "sensio/framework-extra-bundle": "^5.1", // v5.1.6
        "symfony/asset": "^4.0", // v4.0.6
        "symfony/console": "^4.0", // v4.0.6
        "symfony/flex": "^1.0", // v1.18.7
        "symfony/framework-bundle": "^4.0", // v4.0.6
        "symfony/lts": "^4@dev", // dev-master
        "symfony/twig-bundle": "^4.0", // v4.0.6
        "symfony/web-server-bundle": "^4.0", // v4.0.6
        "symfony/yaml": "^4.0", // v4.0.6
        "weaverryan_test/lorem-ipsum-bundle": "^1.0" // v1.0.0
    },
    "require-dev": {
        "easycorp/easy-log-handler": "^1.0.2", // v1.0.4
        "sensiolabs/security-checker": "^4.1", // v4.1.8
        "symfony/debug-bundle": "^3.3|^4.0", // v4.0.6
        "symfony/dotenv": "^4.0", // v4.0.6
        "symfony/maker-bundle": "^1.0", // v1.1.1
        "symfony/monolog-bundle": "^3.0", // v3.2.0
        "symfony/phpunit-bridge": "^3.3|^4.0", // v4.3.3
        "symfony/stopwatch": "^3.3|^4.0", // v4.0.6
        "symfony/var-dumper": "^3.3|^4.0", // v4.0.6
        "symfony/web-profiler-bundle": "^3.3|^4.0" // v4.0.6
    }
}
userVoice