Documentation
when expressions
Turn a complete multi-branch decision into one typed value without hiding meaningful work in helper calls.An if statement controls what happens next. Sometimes the decision itself is the value you need: a shipping quote, a permission level, or the response returned from a service. PHP's ternary operator and match expression are excellent when each result is already a direct expression. A different shape is needed when a branch must validate input, calculate intermediate values, record an event, or iterate over data before it can produce the result.
when gives those branches room to work while requiring the entire decision to produce one compatible result.
Produce a value from realistic branches
ShippingQuote $quote = when ($basket->isEmpty()) {
return ShippingQuote::free();
} else when ($customer->hasPriorityDelivery()) {
Money $fee = $rates->priorityFeeFor($basket);
$audit->recordPriorityQuote($customer, $fee);
return new ShippingQuote('priority', $fee);
} else {
Money $fee = $rates->standardFeeFor($basket);
return new ShippingQuote('standard', $fee);
};
Each branch is a block, so it can declare branch-local values and perform meaningful work before yielding. The return belongs to the when expression; it does not return from the enclosing function.
The surrounding declaration establishes the expected result type. Every reachable branch must produce a ShippingQuote or a compatible subtype.
The final else makes the value total
A value-producing decision cannot leave the destination uninitialized. A final else is therefore mandatory:
ShippingQuote $quote = when ($customer->hasPriorityDelivery()) {
Money $fee = $rates->priorityFeeFor($basket);
$audit->recordPriorityQuote($customer, $fee);
return new ShippingQuote('priority', $fee);
} else when ($customer->acceptsPickup()) {
Store $store = $stores->nearestTo($customer->address());
$audit->recordPickupQuote($customer, $store);
return ShippingQuote::pickup($store);
}; // rejected: other customers still need a result
A branch that always throws is complete because its type is never:
Customer $customer = when ($record !== null) {
ValidatedCustomerRecord $valid = $validator->validate($record);
return Customer::fromRecord($valid);
} else {
$audit->recordMissingCustomer($customerId);
throw new CustomerNotFound($customerId);
};
Every other reachable branch must yield a value or terminate.
Evaluation is predictable
Conditions are evaluated from left to right and at most once. Evaluation stops at the first matching branch, just as it does in an if / else if / else chain. PHP truthiness and comparison semantics remain unchanged.
Variables declared in a branch stay inside that branch:
string $message = when ($response->isSuccessful()) {
string $reference = $response->reference();
return 'Accepted: ' . $reference;
} else {
string $reason = $response->failureReason();
return 'Declined: ' . $reason;
};
// $reference and $reason are not available here.
break and continue are rejected inside a value-producing branch because there is no loop result for them to provide.
Use when where a value is expected
++PHP supports when in the positions where an expression naturally supplies data:
- a typed local initializer;
- the right-hand side of a later assignment;
- a function or method return operand;
- a direct call argument;
- an array element.
return when ($request->acceptsJson()) {
array<string, mixed> $payload = $presenter->forJson($request);
$audit->recordResponseFormat('json');
return Response::json($payload);
} else {
string $html = $presenter->forHtml($request);
$audit->recordResponseFormat('html');
return Response::html($html);
};
Use a normal if when the goal is control flow or side effects and no single value is produced. Use a ternary for a short two-expression choice. Use match when every arm maps directly to an expression. Use when when one or more branches need a readable block of preparation before returning the typed result.
See the generated PHP
The compiler rewrites when as ordinary PHP if and else assignments. For example:
string $message = when ($payment->isSettled()) {
string $reference = $payment->reference();
$audit->recordSettlement($reference);
return 'Paid: ' . $reference;
} else {
string $reason = $payment->pendingReason();
$audit->recordPendingPayment($reason);
return 'Pending: ' . $reason;
};
becomes:
if ($payment->isSettled()) {
/** @var string $reference */
$reference = $payment->reference();
$audit->recordSettlement($reference);
$__ppphp_when_1 = 'Paid: ' . $reference;
} else {
/** @var string $reason */
$reason = $payment->pendingReason();
$audit->recordPendingPayment($reason);
$__ppphp_when_1 = 'Pending: ' . $reason;
}
/** @var string $message */
$message = $__ppphp_when_1;
Diagnostics continue to point to the original when expression, while the generated file remains readable PHP.
Keep branch contracts honest
If one branch returns Order and another returns string, the compiler rejects the expression instead of widening the result silently. If the destination is deliberately broad, write that type at the destination.
Calls inside a branch still contribute checked errors, and branch-local declarations follow the typed local rules. The same checks therefore cover the entire expression and the code around it.