Defaults registry
A nested double of LoggerInterface answers everything with a default and tells the test nothing. A NullLogger is usually what it wanted:
Understudy::defaults(LoggerInterface::class, fn () => new NullLogger());
Understudy::defaults(ClockInterface::class, fn () => FakeClock::frozen());Nullable returns
A registration outranks null. A method declared ?ClockInterface answers with the registered clock, because saying what the type should be means it there too.
Without a registration, such a method is still null.
Which registration wins
The nearest one, measured as distance in the type graph:
- an exact match;
- then the closest registered ancestor.
Two ancestors the same distance away raise AmbiguousDefaultFactory rather than letting whichever was registered first decide — a tie has no order a reader could predict.
A factory that produces the wrong type raises InvalidDefaultValue.
Scope
Registrations belong to the current context. Sibling fibers do not see each other's, and Understudy::reset() drops them with the test.
TIP
Register them in a per-test fixture rather than once for a whole suite. A registry that outlives the test it was written for is a shared mutable default, and the failure it produces reads as if the wrong test was at fault.