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 class NameParser {
Parsing a full name into components in PHP
string-parsing
arrays
normalization
Intermediate
8 steps
php
<?php namespace App\Services\Checkout;
Validating coupons with Laravel's Pipeline
pipeline
chain of responsibility
transactions
Intermediate
7 steps
php
<?php namespace App\Services;
How a password strength validator works in PHP
validation
regular-expressions
data-driven
Intermediate
8 steps
python
import random from typing import Iterator, List
How reservoir sampling picks k items
reservoir-sampling
streaming
randomness
Intermediate
5 steps
php
<?php namespace App\Services;
Building a cached daily leaderboard in Laravel
caching
aggregation
eager-loading
Intermediate
9 steps
rust
use chrono::{Duration, NaiveDate}; #[derive(Debug)] pub struct DateRange {
Parsing and iterating date ranges in Rust
error-handling
iterators
parsing
Intermediate
7 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.