Configuring CoR with 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 SubscribeThis 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.
// ... lines 1 - 8 | |
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure; | |
// ... line 10 | |
( | |
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!
// ... lines 1 - 7 | |
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure; | |
// ... line 9 | |
( | |
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.
// ... 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.
// ... 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.
// ... 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!
// ... 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 | |
} |
// ... 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.
The Null Object Pattern is great.
I discovered it with NullLogger and it help removing lots of if($this->logger) ....🙏
Use it everywhere now!
Thank you for this series of videos, they are very interesting 👌.