Login to bookmark this video
Buy Access to Course
09.

Configuring CoR with 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

This is a Symfony app, so let's take advantage of that and use the autoconfigure feature to set up our chain. There's even a super useful Autoconfigure attribute we can use in our handler classes.

We'll start by opening CasinoHandler and, above the class name, add the #[Autoconfigure()] attribute. When this class is instantiated, we want to call setNext() and pass another handler object. To do that, we'll use the calls option, so inside write calls: [['setNext' => ['@'.LevelHandler::class]]]. Be careful with this nested array syntax.

47 lines | src/ChainHandler/CasinoHandler.php
// ... lines 1 - 8
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
// ... line 10
#[Autoconfigure(
calls: [['setNext' => ['@'.LevelHandler::class]]]
)]
class CasinoHandler implements XpBonusHandlerInterface
// ... lines 15 - 47

Now, when Symfony instantiates this class, it will call setNext() and pass a LevelHandler object.

By the way, if you're wondering what this @ symbol prefix is all about, good eye! This tells Symfony to pass the LevelHandler service. If we didn't add this, it would pass the LevelHandler class string which is definitely not what we want.

We'll do the same thing in the LevelHandler class. Open that up, write #[Autoconfigure()], and then calls: [['setNext' => ['@'.OnFireHandler::class]]]. We can leave the OnFireHandler as it is because it's the last handler in the chain. Perfect!

37 lines | src/ChainHandler/LevelHandler.php
// ... lines 1 - 7
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
// ... line 9
#[Autoconfigure(
calls: [['setNext' => ['@'.OnFireHandler::class]]]
)]
class LevelHandler implements XpBonusHandlerInterface
// ... lines 14 - 37

Now that the chain is set up, we can remove the code that manually initializes it in GameApplication. Open that... and delete everything inside its constructor except this line. The final step is to configure the $xpBonusHandler property. We can add it to the constructor by writing private readonly XpBonusHandlerInterface $xpBonusHandler. Above that, use the #[Autowire] attribute, and inside, write service: CasinoHandler::class because it is the first handler in our chain.

216 lines | src/GameApplication.php
// ... lines 1 - 13
use Symfony\Component\DependencyInjection\Attribute\Autowire;
// ... line 15
class GameApplication
{
// ... lines 18 - 24
public function __construct(
#[Autowire(service: CasinoHandler::class)]
private readonly XpBonusHandlerInterface $xpBonusHandler,
// ... line 28
) {
$this->difficultyContext = new GameDifficultyContext();
}
// ... lines 32 - 214
}

We need the #[Autowire] attribute because Symfony won't know how to inject XpBonusHandlerInterface as there are multiple classes that implement it.

All right! Let's give this a try! Spin over to your terminal and run:

php bin/console app:game:play

Okay, the game is running. That's a good sign! And if we battle... we lost! On the bright side, we can see our info message telling us that the CasinoHandler kicked in, and we didn't get any extra XP, so everything is working nicely.

Bonus: Null Object Pattern

Ready for a bonus topic? It's time to talk about the Null Object pattern. What is the Null Object pattern? In a nutshell, it's a smart way to avoid null checks. Instead of checking to see if a property is null, as we've done in the past, we'll create a "null object" that implements the same interface and does nothing in their methods. What does this mean? Put simply, if a method returns a value, it will return as close to null as possible. For example, if it returns an array, you'd return an empty array. A string? Return an empty string. An int? Return 0. It can get even more complicated that this, but you get the idea.

So let's get to it! Back in our code, find that if we've been talking about, which is inside any handler. What we need to do is remove the if and call the next handler directly. Easy peasy. Create a new handler class and call it NullHandler. Make it implement the XpBonusHandlerInterface and hold "Option" + "Enter" to implement the methods. And now... Let's have the handler do nothing! Well, as close to nothing as possible. The setNext() method returns nothing, so we can leave it empty, but the handle() method returns an int. When you find a method that returns something, you should always ask yourself how the value is being used. The answer will help you identify the closest-to-null value to return. In our case, we can return 0 and that will be fine because it represents the extra XP the player will earn. If this value were used for multiplication however, like a value that multiplies the XP earned for each match, returning 0 would cause problems.

21 lines | src/ChainHandler/NullHandler.php
// ... lines 1 - 7
class NullHandler implements XpBonusHandlerInterface
{
public function handle(Character $player, FightResult $fightResult): int
{
return 0;
}
public function setNext(XpBonusHandlerInterface $next): void
{
// Doing nothing
}
}

Okay, let's keep going! Open up CasinoHandler and add a constructor where we'll initialize $this->next to a new NullHandler() object. Copy this constructor because we'll need it for the other handlers. Inside the handle() method, find that pesky if and remove it. We'll also remove this return 0 at the bottom. Now we'll always return the output of the next handler. That's the beauty of the Null Object pattern.

48 lines | src/ChainHandler/CasinoHandler.php
// ... lines 1 - 13
class CasinoHandler implements XpBonusHandlerInterface
{
// ... lines 16 - 17
public function __construct()
{
$this->next = new NullHandler();
}
// ... line 22
public function handle(Character $player, FightResult $fightResult): int
{
// ... lines 25 - 39
return $this->next->handle($player, $fightResult);
}
// ... lines 42 - 46
}

We'll do the same thing to the other handlers, and... perfect! Let's give this a try!

38 lines | src/ChainHandler/LevelHandler.php
// ... lines 1 - 12
class LevelHandler implements XpBonusHandlerInterface
{
// ... lines 15 - 16
public function __construct()
{
$this->next = new NullHandler();
}
// ... line 21
public function handle(Character $player, FightResult $fightResult): int
{
// ... lines 24 - 29
return $this->next->handle($player, $fightResult);
}
// ... lines 32 - 36
}

34 lines | src/ChainHandler/OnFireHandler.php
// ... lines 1 - 8
class OnFireHandler implements XpBonusHandlerInterface
{
// ... lines 11 - 12
public function __construct()
{
$this->next = new NullHandler();
}
public function handle(Character $player, FightResult $fightResult): int
{
// ... lines 20 - 25
return $this->next->handle($player, $fightResult);
}
// ... lines 28 - 32
}

Spin over to your terminal and run:

php bin/console app:game:play

Hey hey! We won! And we earned extra XP thanks to the LevelHandler! Everything's working and the code looks great, but I know a little trick that could make this even better. We could make the $next handler property a constructor argument and inject NullHandler into the last one in the chain using the Autowire attribute we've seen before, like this:

class OnFireHandler implements XpBonusHanderInterface
{
    public function __construct(
        #[Autowire(service: NullHandler::class)]
        private readonly XpBonusHanderInterface $next
    ) {
    }
}

This would even allow us to remove the setNext() method from the interface, which is pretty handy. I don't want to deviate that much from the original pattern's design, so I'll undo that, but it's good to keep in mind.

Next: We'll meet the Chain of Responsibility's cousin - the Middleware pattern.