php 52 lines · 9 steps

Streaming a filtered CSV export in Laravel

An invokable controller streams a memory-safe CSV of orders using validated filters and chunked queries.

Explained by highlit
1<?php
2 
3namespace App\Http\Controllers;
4 
5use App\Models\Order;
6use Illuminate\Http\Request;
7use Symfony\Component\HttpFoundation\StreamedResponse;
8 
9class OrderExportController extends Controller
10{
11 public function __invoke(Request $request): StreamedResponse
12 {
13 $filters = $request->validate([
14 'status' => ['nullable', 'in:pending,paid,shipped,cancelled'],
15 'from' => ['nullable', 'date'],
16 'to' => ['nullable', 'date', 'after_or_equal:from'],
17 ]);
18 
19 $query = Order::query()
20 ->with('customer:id,name,email')
21 ->when($filters['status'] ?? null, fn ($q, $status) => $q->where('status', $status))
22 ->when($filters['from'] ?? null, fn ($q, $from) => $q->whereDate('created_at', '>=', $from))
23 ->when($filters['to'] ?? null, fn ($q, $to) => $q->whereDate('created_at', '<=', $to))
24 ->latest('created_at');
25 
26 $filename = sprintf('orders-%s.csv', now()->format('Y-m-d-His'));
27 
28 return response()->streamDownload(function () use ($query) {
29 $handle = fopen('php://output', 'wb');
30 
31 fputcsv($handle, ['Order #', 'Customer', 'Email', 'Status', 'Total', 'Placed At']);
32 
33 $query->chunk(500, function ($orders) use ($handle) {
34 foreach ($orders as $order) {
35 fputcsv($handle, [
36 $order->reference,
37 $order->customer->name,
38 $order->customer->email,
39 ucfirst($order->status),
40 number_format($order->total / 100, 2),
41 $order->created_at->toDateTimeString(),
42 ]);
43 }
44 });
45 
46 fclose($handle);
47 }, $filename, [
48 'Content-Type' => 'text/csv',
49 'Cache-Control' => 'no-store, no-cache',
50 ]);
51 }
52}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Streaming responses let you send a file to the client without ever holding the whole dataset in memory.
  2. 2Chunking database results keeps large exports flat in memory instead of loading every row at once.
  3. 3Building the query lazily and running it inside the stream callback defers all the heavy work until download time.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Streaming a filtered CSV export in Laravel — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code