Buy Access to Course
05.

Builder Improvements

|

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

The 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.

80 lines | src/Builder/CharacterBuilder.php
// ... 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().

84 lines | src/GameApplication.php
// ... 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!

102 lines | src/GameApplication.php
// ... 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.

102 lines | src/GameApplication.php
// ... 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.

88 lines | src/Builder/CharacterBuilder.php
// ... 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.

88 lines | src/Builder/CharacterBuilder.php
// ... 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.

88 lines | src/Builder/CharacterBuilder.php
// ... 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 an AttackType object. Then set all of those AttackType 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.

88 lines | src/Builder/CharacterBuilder.php
// ... 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.