Use array_reduce to Build Dynamic Union Queries in Laravel

📖 2 minutes read

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.

Daryle De Silva

VP of Technology

11+ years building and scaling web applications. Writing about what I learn in the trenches.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *