Skip to content

Creating a double

php
use Rasuvaeff\Understudy\Understudy;

$repository = Understudy::for(BookRepository::class);

for() returns the contract's own type, so your IDE and your static analyser treat $repository as a BookRepository. It carries no members of its own — see What is understudy for why.

Combining contracts

Several interfaces can be doubled at once:

php
$double = Understudy::for(BookRepository::class, Countable::class);

Understudy unifies compatible signatures across them:

Parameter typeswidened
Return typesthe narrowest compatible declaration, or a synthesised interface intersection
Named argumentsfollow the first (primary) interface

Static contract methods exist on the generated class, because the interface has to be implemented — but calling one raises InvalidCallSpecification. A static call has no double instance to own its state.

Doubling a class

A class can be the first target, with interfaces after it:

php
$repository = Understudy::for(DoctrineBookRepository::class, Countable::class);

What a class double does and does not do:

The target's constructornever runs — the double is built without it, so no side effect of construction reaches your test
Public and protected methodsoverridden and dispatched; a protected one shows up in the transcript and under strict mode, but PHP's own visibility keeps it out of a specification closure
Private and static methodsuntouched — the target keeps them, because there is no instance state to intercept
The destructorreplaced with an empty one, so nothing is torn down that was never built
Writable public propertiesstart at an empty value of their type; object-typed, hooked, final, readonly and private(set) ones are left uninitialized, and reading one raises PHP's own error
cloneproduces a double of its own: same contracts, no expectations, no call log, owned by the context that cloned it

A readonly target produces a readonly double, which PHP requires and which costs nothing — the double declares no properties of its own.

What is refused, and why

Some targets are refused before anything is generated, each with the reason and what to do instead:

  • a final class — see Doubling a final class
  • a class with a non-private final instance method
  • an enum, a trait, an internal class, an anonymous class
  • any class that is not the first target

The rule behind all of them is one rule. A double that cannot intercept every method would run the target's real code against an object whose constructor never ran — which is worse than not building the double at all.

Next