Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Observer Inside Symfony + Benefits

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 $10.00

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

Login Subscribe

We've implemented the Observer Pattern! The GameApplication is our subject, which notifies all of the observers... and we have one at the moment: XpEarnedObserver. Inside GameCommand, we connected all of this by manually instantiating the observer and XpCalculator... then calling $this->game->subscribe():

... lines 1 - 16
class GameCommand extends Command
{
... lines 19 - 25
protected function execute(InputInterface $input, OutputInterface $output): int
{
$xpObserver = new XpEarnedObserver(
new XpCalculator()
);
$this->game->subscribe($xpObserver);
... lines 32 - 47
}
... lines 49 - 105
}

But... that isn't very Symfony-like.

Both XpEarnedObserver and XpCalculator are services. So we would normally autowire them from the container, not instantiate them manually. We are autowiring GameApplication... but our overall situation isn't quite right. In a perfect world, by the time Symfony gives us this GameApplication, Symfony's container would have already hooked up all of its observers so that it's ready to use immediately. How can we do that? Let's do it the simple way first.

Manually Specifying the Services

Remove all of the manual code inside of GameCommand:

... lines 1 - 16
class GameCommand extends Command
{
... lines 19 - 25
protected function execute(InputInterface $input, OutputInterface $output): int
{
$xpObserver = new XpEarnedObserver(
new XpCalculator()
);
$this->game->subscribe($xpObserver);
... lines 32 - 47
}
... lines 49 - 105
}

We're going to recreate this same setup... but inside services.yaml. Open that... and at the bottom, we need to modify the service App\GameApplication. But we don't need to configure any arguments. In this case, we need to configure some calls. Here, I'm basically telling Symfony:

Yo! After you instantiate GameApplication, call the subscribe() method on it and pass, as an argument, the @App\Observer\XpEarnedObserver service.

... lines 1 - 7
services:
... lines 9 - 25
App\GameApplication:
calls:
- subscribe: ['@App\Observer\XpEarnedObserver']

So when we autowire GameApplication, Symfony will go grab the XpEarnedObserver service and that service will, of course, get XpCalculator autowired into it. This is pretty normal autowiring: the only special part is that Symfony will now call the subscribe() method on GameApplication before it passes that object to GameCommand.

In other words, this should work. Let's give it a try! Run:

./bin/console app:game:play

There are no errors so far and... oh. We lost. Bad luck. Let's try again! We won and we received 30 XP. It's working!

Setting up Autoconfiguration

The downside to this solution is that every time we add a new observer, we'll need to go to services.yaml and wire it manually. Gasp, how undignified...

Could we automatically subscribe all services that implement GameObserverInterface? Why, yes! And what an excellent idea! We can do that in two steps.

First, open src/Kernel.php. This isn't a file we work with much, but we're about to do some deeper things with the container and so this is exactly where we want to be. Go to Code Generate or Command+O and select "Override Methods". We're going to override one called build():

20 lines src/Kernel.php
... lines 1 - 6
use Symfony\Component\DependencyInjection\ContainerBuilder;
... lines 8 - 9
class Kernel extends BaseKernel
{
... lines 12 - 13
protected function build(ContainerBuilder $container)
{
... lines 16 - 17
}
}

Perfect! The parent method is empty, so we don't need to call it at all. Instead, say $container->registerForAutoconfiguration(), pass it GameObserverInterface::class, and then say ->addTag(). I'm going to invent a new tag here called game.observer:

20 lines src/Kernel.php
... lines 1 - 9
class Kernel extends BaseKernel
{
... lines 12 - 13
protected function build(ContainerBuilder $container)
{
$container->registerForAutoconfiguration(GameObserverInterface::class)
->addTag('game.observer');
}
}

This probably isn't something you see very often (or ever) in your code, but it's really common in third-party bundles. This says that any service that implements GameObserverInterface should automatically be given this game.observer tag... assuming that service has autoconfigure enabled, which all of our services do.

That tag name could be any string... and it doesn't do anything at the moment: it's just a random string that's now attached to our service.

But we should, at least, be able to see it. Spin over and run:

./bin/console debug:container xpearnedobserver

It found our service! And check it out: Tags - game.observer.

Ok, now that our service has a tag, we're going to write a little more code that automatically calls the subscribe method on GameApplication for every service with that tag. This is also going to go in Kernel, but in a different method. In this case, we're going to implement something called a "compiler pass".

Add a new interface called CompilerPassInterface. Then, below, go back to "Code Generate", "Implement Methods", and select process():

31 lines src/Kernel.php
... lines 1 - 6
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
... lines 8 - 11
class Kernel extends BaseKernel implements CompilerPassInterface
{
... lines 14 - 21
public function process(ContainerBuilder $container)
{
... lines 24 - 28
}
}

Compiler passes are a bit more advanced, but super cool! It's a piece of code that runs at the very end of the container and services being built... and you can do whatever you want inside.

Check it out! Say $definition = $container->findDefinition(GameApplication::class):

