Table of Contents
When you need to union multiple query builders dynamically, array_reduce provides a clean alternative to chaining .union() calls manually. This is especially useful when the number of queries varies or comes from configuration.
The Problem with Manual Chaining
When building complex queries that combine multiple query builders with UNION, manual chaining becomes verbose:
$query1 = Order::where('status', 'pending')->toBase();
$query2 = Order::where('status', 'processing')->toBase();
$query3 = Order::where('status', 'completed')->toBase();
$combined = $query1->union($query2)->union($query3);
This gets unwieldy when:
– The number of queries changes
– Queries come from configuration
– You’re building queries conditionally
Use array_reduce Instead
array_reduce lets you build the union dynamically:
$queries = [
Order::where('status', 'pending'),
Order::where('status', 'processing'),
Order::where('status', 'completed'),
];
$combined = array_reduce(
$queries,
fn($sub, $query) => $sub ? $sub->union($query->toBase()) : $query->toBase()
);
The closure handles the first iteration (when $sub is null) and subsequent iterations (when $sub contains the accumulated union).
Combine with fromSub for Complex Queries
This pattern shines when used with Eloquent’s fromSub():
$data = Task::query()
->fromSub(
array_reduce(
[
Task::where('priority', 'high'),
Task::where('priority', 'urgent'),
Task::where('status', 'overdue'),
],
fn($sub, $query) => $sub
? $sub->union($query->toBase())
: $query->toBase()
),
'tasks'
)
->with('project', 'assignee')
->get();
This gives you a clean subquery with proper eager loading.
Works with Any Number of Queries
The real power is flexibility:
// Configuration-driven queries
$statusQueries = config('report.statuses')
->map(fn($status) => Report::where('status', $status));
$results = Report::withTrashed()
->fromSub(
array_reduce(
$statusQueries->all(),
fn($sub, $q) => $sub ? $sub->union($q->toBase()) : $q->toBase()
),
'reports'
)
->orderByDesc('created_at')
->get();
// Conditional queries
$queries = collect([
$request->filled('active') ? Item::where('is_active', true) : null,
$request->filled('pending') ? Item::where('status', 'pending') : null,
$request->filled('archived') ? Item::onlyTrashed() : null,
])->filter();
$items = Item::fromSub(
array_reduce(
$queries->all(),
fn($sub, $q) => $sub ? $sub->union($q->toBase()) : $q->toBase()
),
'items'
)->paginate();
The pattern keeps your code DRY when query sources change or grow.
Leave a Reply