illuminate/conditionable
Illuminate/Conditionable provides the Conditionable trait, adding when() and unless() methods to Laravel classes. Run callbacks conditionally without verbose if statements, keeping fluent method chains clean and readable. Lightweight and dependency-free, it ships as part of the Laravel framework.
You probably already have it. illuminate/conditionable is a read-only split of laravel/framework and is pulled in transitively by illuminate/support, so a normal Laravel app needs no composer require. Confirm with:
composer show illuminate/conditionable # standalone/component installs
composer show laravel/framework # full app: framework `replace`s it
In a full app the source lives at vendor/laravel/framework/src/Illuminate/Support/Traits/Conditionable.php; in a component-based install it is vendor/illuminate/conditionable/Traits/Conditionable.php. Read that file first — it is ~70 lines and defines the entire contract (when, unless, the HigherOrderWhenProxy).
Only add the direct dependency when you need the trait without the rest of Laravel (a standalone package, Lumen, a non-Laravel codebase using illuminate components):
composer require illuminate/conditionable
First use case — optional request filters. The canonical day-to-day use is keeping a query readable when filters arrive from a request:
$users = User::query()
->when($request->filled('search'), fn ($q, $search) => $q->where('name', 'like', "%{$search}%"))
->when($request->boolean('active_only'), fn ($q) => $q->where('active', true))
->when($request->user()->isAdmin(), fn ($q) => $q->withTrashed())
->latest()
->paginate();
Import the trait as Illuminate\Support\Traits\Conditionable (note the Traits segment) and use it in any of your own classes to get the same chainability.
Conditional object/DTO assembly. Because the callback receives the instance and the chain falls back to $this when the condition is false, you can build value objects without a single if:
return (new Invoice())
->when($order->isPaid(), fn ($i) => $i->markPaid())
->when($order->hasDiscount(), fn ($i, $on) => $i->applyDiscount($order->discount))
->when($order->isGift(), fn ($i) => $i->asGift(), fn ($i) => $i->asStandard());
The third argument is the else-branch. Prefer it over when(!$cond, ...) — it reads better and evaluates the condition once.
Reuse the condition value inside the closure. The callback signature is ($this, $value), so you never need to capture the condition again:
->when($request->input('role'), fn ($q, $role) => $q->where('role', $role))
Higher-order shorthand. With a single argument, when() returns a HigherOrderWhenProxy, letting you drop the closure for simple calls:
$query->when($includeTrashed)->withTrashed();
$mail->when($isDebug)->onQueue('debug');
Reach for this only when the conditional call takes no arguments of its own and needs no branching — otherwise the callback form is clearer.
Lazy conditions. Pass a closure as the condition to defer evaluation (and to receive $this as its argument):
$query->when(fn () => auth()->user()?->isAdmin(), fn ($q) => $q->withTrashed());
Use this for anything that hits the DB, the container, or config, so it isn't evaluated when the chain is assembled for an unrelated path.
Pipeline/service chaining. The trait is most valuable in service classes, jobs, and HTTP client calls — anywhere Laravel already chains fluently:
Http::withToken($token)
->when($trace, fn ($http) => $http->withHeader('X-Trace', $traceId))
->when($retryable, fn ($http) => $http->retry(3, 200))
->post('/orders', $payload);
Encapsulate recurring conditionals. If the same when() expression appears in several places, wrap it in your own trait (which itself uses Conditionable) or a local scope:
trait HasVisibilityFilters
{
use Conditionable;
public function whenPublished(bool $published): static
{
return $this->when($published, fn ($q) => $q->whereNotNull('published_at'));
}
}
Test both branches explicitly. Assert the callback does not run when the condition is false, rather than only asserting the happy path:
$called = false;
$subject->when(false, function () use (&$called) { $called = true; });
$this->assertFalse($called);
$this->assertSame($subject, $subject->when(false, fn ($s) => $s));
Note Collection has the same when/unless semantics plus whenEmpty()/whenNotEmpty() — the mental model transfers directly between collections, query builders, and your own classes.
Illuminate\Support\Traits\Conditionable. Older tutorials show Illuminate\Support\Conditionable, which no longer resolves — a wasted half hour if you copy from a stale blog post.replaced by laravel/framework. Never fork or pin the split repo — send fixes upstream to the monorepo.when() returns a proxy, not $this. $obj->when(true) yields a HigherOrderWhenProxy, which breaks assertSame, typed return declarations (: static), and instanceof checks. Fix: pass a callback, or immediately force resolution with ->when($cond, fn ($x) => $x).?? $this swallow. The callback's return value replaces the chain subject unless it is null, in which case $this is returned. So a callback that returns null on purpose silently keeps chaining, and one that returns a Collection/int/string silently changes the chain's type. End the chain outside the last when() if the terminal call returns something else.filled(). 0, '', [], and '0' are all falsy. Pass real booleans ($request->boolean(...), $model->exists) rather than raw input, or you will ship a filter that silently never runs.when() calls sharing one condition is an if block in disguise; hoist the condition into a variable or a named scope. The trait earns its keep on 1–3 conditionals inside an otherwise linear chain.dump()/ray() inside the closure. If it never fires, the condition was falsy (see truthiness above) or the method was reached via the proxy path. Keep a breakpoint in Conditionable::when() when a chain "skips" a step.$this or the callback's type," so generics on Collection/Builder can degrade after a when(). Add @return $this on wrapper methods and type-hint the closure param (fn (Builder $q) => ...) for inference inside the callback.illuminate/conditionable on the same major as your other illuminate packages (^9.0|^10.0|^11.0|^12.0). In a full app, adding it directly invites composer conflicts for zero benefit.Collection/Builder for domain-specific helpers (whenFilled(), whenOwnedBy()).How can I help you explore Laravel packages today?