Buy Access to Course
11.

Pub Sub Event Class & Subscribers in Symfony

|

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

We are able to run code right before a battle starts by registering what's called a "listener" to FightStartingEvent. As you can see, a listener can be any function... though what we see here is a bit less common. Usually a listener will be a method inside a class. And we'll refactor to that in a few minutes.

Passing Data to Listeners

But before we do, it might be useful to have a little bit more info in our listener function, like who is about to battle. That's the job of this event class. It can carry whatever data we want. For example, create a public function __construct() with two properties... which I'm going to make public for simplicity: $player and $ai:

13 lines | src/Event/FightStartingEvent.php
// ... lines 1 - 4
use App\Character\Character;
class FightStartingEvent
{
public function __construct(public Character $player, public Character $ai)
{
}
}

Cool! Over in GameApplication, we need to pass those in: $player and $ai:

143 lines | src/GameApplication.php
// ... lines 1 - 12
class GameApplication
{
// ... lines 15 - 24
public function play(Character $player, Character $ai): FightResult
{
$this->eventDispatcher->dispatch(new FightStartingEvent($player, $ai));
// ... lines 28 - 52
}
// ... lines 54 - 141
}

Back over in our listener, this function will be passed a FightStartingEvent object. In fact, it was always being passed... it just wasn't useful before. Now we can say Fight is starting against, followed by $event->ai->getNickname():

106 lines | src/Command/GameCommand.php
// ... lines 1 - 16
class GameCommand extends Command
{
// ... lines 19 - 26
protected function execute(InputInterface $input, OutputInterface $output): int
{
// ... line 29
$this->eventDispatcher->addListener(FightStartingEvent::class, function(FightStartingEvent $event) use ($io) {
$io->note('Fight is starting against ' . $event->ai->getNickname());
});
// ... lines 33 - 46
}
// ... lines 48 - 104
}

Super nice. Give it a try! I'll run the command again and... sweet! We see

! [NOTE] Fight is starting against AI: Mage

The only thing I missed is the space after "against" so it looks nicer. I'll fix that really quick:

106 lines | src/Command/GameCommand.php
// ... lines 1 - 16
class GameCommand extends Command
{
// ... lines 19 - 26
protected function execute(InputInterface $input, OutputInterface $output): int
{
// ... line 29
$this->eventDispatcher->addListener(FightStartingEvent::class, function(FightStartingEvent $event) use ($io) {
$io->note('Fight is starting against ' . $event->ai->getNickname());
});
// ... lines 33 - 46
}
// ... lines 48 - 104
}

Allowing Listeners to Control Behavior

As I mentioned, you can really put whatever data you want inside FightStartingEvent. Heck, you could create a public $shouldBattle = true property if you wanted. Then, in a listener, you could say $event->shouldBattle = false... maybe because the characters have used communication and honesty to solve their problems. Brave move!

Anyways, in GameApplication, you could then set this event to a new $event object, dispatch it, and if they shouldn't battle, just return. Or you could return new FightResult() or throw an exception. Either way, you see the point. Your listeners can, in a sense, communicate back to the central object to control its behavior.

I'll undo all of that inside of GameApplication, FightStartingEvent and also GameCommand.

Creating an Event Subscriber

As easy as this inline listener is, it's more common to create a separate class for your listener. You can either create a listener class, which is basically a class that has this code here as a public function, or you can create a class called a subscriber. Both are completely valid ways to use the pub/sub pattern. The only difference is how you register a listener versus a subscriber, which is pretty minor, and you'll see that in a minute. Let's refactor to a subscriber because they're easier to set up in Symfony.

In the Event/ directory, create a new PHP class called... how about... OutputFightStartingSubscriber, since this subscriber is going to output that a battle is beginning:

// ... lines 1 - 2
namespace App\Event;
// ... lines 4 - 9
class OutputFightStartingSubscriber implements EventSubscriberInterface
{
// ... lines 12 - 24
}

Event listeners don't need to extend any base class or implement any interface, but event subscribers do. They need to implement EventSubscriberInterface:

