Pub Sub Event Class & Subscribers in Symfony
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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeWe 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
:
// ... 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
:
// ... 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()
:
// ... 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:
// ... 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 theonFightStart()
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()
:
// ... 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!
// ... 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.
Why does the name of the subscriber explain what the subscriber does instead of what it is? Since it only subscribes, I would expect it to be named
FightStartingSubscriber
(without the Output action). Doing that, I would also find the following code more logical:Is there any definition how the naming should be done here? Or best practices?