Conditionable 트레이트란
Illuminate\Support\Traits\Conditionable 트레이트는 오브젝트에 when()과 unless() 메서드를 추가합니다. 조건에 따라 처리를 분기하면서 메서드 체인을 이어갈 수 있는 것이 특징입니다.
실제 소스는 src/Illuminate/Conditionable/Traits/Conditionable.php에 있습니다. Illuminate\Support\Traits\Conditionable이라는 별칭에서 참조되고 있습니다.
QueryBuilder·EloquentBuilder·Mail·Notification 등, Laravel의 많은 클래스가 이 트레이트를 사용하고 있습니다.
기본적인 사용법
when() — 조건이 참일 때 실행하기
use Illuminate\Support\Collection;
$results = collect([1, 2, 3, 4, 5])
->when(true, function (Collection $collection) {
return $collection->filter(fn ($n) => $n > 2);
});
// [3, 4, 5]
첫 번째 인수의 값이 참일 때, 두 번째 인수의 콜백이 실행됩니다. 거짓일 때는 세 번째 인수의 콜백(기본값)이 실행됩니다.
$results = collect([1, 2, 3, 4, 5])
->when(false, function (Collection $collection) {
return $collection->filter(fn ($n) => $n > 2);
}, function (Collection $collection) {
return $collection->filter(fn ($n) => $n < 3);
});
// [1, 2]
unless() — 조건이 거짓일 때 실행하기
unless()는 when()의 반대입니다. 조건이 거짓일 때 콜백이 실행됩니다.
$results = collect([1, 2, 3, 4, 5])
->unless(false, function (Collection $collection) {
return $collection->take(3);
});
// [1, 2, 3]
메서드 체인이 이어지는 이유
콜백의 반환값이 null인 경우, $this(트레이트를 사용하는 오브젝트)가 반환됩니다. 콜백이 null을 반환하지 않는 경우에는 그 반환값이 그대로 반환됩니다.
// $this が返るのでチェーンが続く
$query = User::query()
->when($request->has('active'), function ($query) {
$query->where('active', true); // void / null を返す
})
->when($request->filled('name'), function ($query) use ($request) {
$query->where('name', 'like', "%{$request->name}%");
})
->orderBy('created_at', 'desc');
소스 코드에서는 다음과 같이 구현되어 있습니다.
if ($value) {
return $callback($this, $value) ?? $this;
} elseif ($default) {
return $default($this, $value) ?? $this;
}
return $this;
콜백이 명시적인 값을 반환한 경우에는 그 값이 체인의 다음으로 전달됩니다. 아무 것도 반환하지 않는(null) 경우에는 $this가 반환됩니다.
인수 없이 호출하기 — HigherOrderWhenProxy
인수 없이 when()을 호출하면 HigherOrderWhenProxy가 반환됩니다. 이를 사용하면 조건을 나중에 설정할 수 있습니다.
$query = User::query()
->when()->isActive() // isActive() を条件として評価
->where('role', 'admin');
인수 하나로 호출하면, 그 값을 조건으로 갖는 프록시가 반환됩니다.
// 引数1つ: 条件だけを渡してプロキシを得る
$proxy = collect([1, 2, 3])->when($request->has('filter'));
// $proxy->methodName() で条件が真のときだけ methodName() を呼び出せる
클로저를 값으로 전달하기
첫 번째 인수에 클로저를 전달하면, 해당 클로저가 실행된 반환값이 조건으로 사용됩니다.
$results = User::query()
->when(
fn ($query) => $request->filled('role'),
fn ($query) => $query->where('role', $request->role)
)
->get();
이를 통해 조건의 평가 로직을 콜백으로 분리할 수 있습니다.
QueryBuilder에서의 전형적인 패턴
when()을 사용한 동적 쿼리 구축은 가장 일반적인 유스케이스입니다.
public function index(Request $request)
{
$users = User::query()
->when($request->filled('search'), function ($query) use ($request) {
$query->where('name', 'like', "%{$request->search}%")
->orWhere('email', 'like', "%{$request->search}%");
})
->when($request->filled('role'), fn ($q) => $q->where('role', $request->role))
->when($request->boolean('verified'), fn ($q) => $q->whereNotNull('email_verified_at'))
->when(
$request->filled('sort'),
fn ($q) => $q->orderBy($request->sort, $request->get('direction', 'asc')),
fn ($q) => $q->latest()
)
->paginate();
return UserResource::collection($users);
}
Conditionable을 자체 클래스에 적용하기
트레이트를 use하는 것만으로 when() / unless()를 사용할 수 있게 됩니다.
namespace App\Services;
use Illuminate\Support\Traits\Conditionable;
class ReportBuilder
{
use Conditionable;
protected array $filters = [];
protected bool $includeArchived = false;
protected ?string $groupBy = null;
public function withArchived(): static
{
$this->includeArchived = true;
return $this;
}
public function groupBy(string $column): static
{
$this->groupBy = $column;
return $this;
}
public function addFilter(string $column, mixed $value): static
{
$this->filters[$column] = $value;
return $this;
}
public function build(): \Illuminate\Database\Eloquent\Builder
{
return Report::query()
->when($this->includeArchived, fn ($q) => $q->withTrashed())
->when($this->groupBy, fn ($q) => $q->groupBy($this->groupBy))
->when($this->filters, function ($q) {
foreach ($this->filters as $column => $value) {
$q->where($column, $value);
}
});
}
}
// 使用例
$query = (new ReportBuilder)
->when($request->boolean('archived'), fn ($b) => $b->withArchived())
->when($request->filled('group'), fn ($b) => $b->groupBy($request->group))
->addFilter('status', 'published')
->build();
Mail·Notification·Response에서의 활용
when()은 메일·알림·응답 구축에서도 사용할 수 있습니다.
use Illuminate\Mail\Mailable;
use Illuminate\Support\Traits\Conditionable;
class OrderConfirmation extends Mailable
{
public function build(): static
{
return $this
->subject('ご注文を承りました')
->view('emails.order.confirmation')
->when($this->order->hasDiscount(), function (Mailable $mail) {
$mail->attach(storage_path('discounts/coupon.pdf'));
})
->when(app()->environment('production'), function (Mailable $mail) {
$mail->bcc('[email protected]');
});
}
}
tap()과의 사용 구분
tap()과 when()은 비슷하지만 목적이 다릅니다.
| tap() | when() |
|---|
| 목적 | 부수 효과(로그·디버그) | 조건 분기 |
| 반환값 | 항상 $this | 조건에 따라 $this 또는 콜백의 반환값 |
| 조건 | 없음 | 있음 |
// tap: 副作用のために使う。戻り値は常に $this
$user = User::find($id)
->tap(fn ($user) => Log::info("User {$user->id} loaded"));
// when: 条件分岐に使う
$user = User::query()
->when($isAdmin, fn ($q) => $q->where('role', 'admin'))
->first();
디버그나 부수 효과를 위해 체인 중간에 뭔가를 하고 싶은 것뿐이라면 tap()을 사용합니다. 조건에 따라 처리를 전환하고 싶다면 when() / unless()를 사용합니다.
다음 단계
컬렉션의 고차 메시지
$collection->map->method()와 같은 구문의 구조와 실전적인 사용법을 배웁니다.