The ObjectBehavior Magic
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 hardest part of phpspec for me was how weird these spec classes look. They're... complete magic! You're supposed to pretend that the $this
variable is a Dinosaur
object... even though we're not in that class? And also... I guess that means that phpspec somehow instantiates a new Dinosaur
object before it runs each example? Then, just when you get used to the weirdness of treating $this
like a Dinosaur
object and calling real methods on it... we suddenly call a matcher method - like shouldReturn(0)
. What is going on!?
Digging into ObjectBehavior
Let's find out. Because when I finally saw how all this worked behind the scenes, I instantly felt much more comfortable. All of this magic starts with the base ObjectBehavior
class. Hold Command or Ctrl and click to open that.
Ah, ok: see that protected $object
property? Surprise! That is actually the underlying Dinosaur
object that we're testing. Well, that's not 100% true - but imagine it is for a minute. So, at some point, phpspec instantiates a Dinosaur
object and stores it on that property.
Pretty much all of the magic of this class is thanks to the __call()
method. If you're not familiar with this method, that's great! It's a magic PHP method that you should probably not use - but it's perfect for phpspec. If you call a non-existent method on an object, but that class has an __call()
method, instead of freaking out and throwing an error, PHP will instead execute __call()
and pass it the method name and the arguments you were trying to use.
And what does ObjectBehavior
do in this method? It basically calls that method on $this->object
and passes it the arguments! This is why, when we say $this->getLength()
, it works! The getLength()
method does not exist on ObjectBehavior
. But thanks to the __call()
method, it forwards that call to the actual Dinosaur
object. ObjectBehavior
also has a few other methods, like __get()
and __set()
to forward setting properties and other stuff.
The Wrapped Object
Let's see what some of this looks like in the wild. Close that class and, in any of the examples, var_dump($this)
. Go tests go!
// ... lines 1 - 9 | |
class DinosaurSpec extends ObjectBehavior | |
{ | |
// ... lines 12 - 49 | |
function it_should_not_shrink() | |
{ | |
// ... lines 52 - 55 | |
var_dump($this); | |
} | |
} |
./vendor/bin/phpspec run
Interesting... As we expected, $this
is really an instance of DinosaurSpec
. But check out the $object
property. I lied! It is not an instance of Dinosaur
! Gasp! Nope, it's some Subject
object from phpspec. But inside of it is something called a WrappedObject
and inside if it, yep! There is the Dinosaur
object.
So, it's a bit more complex than we thought at first, but phpspec did create a Dinosaur
object and set it on that object
property... just wrapped inside a few other objects to help the magic.
For the most part, we pretend like we're interacting directly with a Dinosaur
object. But, if you did need to get the actual, underlying Dinosaur
object, that's possible! Try $this->getWrappedObject()
, then run the test again:
// ... lines 1 - 49 | |
function it_should_not_shrink() | |
{ | |
// ... lines 52 - 55 | |
var_dump($this->getWrappedObject()); | |
} | |
// ... lines 58 - 59 |
./vendor/bin/phpspec run
Cool! That gives us the real Dinosaur object. And it's length is really 15, because when we call $this->setLength(15)
, that eventually is called on the real, underlying object.
Most of the time, you won't need to call getWrappedObject()
, though there are a few edge-case exceptions. Like, imagine if our Dinosaur
class had a method on it that started with should
, like shouldHandle()
. Well... that won't work. phpspec thinks that when we call anything starting with should
or shouldNot
, that we're trying to execute a matcher - not a method. Check it out:
// ... lines 1 - 49 | |
function it_should_not_shrink() | |
{ | |
// ... lines 52 - 55 | |
$this->shouldHandle(2); | |
} | |
// ... lines 58 - 59 |
./vendor/bin/phpspec run
There it is: "no handle matcher found". For this edge-case, you can use $this->callOnWrappedObject()
with shouldHandle
and an array of arguments you want. Try it now:
// ... lines 1 - 49 | |
function it_should_not_shrink() | |
{ | |
// ... lines 52 - 55 | |
$this->callOnWrappedObject('shouldHandle', [2]); | |
} | |
// ... lines 58 - 59 |
./vendor/bin/phpspec run
Nice! It fails... but with the correct failure: it sees that there is no shouldHandle()
method and, actually, asks us to generate it. Choose no - we're just messing around.
Next: there's one more piece of magic we haven't talked about: when we call $this->getLength()
, that should return 15. So then... how the heck are we able to call a method on that?