31 lines src/Kernel.php
... lines 1 - 4
use App\Observer\GameObserverInterface;
... lines 6 - 11
class Kernel extends BaseKernel implements CompilerPassInterface
{
... lines 14 - 21
public function process(ContainerBuilder $container)
{
$definition = $container->findDefinition(GameApplication::class);
... lines 25 - 28
}
}

No, this does not return the GameApplication object. It returns a Definition object that knows everything about how to instantiate a GameApplication, like its class, constructor arguments, and any calls it might have on it.

Next, say $taggedObservers = $container->findTaggedServiceIds('game.observer'):

31 lines src/Kernel.php
... lines 1 - 11
class Kernel extends BaseKernel implements CompilerPassInterface
{
... lines 14 - 21
public function process(ContainerBuilder $container)
{
$definition = $container->findDefinition(GameApplication::class);
$taggedObservers = $container->findTaggedServiceIds('game.observer');
... lines 26 - 28
}
}

This will return an array of all the services that have the game.observer tag. Then we can loop over them with foreach ($taggedObservers as $id => $tags). The $id is the service id... and $tags is an array because you can technically put the same tag on a service multiple times... but that's not something we care about:

31 lines src/Kernel.php
... lines 1 - 11
class Kernel extends BaseKernel implements CompilerPassInterface
{
... lines 14 - 21
public function process(ContainerBuilder $container)
{
$definition = $container->findDefinition(GameApplication::class);
$taggedObservers = $container->findTaggedServiceIds('game.observer');
foreach ($taggedObservers as $id => $tags) {
... line 27
}
}
}

Now say $definition->addMethodCall(), which is the PHP version of calls in YAML. Pass this the subscribe method and, for the arguments, a new Reference() (the one from DependencyInjection), with id:

31 lines src/Kernel.php
... lines 1 - 8
use Symfony\Component\DependencyInjection\Reference;
... lines 10 - 11
class Kernel extends BaseKernel implements CompilerPassInterface
{
... lines 14 - 21
public function process(ContainerBuilder $container)
{
$definition = $container->findDefinition(GameApplication::class);
$taggedObservers = $container->findTaggedServiceIds('game.observer');
foreach ($taggedObservers as $id => $tags) {
$definition->addMethodCall('subscribe', [new Reference($id)]);
}
}
}

This is a fancy way of saying that we want the subscribe() method to be called on GameApplication... and for it to pass the service that holds the game.observer tag.

The end result is the same as what we had before in services.yaml... just more dynamic and better for impressing your programmer friends. So, remove all of the YAML code we added:

... lines 1 - 7
services:
... lines 9 - 25
App\GameApplication:
calls:
- subscribe: ['@App\Observer\XpEarnedObserver']

If we try our game again...

./bin/console app:game:play

No errors! And... yes! It still works! If we need to add another observer later, we can just create a class, make it implement GameObserverInterface and... done! It will automatically be subscribed to GameApplication.

Observer Pattern in the Wild

So that is the observer pattern. How it looks can differ, with different method names for subscribing. Heck, sometimes the observers are passed in through the constructor! But the idea is always the same: a central object loops over and calls a method on a collection of other objects when something happens.

Where do we see this in the wild? It shows up in a lot of places, but here's one example. Over on Symfony's GitHub page, I'm going to hit "T" and search for a class called LocaleSwitcher. If you need to do something in your application each time the locale switches, you can register your code with the LocaleSwitcher and it will call you. In this case, the observers are passed through the constructor. And then you can see down here, after the locale is set, it loops over all of those and calls setLocale(). So LocaleSwitcher is the subject, and these are the observers.

How do you register an observer? Not surprisingly, it's by creating a class that implements LocaleAwareInterface. Thanks to autoconfiguration, Symfony will automatically tag your service with kernel.locale_aware. Yup, it uses the same mechanism for hooking all of this up that we just used!

Benefits of the Observer Pattern

The benefits of the observer pattern are actually best described by looking at the SOLID principles. This pattern helps the Single Responsibility pattern because you can encapsulate (or isolate) code into smaller classes. Instead of putting everything into GameApplication, like all of our XP logic right here, we were able to isolate things in XpEarnedObserver and keep both classes more focused. This pattern also helps with the Open-closed Principle, because we can now extend the behavior of GameApplication without modifying its code.

The observer pattern also follows the Dependency Inversion Principle or DIP, which is one of the trickier principles if you ask me. Anyways, DIP is happy because the high-level class - GameApplication - accepts an interface - GameObserverInterface - and that interface was designed for the purpose of how GameApplication will use it. From GameApplication's perspective, this interface represents something that wants to "observe" what happens when something occurs within the game. Namely, the fight finishing. And so, GameObserverInterface is a good name.

But, if we had named it based on how the observers will use the interface, that would have made DIP sad. For example, had we called it XpChangerInterface and the method timeToChangeTheXp, that would be a violation of the Dependency Inversion Principle. If that's fuzzy and you want to know more, check out our SOLID tutorial.

Next, let's quickly turn to the brother pattern of observer: Pub/sub.

Leave a comment!

0
Login or Register to join the conversation
Cat in space

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

userVoice