Builder Improvements
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 SubscribeThe first version of our builder class is done! Though, in GameApplication
, the mage_archer
has two different attack types. Our CharacterBuilder
doesn't support that right now... but we'll add it in a minute.
Clearing State After Building?
Oh, one more thing about the builder class! In the "build" method, after you create the object, you may choose to "reset" the builder object. For example, we could set the Character
to a variable, then, before we return it, reset $maxHealth
and all the other properties back to their original state. Why would we do this? Because it would allow for a single builder to be used over and over again to create many objects - or, characters in this case.
// ... lines 1 - 14 | |
class CharacterBuilder | |
{ | |
// ... lines 17 - 49 | |
public function buildCharacter(): Character | |
{ | |
return new Character( | |
$this->maxHealth, | |
$this->baseDamage, | |
$this->createAttackType(), | |
$this->createArmorType(), | |
); | |
} | |
// ... lines 59 - 78 | |
} |
However, I'm not going to do that... which just means that a single CharacterBuilder
will be meant to be used just one time to build one character. You can choose either option in your app: there isn't a right or wrong way for the builder pattern.
Using the Builder
All right, let's go use this! Inside of GameApplication
, first, just to make life easier, I'm going to create a private function
at the bottom called createCharacterBuilder()
which will return CharacterBuilder
. Inside, return new CharacterBuilder()
.
// ... lines 1 - 4 | |
use App\Builder\CharacterBuilder; | |
// ... lines 6 - 14 | |
class GameApplication | |
{ | |
// ... lines 17 - 78 | |
private function createCharacterBuilder(): CharacterBuilder | |
{ | |
return new CharacterBuilder(); | |
} | |
} |
That's going to be nice because... up here in createCharacter()
, we can use that. I'm going to clear out the old stuff... and now, use the fluid way to make characters: $this->createCharacterBuilder()
, ->setMaxHealth(90)
, ->setBaseDamage(12)
, ->setAttackType('sword')
and ->setArmorType('shield')
. Oh, and, though I didn't do it, it would be nice to add constants on the builder for these strings, like sword
and shield
.
Finally, call ->buildCharacter()
to... build that character!
// ... lines 1 - 7 | |
class GameApplication | |
{ | |
// ... lines 10 - 38 | |
public function createCharacter(string $character): Character | |
{ | |
return match (strtolower($character)) { | |
'fighter' => $this->createCharacterBuilder() | |
->setMaxHealth(90) | |
->setBaseDamage(12) | |
->setAttackType('sword') | |
->setArmorType('shield') | |
->buildCharacter(), | |
// ... lines 48 - 70 | |
}; | |
} | |
// ... lines 73 - 100 | |
} |
That's really nice! And it would be even nicer if creating a character were even more complex, like involving database calls.
To save some time, I'm going to paste in the other three characters, which look similar. Down here for our mage_archer
, I'm currently using the fire_bolt
attack type. We do need to re-add a way to have both fire_bolt
and bow
, but this should work for now.
// ... lines 1 - 38 | |
public function createCharacter(string $character): Character | |
{ | |
return match (strtolower($character)) { | |
// ... lines 42 - 48 | |
'archer' => $this->createCharacterBuilder() | |
->setMaxHealth(80) | |
->setBaseDamage(10) | |
->setAttackType('bow') | |
->setArmorType('leather_armor') | |
->buildCharacter(), | |
'mage' => $this->createCharacterBuilder() | |
->setMaxHealth(70) | |
->setBaseDamage(8) | |
->setAttackType('fire_bolt') | |
->setArmorType('ice_block') | |
->buildCharacter(), | |
'mage_archer' => $this->createCharacterBuilder() | |
->setMaxHealth(75) | |
->setBaseDamage(9) | |
->setAttackType('fire_bolt') // TODO re-add bow! | |
->setArmorType('shield') | |
->buildCharacter(), | |
default => throw new \RuntimeException('Undefined Character') | |
}; | |
} | |
// ... lines 73 - 102 |
Let's try it out! At your terminal, run:
php bin/console app:game:play
Hey! It didn't explode! That's always a happy sign. And if I fight as an archer
... I win! Our app still works!
Allow for Multiple Attack Types
So what about allowing our mage_archer's two attack types? Well, that's the beauty of the builder pattern. Part of our job, when we create the builder class, is to make life as easy as possible for whoever uses this class. That's why I chose to use string
$armorType
and $attackType
instead of objects.
We can solve handling two different AttackTypes
however we want. Personally, I think it would be cool to be able to pass multiple arguments. So let's make that happen!
Over in CharacterBuilder
, change the argument to ...$attackTypes
with an "s", using the fancy ...
to accept any number of arguments. Then, since this will now hold an array, change the property to private array $attackTypes
... and down here, $this->attackTypes = $attackTypes
.
// ... lines 1 - 15 | |
class CharacterBuilder | |
{ | |
// ... lines 18 - 19 | |
private array $attackTypes; | |
// ... lines 21 - 36 | |
public function setAttackType(string ...$attackTypes): self | |
{ | |
$this->attackTypes = $attackTypes; | |
return $this; | |
} | |
// ... lines 43 - 86 | |
} |
Easy. Next we need to make a few changes down in buildCharacter()
, like changing the $attackTypes
strings into objects. To do that, I'm going to say $attackTypes =
and... get a little fancy. You don't have to do this, but I'm going to use array_map()
and the new short fn
syntax - (string $attackType) => $this->createAttackType($attackType)
. For the second argument of array_map()
- the array that we actually want to map - use $this->attackTypes
.
// ... lines 1 - 50 | |
public function buildCharacter(): Character | |
{ | |
$attackTypes = array_map(fn(string $attackType) => $this->createAttackType($attackType), $this->attackTypes); | |
// ... lines 54 - 65 | |
} | |
// ... lines 67 - 88 |
Now, in the private method, instead of reading the property, read an $attackType
argument.
// ... lines 1 - 67 | |
private function createAttackType(string $attackType): AttackType | |
{ | |
return match ($attackType) { | |
// ... lines 71 - 74 | |
}; | |
} | |
// ... lines 77 - 88 |
Ok, we could have done this with a foreach
loop... and if you like foreach
loops better, do it. Honestly, I think I've been writing too much JavaScript lately. Anyways, this basically says:
I want to loop over all of the "attack type" strings and, for each one, call this function where we change that
$attackType
string into anAttackType
object. Then set all of thoseAttackType
objects onto a new$attackTypes
variable.
In other words, this is now an array of AttackType
objects.
To finish this, say if (count($attackTypes) === 1)
, then $attackType = $attackTypes[0]
to grab the first and only attack type. Otherwise, say $attackType = new MultiAttackType()
passing $attackTypes
. Finally, at the bottom, use the $attackType
variable.
// ... lines 1 - 50 | |
public function buildCharacter(): Character | |
{ | |
// ... line 53 | |
if (count($attackTypes) === 1) { | |
$attackType = $attackTypes[0]; | |
} else { | |
$attackType = new MultiAttackType($attackTypes); | |
} | |
// ... line 59 | |
return new Character( | |
// ... lines 61 - 62 | |
$attackType, | |
// ... line 64 | |
); | |
} | |
// ... lines 67 - 88 |
Phew! You can see it's a bit ugly, but that's okay! We're hiding the creation complexity inside this class. And we could easily unit test it.
Let's try things out. Run our command...
./bin/console app:game:play
... let's be a mage_archer
and... awesome! No error! So... I'm going to assume that's all working.
Ok, in GameApplication
, we're instantiating the CharacterBuilder
manually. But what if the CharacterBuilder
needs access to some services to do its job, like the EntityManager so it can make database queries?
Next, let's make this example more useful by seeing how we handle the creation of this CharacterBuilder
object in a real Symfony app by leveraging the service container. We'll also talk about the benefits of the builder pattern.