// ... lines 1 - 7
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class OutputFightStartingSubscriber implements EventSubscriberInterface
{
// ... lines 12 - 24
}

Go to "Code" -> "Generate" or Command+N on a Mac and select "Implement methods" to generate getSubscribedEvents():

// ... lines 1 - 9
class OutputFightStartingSubscriber implements EventSubscriberInterface
{
// ... lines 12 - 18
public static function getSubscribedEvents(): array
{
// ... lines 21 - 23
}
}

Nice! With an event subscriber, you'll list which events you subscribe to right inside this class. So we'll say FightStartingEvent::class => 'onFightStart':

// ... lines 1 - 9
class OutputFightStartingSubscriber implements EventSubscriberInterface
{
// ... lines 12 - 18
public static function getSubscribedEvents(): array
{
return [
FightStartingEvent::class => 'onFightStart',
];
}
}

This says:

When the FightStartingEvent happens, I want you to call the onFightStart() method right inside this class!

Create that: public function onFightStart()... which will receive a FightStartingEvent argument:

// ... lines 1 - 9
class OutputFightStartingSubscriber implements EventSubscriberInterface
{
public function onFightStart(FightStartingEvent $event)
{
// ... lines 14 - 16
}
// ... lines 18 - 24
}

For the guts of this, go over to GameCommand and steal the $io line:

// ... lines 1 - 9
class OutputFightStartingSubscriber implements EventSubscriberInterface
{
public function onFightStart(FightStartingEvent $event)
{
// ... lines 14 - 15
$io->note('Fight is starting against ' . $event->ai->getNickname());
}
// ... lines 18 - 24
}

By the way, the $io object is kind of hard to pass from console commands into other parts of your code... so I'm going to ignore that complexity here and just create a new one with $io = new SymfonyStyle(new ArrayInput([]), new ConsoleOutput():

// ... lines 1 - 4
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\ConsoleOutput;
use Symfony\Component\Console\Style\SymfonyStyle;
// ... lines 8 - 9
class OutputFightStartingSubscriber implements EventSubscriberInterface
{
public function onFightStart(FightStartingEvent $event)
{
$io = new SymfonyStyle(new ArrayInput([]), new ConsoleOutput());
$io->note('Fight is starting against ' . $event->ai->getNickname());
}
// ... lines 18 - 24
}

Now that we have a subscriber, back in GameCommand, let's hook that up! Instead of addListener(), say addSubscriber(), and inside of that, new OutputFightStartingSubscriber():

104 lines | src/Command/GameCommand.php
// ... lines 1 - 5
use App\Event\OutputFightStartingSubscriber;
// ... lines 7 - 16
class GameCommand extends Command
{
// ... lines 19 - 26
protected function execute(InputInterface $input, OutputInterface $output): int
{
// ... line 29
$this->eventDispatcher->addSubscriber(new OutputFightStartingSubscriber());
// ... lines 31 - 44
}
// ... lines 46 - 102
}

Easy! Testing time! I'll exit, choose my character and... wow! It's working so well, it's outputting twice. We're amazing!

But... seriously, why is it printing twice? This is, once again, thanks to auto-configuration! Whenever you create a class that implements EventSubscriberInterface, Symfony's container is already taking that and registering it on the EventDispatcher. In other words, Symfony, internally, is already calling this line right here. So, we can delete it!

104 lines | src/Command/GameCommand.php
// ... lines 1 - 29
$this->eventDispatcher->addSubscriber(new OutputFightStartingSubscriber());
// ... lines 31 - 104

I guess that answers the question of:

How do we use the pub/sub pattern in Symfony?

Just create a class, make it implement EventSubscriberInterface and... done! Symfony will automatically register it. To dispatch an event, create a new event class and dispatch that event anywhere in your code.

If we try this again (I'll exit the battle first)... it only outputs once. Great!

And... what are the benefits of pub/sub? They're really the same as the observer, though, in practice, pub/sub is a bit more common... probably because Symfony already has this great event dispatcher. Half of the work is already done for us!

Next, let's dive into our final pattern! It's one of my favorites and, I think, the most powerful in Symfony: The decorator pattern.