This course is still being released! Check back later for more chapters.
Triggering Chain of Responsibility
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 SubscribeOpen up GameApplication
. We're going to set up the chain inside its constructor, and we'll start by instantiating all of the handlers. Write $casinoHandler = new CasinoHandler()
, $levelHandler = new LevelHandler()
, and finally $onFireHandler = new OnFireHandler()
.
// ... lines 1 - 8 | |
use App\ChainHandler\CasinoHandler; | |
use App\ChainHandler\LevelHandler; | |
use App\ChainHandler\OnFireHandler; | |
// ... lines 12 - 16 | |
class GameApplication | |
{ | |
// ... lines 19 - 26 | |
public function __construct(private readonly CharacterBuilder $characterBuilder) | |
{ | |
// ... lines 29 - 30 | |
$casinoHandler = new CasinoHandler(); | |
$levelHandler = new LevelHandler(); | |
$onFireHandler = new OnFireHandler(); | |
// ... lines 34 - 38 | |
} | |
// ... lines 40 - 219 | |
} |
Here's where we can decide the order or sequence of the chain. If your application doesn't need to execute the handlers in any particular order, great! You can set this up however you want! But in our case, we know that the CasinoHandler
can impact the player if we roll a 7
, so that needs to be called first. The other two handlers can go in any order, so we'll start with the CasinoHandler
.
Write $casinoHandler->setNext($levelHandler)
, followed by the LevelHandler
- $levelHandler->setNext($onFireHandler)
- and we can leave the OnFireHandler
alone. It'll be the last one in the chain. The last thing we need to do is to set the CasinoHandler
in a property of this class, so write $this->xpBonusHandler = $casinoHandler
, and hold "Option" + "Enter" to add the property. Oh! And change its type to XpBonusHandlerInterface
. Perfect!
// ... lines 1 - 11 | |
use App\ChainHandler\XpBonusHandlerInterface; | |
// ... lines 13 - 16 | |
class GameApplication | |
{ | |
// ... lines 19 - 24 | |
private XpBonusHandlerInterface $xpBonusHandler; | |
// ... line 26 | |
public function __construct(private readonly CharacterBuilder $characterBuilder) | |
{ | |
// ... lines 29 - 34 | |
$casinoHandler->setNext($levelHandler); | |
$levelHandler->setNext($onFireHandler); | |
$this->xpBonusHandler = $casinoHandler; | |
} | |
// ... lines 40 - 219 | |
} |
Okay, this chain needs to be triggered after a battle finishes, and there's a convenient method we can use to do that. Find the endBattle()
method, and right before notifying the observers, trigger the chain by writing $xpBonus = $this->xpBonusHandler->handle()
, where the first argument is the $winner
and the second is $fightResultSet->of($winner)
. Finally, we'll add the extra XP to the $winner
with $winner->addXp($xpBonus)
.
// ... lines 1 - 16 | |
class GameApplication | |
{ | |
// ... lines 19 - 100 | |
private function endBattle(FightResultSet $fightResultSet, Character $winner, Character $loser): void | |
{ | |
// ... lines 103 - 109 | |
$xpBonus = $this->xpBonusHandler->handle($winner, $fightResultSet->of($winner)); | |
$winner->addXp($xpBonus); | |
// ... lines 112 - 116 | |
} | |
// ... lines 118 - 222 | |
} |
Cool! Let's give this a try!
Spin over to your terminal and run:
php bin/console app:game:play
I'll be a fighter and attack until I hopefully win, and... we won! But, hmm... how do we know which handler kicked in? Well, that's a downside of this design pattern. It's hard to debug, since any handler can be called. To get around this, we can print a message in each handler so it's obvious.
Debugging Handlers
To do this quickly, you can copy the code from below this video, paste, and... sweet! Let's see if that worked!
// ... lines 1 - 6 | |
use App\GameApplication; | |
// ... line 8 | |
class LevelHandler implements XpBonusHandlerInterface | |
{ | |
// ... lines 11 - 12 | |
public function handle(Character $player, FightResult $fightResult): int | |
{ | |
if ($player->getLevel() === 1) { | |
GameApplication::$printer->info('You earned extra XP thanks to the Level handler!'); | |
// ... lines 17 - 18 | |
} | |
// ... lines 20 - 25 | |
} | |
// ... lines 27 - 31 | |
} |
// ... lines 1 - 6 | |
use App\GameApplication; | |
// ... line 8 | |
class OnFireHandler implements XpBonusHandlerInterface | |
{ | |
// ... lines 11 - 12 | |
public function handle(Character $player, FightResult $fightResult): int | |
{ | |
if ($fightResult->getWinStreak() >= 3) { | |
GameApplication::$printer->info('You earned extra XP thanks to the OnFire handler!'); | |
// ... lines 17 - 18 | |
} | |
// ... lines 20 - 25 | |
} | |
// ... lines 27 - 31 | |
} |
// ... lines 1 - 7 | |
use App\GameApplication; | |
// ... line 9 | |
class CasinoHandler implements XpBonusHandlerInterface | |
// ... lines 11 - 13 | |
public function handle(Character $player, FightResult $fightResult): int | |
{ | |
// ... lines 16 - 24 | |
if ($dice1 === $dice2) { | |
GameApplication::$printer->info('You earned extra XP thanks to the Casino handler!'); | |
// ... lines 27 - 28 | |
} | |
// ... lines 30 - 35 | |
} | |
// ... lines 37 - 41 | |
} |
Back at your terminal, run
php bin/console app:game:play
Again, play until the battle finishes, and... look at this! There's our message! The LevelHandler
kicked in and rewarded us with extra XP. Awesome!
Okay, this is cool, but I think we can do better. We can leverage Symfony's dependency injection to initialize the chain. As a bonus, we'll refactor the handlers to stop checking to see if $next
is set by applying the Null Object pattern. Let's do this!