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
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Streaming responses let you send a file to the client without ever holding the whole dataset in memory.
- 2Chunking database results keeps large exports flat in memory instead of loading every row at once.
- 3Building the query lazily and running it inside the stream callback defers all the heavy work until download time.
Related explainers
php
<?php namespace App\Http\Middleware;
Idempotent requests with Laravel middleware
idempotency
middleware
caching
Advanced
8 steps
php
<?php namespace App\Services;
Batch image resizing with PHP GD
image-processing
constructor-promotion
match-expression
Intermediate
10 steps
php
<?php namespace App\Cache;
A content-hashed fragment cache in PHP
caching
content-hashing
atomic-writes
Intermediate
10 steps
go
package handler type LineItem struct { SKU string `json:"sku" binding:"required,alphanum"`
Structured validation errors in Gin
validation
json-binding
error-handling
Intermediate
8 steps
php
<?php namespace App\View\Composers;
How a Laravel view composer feeds the navigation
view-composer
caching
dependency-injection
Intermediate
5 steps
php
public function transaction(callable $callback, int $maxAttempts = 3): mixed { $attempt = 0;
Retrying database deadlocks in PHP
retry
transactions
deadlock
Intermediate
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/streaming-a-filtered-csv-export-in-laravel-explained-php-1c90/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.