Exports and Reports — The BaseExport Pattern
Series: Every Laravel Project Should Have These Building Blocks
Part: 22 of 35 Level: Intermediate Prerequisites: Jobs and Queues
What You’ll Learn
- The
BaseExportabstract class pattern - Collection exports for small datasets
- Query exports for medium datasets
- Queueable exports for large datasets
- Adding formatting, column widths, and headings
The Problem: Duplicated Export Code
Without a shared base class, every export class reinvents the same boilerplate: heading styles, column widths, date formatting, default heading row setup. Five export classes means five copies of the same configuration.
The BaseExport pattern extracts all of that into one place. Each concrete export provides only what’s different: the headers, the data source, and optionally the formatting.
The BaseExport Abstract Class
// app/Exports/BaseExport.php
<?php
declare(strict_types=1);
namespace App\Exports;
use Maatwebsite\Excel\Concerns\FromCollection;
use Maatwebsite\Excel\Concerns\ShouldAutoSize;
use Maatwebsite\Excel\Concerns\WithColumnWidths;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\WithStyles;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
abstract class BaseExport implements
FromCollection,
ShouldAutoSize,
WithHeadings,
WithStyles,
WithColumnWidths
{
/**
* Define the column headings.
*
* @return array<string>
*/
abstract public function headings(): array;
/**
* The number of columns — used to style the heading row.
*/
abstract protected function columnCount(): int;
/**
* Default heading row style: bold text, light gray background.
*/
public function styles(Worksheet $sheet): array
{
return [
1 => [
'font' => ['bold' => true, 'size' => 12],
'alignment' => ['horizontal' => 'center'],
'fill' => [
'fillType' => 'solid',
'startColor' => ['rgb' => 'E2EFDA'],
],
],
];
}
/**
* Default column widths — override in subclass for custom widths.
*
* @return array<string, float>
*/
public function columnWidths(): array
{
// Build default widths for all columns (A, B, C, ...)
$columns = range('A', chr(ord('A') + $this->columnCount() - 1));
return array_fill_keys($columns, 20.0);
}
}
A Simple Collection Export
For small datasets (< ~5,000 rows), load everything into memory:
// app/Exports/OrdersExport.php
final class OrdersExport extends BaseExport
{
public function __construct(
private readonly Carbon $fromDate,
private readonly Carbon $toDate,
) {}
public function collection(): Collection
{
return Order::with(['user', 'items'])
->whereBetween('created_at', [$this->fromDate, $this->toDate])
->orderByDesc('created_at')
->get()
->map(fn (Order $order): array => [
$order->id,
$order->user->name,
$order->user->email,
$order->total_formatted,
$order->status->label(),
$order->created_at->format('Y-m-d H:i'),
$order->items->count(),
]);
}
public function headings(): array
{
return [
'Order ID',
'Customer Name',
'Customer Email',
'Total',
'Status',
'Created At',
'Items Count',
];
}
protected function columnCount(): int
{
return 7;
}
// Override column widths for specific columns
public function columnWidths(): array
{
return [
'A' => 12, // Order ID
'B' => 25, // Customer Name
'C' => 35, // Customer Email
'D' => 15, // Total
'E' => 18, // Status
'F' => 20, // Created At
'G' => 12, // Items Count
];
}
}
Dispatch from a controller:
public function export(ExportOrdersRequest $request): BinaryFileResponse
{
$export = new OrdersExport(
fromDate: $request->date('from_date'),
toDate: $request->date('to_date'),
);
return Excel::download($export, 'orders-export.xlsx');
}
A Query Export (Medium Datasets)
For 5,000–50,000 rows, use FromQuery to avoid loading all records at once:
// app/Exports/ServiceRequestsExport.php
use Maatwebsite\Excel\Concerns\FromQuery;
use Maatwebsite\Excel\Concerns\WithMapping;
final class ServiceRequestsExport extends BaseExport implements FromQuery, WithMapping
{
public function __construct(
private readonly ?Carbon $fromDate = null,
private readonly ?Carbon $toDate = null,
private readonly ?ServiceRequestStatus $status = null,
) {}
public function query(): Builder
{
return ServiceRequest::with(['createdBy', 'rejectedBy', 'tasks'])
->when($this->fromDate, fn ($q) => $q->where('created_at', '>=', $this->fromDate))
->when($this->toDate, fn ($q) => $q->where('created_at', '<=', $this->toDate))
->when($this->status, fn ($q) => $q->where('status', $this->status))
->orderByDesc('created_at');
}
/**
* Map each Eloquent model to a row array.
*/
public function map(ServiceRequest $serviceRequest): array
{
return [
$serviceRequest->id,
$serviceRequest->createdBy?->name,
$serviceRequest->status->label(),
$serviceRequest->tasks->count(),
$serviceRequest->created_at->format('Y-m-d'),
$serviceRequest->completed_at?->format('Y-m-d') ?? '—',
$serviceRequest->rejection_reason ?? '—',
];
}
public function headings(): array
{
return [
'ID',
'Created By',
'Status',
'Tasks',
'Created',
'Completed',
'Rejection Reason',
];
}
protected function columnCount(): int
{
return 7;
}
}
A Queueable Export (Large Datasets)
For 50,000+ rows, queue the export and email a download link when done:
use Maatwebsite\Excel\Concerns\FromQuery;
use Maatwebsite\Excel\Concerns\WithMapping;
use Maatwebsite\Excel\Concerns\WithChunkReading;
use Maatwebsite\Excel\Concerns\ShouldQueue;
use Illuminate\Contracts\Queue\ShouldQueue as ShouldQueueContract;
final class LargeProductCatalogueExport extends BaseExport
implements FromQuery, WithMapping, ShouldQueue, ShouldQueueContract, WithChunkReading
{
use Queueable;
public string $queue = 'exports';
public function query(): Builder
{
return Product::with(['category', 'supplier'])->active();
}
public function map(Product $product): array
{
return [
$product->sku,
$product->name,
$product->category->name,
$product->supplier->name,
$product->price_formatted,
$product->stock_quantity,
];
}
public function headings(): array
{
return ['SKU', 'Name', 'Category', 'Supplier', 'Price', 'Stock'];
}
protected function columnCount(): int
{
return 6;
}
public function chunkSize(): int
{
return 1000; // process 1,000 rows at a time
}
}
Dispatch and email when complete:
public function exportLarge(Request $request): JsonResponse
{
Excel::queue(new LargeProductCatalogueExport, 'exports/catalogue.xlsx', 'private')
->chain([
new NotifyUserExportComplete($request->user(), 'exports/catalogue.xlsx'),
]);
return response()->json([
'success' => true,
'message' => 'Export queued. You will receive an email when it is ready.',
]);
}
Multi-Sheet Exports
For reports that need multiple tabs in one file:
use Maatwebsite\Excel\Concerns\WithMultipleSheets;
class SalesReportExport implements WithMultipleSheets
{
public function __construct(
private readonly int $year
) {}
public function sheets(): array
{
return [
'Summary' => new SalesSummarySheet($this->year),
'By Month' => new SalesByMonthSheet($this->year),
'By Region' => new SalesByRegionSheet($this->year),
];
}
}
Each sheet is its own class extending BaseExport.
Key Takeaways
BaseExportprovides shared styles, auto-sizing, and column widths. Subclasses provide only their data and headers.- Use
FromCollectionfor < 5,000 rows.FromQueryfor medium datasets.ShouldQueue+WithChunkReadingfor large ones. WithMappingmaps Eloquent models to arrays — keep the transformation logic in the map method, not the query.- Queue large exports and email a download link when complete. Never block an HTTP request for 50,000 rows.
WithMultipleSheetslets you split a report into tabs, each with its own class and data source.
Tips and Gotchas
⚠️ Warning: Never generate large exports synchronously in a controller. A 50,000 row export can take 30–120 seconds and will time out the HTTP request. Always dispatch to a queue, store the result in S3/disk, and notify the user with a download link.
💡 Tip: When building the
BaseExport, define column widths as constants (const COL_WIDTHS = [15, 30, 12, ...]) rather than magic numbers. This makes the layout easy to adjust and self-documenting.
🔥 Expert Note: Use
ShouldQueuewithWithChunkReadingfor memory efficiency on large datasets. Processing 50,000 rows in one chunk will exhaust PHP memory. Chunk size of 1,000 rows is a safe starting point for most datasets.
Further Reading
- Laravel Excel (Maatwebsite) — the standard Laravel package for Excel/CSV exports
- Laravel Excel Docs: Queued Exports
- PhpSpreadsheet — the underlying library; useful when you need low-level control
- Laravel Docs: File Storage — for storing generated export files