Buy
Buy

Describing the Factory Service

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Ok first, we've learned a lot about phpspec so far. But... we've still only described one class - and a pretty simple one! It's time to dig deeper and add more complexity to our app.

Here's the deal: that new growVelociraptor() factory method has made our life a lot easier because, in our pretend app, we constantly need to create new velociraptors. But now, we also need to be able to create a few other popular dinosaurs - like T-rexes and Stegosaurus! We could keep adding more static methods to Dinosaur. But to keep things organized, I'd rather put all the logic into a new class - how about DinosaurFactory. Or, we might choose to do this because creating a Dinosaur requires some other services - like a database object - and we can't access services from simple model classes like Dinosaur.

Describing a new Class

So, hey! We need a new class! Well, to say it better, it's time for us to describe a new class: ./vendor/bin/phpspec describe and, for the name, how about App/Factory/DinosaurFactory.

./vendor/bin/phpspec describe App/Factory/DinosaurFactory

That creates one new file: DinosaurFactorySpec. Let's go check it out! Like last time, we get one super basic example for free - asserting that $this should be an instance of DinosaurFactory. That's... kinda silly... but it's enough to force some code generation! Go run phpspec:

... lines 1 - 9
class DinosaurFactorySpec extends ObjectBehavior
{
function it_is_initializable()
{
$this->shouldHaveType(DinosaurFactory::class);
}
}
./vendor/bin/phpspec run

Why, yes! I would love for you to generate that class for us. Now, the spec passes.

growVelociraptor() Example

Our first goal is to move the growVelociraptor() method into DinosaurFactory, but I want to follow the red, green, refactor cycle. So first, describe that functionality with a new example: function it_grows_a_large_velociraptor(). Then, call the method: $dinosaur = $this->growVelociraptor(5).

... lines 1 - 16
function it_grows_a_large_velociraptor()
{
$dinosaur = $this->growVelociraptor(5);
... line 20
}
... lines 22 - 23

The Magic Behind phpspec's Subject

Eventually, after coding all of this up, we know that the $dinosaur variable should be a Dinosaur object. But we also know that phpspec adds a lot of magic. Check this out: var_dump($dinosaur). Now, run phpspec:

... lines 1 - 16
function it_grows_a_large_velociraptor()
{
... line 19
var_dump($dinosaur);
}
... lines 22 - 23
./vendor/bin/phpspec run

First, it notices that the growVelociraptor() method is missing. Hit enter to generate that. Ok: scroll up to check out the dumped object. Cool! The $dinosaur variable is actually a Subject object! Right now, the underlying value is null because the new growVelociraptor() method doesn't return anything.

But more importantly, do you remember where we saw the Subject object earlier? It was in DinosaurSpec! When we call $this->getLength(), that returns the length, but wrapped inside of a Subject object. Why do we care? Because that was the magic layer that allowed us to call ->shouldReturn().

Inside DinosaurFactorySpec, it's the same thing! growVelociraptor will eventually return a Dinosaur object, but phpspec wraps that inside a Subject object. Thanks to that, we can call real methods on the Dinosaur or matcher methods. In other words, the $dinosaur in this class works pretty much exactly like the $this variable in DinosaurSpec. In fact, let's steal four lines of code from here. Paste these into the new example and change all of the $this to $dinosaur. Re-type the "r" in Dinosaur and hit tab so PhpStorm adds its use statement.

... lines 1 - 16
function it_grows_a_large_velociraptor()
{
... lines 19 - 20
$dinosaur->shouldBeAnInstanceOf(Dinosaur::class);
$dinosaur->getGenus()->shouldBeString();
$dinosaur->getGenus()->shouldBe('Velociraptor');
$dinosaur->getLength()->shouldBe(5);
}
... lines 26 - 27

Ok! The growVelociraptor() method is still empty, but let's see what phpspec thinks!

./vendor/bin/phpspec run

Implement the Code

And the tests are red! Step 2: make this work with as little work as possible... or at least without over-engineering it. We can cheat: copy the code from the old growVelociraptor() method. I'll keep this method here just as an example. Back in DinosaurFactory, paste, change the new static to new Dinosaur, change the argument to int $length and give this a Dinosaur return type.

... lines 1 - 6
class DinosaurFactory
{
public function growVelociraptor(int $length): Dinosaur
{
$dinosaur = new Dinosaur('Velociraptor', true);
$dinosaur->setLength($length);
return $dinosaur;
}
}

Try it out:

./vendor/bin/phpspec run

Refactor

Green! So now we get to step 3: refactor. This is our chance to remove duplication or improve things. For example, if I absolutely know that we will add other methods to this class - like growTyrannosaurus() - it might make sense to refactor some logic into a new private function called createDinosaur(). Give this 3 arguments: string $genus, bool $isCarnivorous and int $length. Copy the first two lines above and make each part dynamic.

... lines 1 - 13
private function createDinosaur(string $genus, bool $isCarnivorous, int $length)
{
$dinosaur = new Dinosaur($genus, $isCarnivorous);
$dinosaur->setLength($length);
}
... lines 19 - 20

Finally, the first method can be simplified to: return $this->createDinosaur(), passing Velociraptor, true, and $length. We could have wrote the code this way initially. But now we can refactor confidently because our tests will prove we didn't mess anything up:

... lines 1 - 8
public function growVelociraptor(int $length): Dinosaur
{
return $this->createDinosaur('Velociraptor', true, $length);
}
... lines 13 - 20
./vendor/bin/phpspec run

Oh. Except... I messed something up:

Return value of DinosaurFactory::growVelociraptor() must be an instance of Dinosaur, null returned.

Duh! I forgot my return statement! And I should have added a return type too. Try it again:

... lines 1 - 13
private function createDinosaur(string $genus, bool $isCarnivorous, int $length): Dinosaur
{
... lines 16 - 18
return $dinosaur;
}
... lines 21 - 22
./vendor/bin/phpspec run

Now we know it works. To be honest, I love the red, green, refactor cycle, but I also don't always do it. Heck, I don't even unit test all my code - only the parts that are complex enough to keep me up at night. But I do take one important lesson from it into everything I do: focus on accomplishing the behavior you need and nothing more. Keep things simple until they can't be. And when you get there, write a test first, then get crazy.

Next: we'll describe a new class that depends on another class. Is it finally time to talk about mocking in phpspec? Well... not so fast...

Leave a comment!