WhenBuilder
Rasuvaeff\Understudy\WhenBuilder
Class — Package: rasuvaeff/understudy — Source — Version: v1.1.0-1-g6ed023f
Type parameters:
TReturn of mixed = mixed
Configures what a stubbed call does.
TReturn is the return type of the specified method. Plain Psalm and PHPStan cannot infer it from the closure on their own — understudy-psalm fills it in, and until then the parameter stays mixed, which is why the template is declared here rather than added later: a published signature cannot grow one without changing its contract.
Not final, and the one class here that is not: ExpectBuilder extends it to add the cardinality verbs, and the two are one fluent vocabulary rather than two. For everyone else it is closed by contract, not by keyword: subclassing is not supported, the protected readonly Expectation it carries is an @internal type that may change in any release, and a subclass reading it is on its own. The keyword is missing only because closing the class would mean duplicating every action verb on ExpectBuilder — and a @final tag is not an option either, since Psalm would then refuse ExpectBuilder itself.
Constructor
__construct(
Expectation\Expectation $expectation,
\Expectation\ReturnContract|null $contract = NULL,
)| Parameter | Type | Default | Description |
|---|---|---|---|
$expectation | Expectation\Expectation | required | |
$contract | \Expectation\ReturnContract|null | NULL | what the specified method may answer with; null only where no signature is known |
Methods
returns()
returns(\TReturn $values): staticReturns each value in turn across successive calls, then keeps returning the last one.
throws()
throws(Throwable $error): staticThrows this exact instance on the call — the same object every time the link answers, which is what a test holding a reference to it expects.
throwsWith()
throwsWith(callable $build): staticThrows an exception built from the call itself, one per call.
$build— builds the exception from the call it answers
The shape throws() cannot express: an exception that carries what the call was made with — new PublishException($message, outboxMessage: $call->args[0]). A throwing answers() closure does the same thing and stays supported; this reads as what it is at the call site.
when(fn () => $publisher->publish(Arg::any()))
->throwsWith(fn (Invocation $call) => new PublishException(
message: 'Publish failed',
outboxMessage: $call->arg('message'),
));answers()
answers(callable $answer): staticComputes the return value for each matching call.
then()
then(): staticOpens the next link of the chain, so the verb that follows describes what the next call does:
when(fn () => $breaker->call($operation))
->returns('ok')
->then()->throws(new ConnectionLost());Once the chain runs out, its last link keeps answering.
where()
where(callable $guard, non-empty-string $description = 'where(…)'): staticNarrows the specification by a predicate over the whole argument list — the one shape a positional matcher cannot express: a constraint over two or more arguments together.
$guard— accepts the argument list the call carried, or refuses it$description— how the guard reads in a failure message
when(fn () => $ledger->transfer($from, Arg::any()))
->where(fn ($from, $to) => $from <= $to)
->returns(true);The closure receives the arguments the call carried, positionally, so it reads like the method's own signature; it runs wherever matching runs — dispatch, verify(), the by-reference slot decision — and a call the positions accept but the guard refuses simply does not match: a broader expectation may still answer it. That also means the guard runs inside the code under test, and an exception it raises is the test's own error.
Works on expect() the same way, and verify() takes one through its where: parameter. One guard per expectation; a second where() is refused. In a failure message the guard renders as where(…) — or as the $description given, which is how a test names what the constraint means:
->where(fn ($w, $h) => $w / $h === 2, description: 'where(16:9)')times()
times(int<0, max> $minimum, int<0, max>|null $maximum = NULL): staticMakes the call count part of what verifyAll() checks. Without it a stub is permission, not a claim.
With one argument the count is exact; with two it is a range, and a null maximum means no upper bound.