saroven / laravel-reportify
Unified PDF, Excel, CSV, TXT export engine for Laravel applications.
Requires
- php: ^8.1
- laravel/framework: ^9.0|^10.0|^11.0|^12.0|^13.0
- maatwebsite/excel: ^3.1
- mpdf/mpdf: ^8.0
Requires (Dev)
- orchestra/testbench: ^7.0|^8.0|^9.0
- pestphp/pest: ^1.22|^2.0|^3.0
- pestphp/pest-plugin-laravel: ^1.3|^2.0|^3.0
- phpunit/phpunit: ^9.5|^10.0
README
Reportify is a unified, high-performance report generation and document export engine for Laravel applications. Easily stream or export PDFs (via mPDF), Excel (.xlsx), CSV, TXT, and ZIP archives using clean Laravel syntax, event-driven background queues, and customizable Blade templates.
๐ฎ Demo Application
A complete working demo showing User Directory exports, PDF streaming, and a full Download Manager lifecycle implementation is available at:
๐ https://github.com/saroven/laravel-reportify-demo
# Clone and run the demo locally git clone https://github.com/saroven/laravel-reportify-demo.git cd laravel-reportify-demo composer install php artisan migrate:fresh --seed php artisan serve
๐ฆ Features
- ๐ Multi-Format Export Engine: Generate PDF, Excel (.xlsx), CSV, TXT, and ZIP packages.
- โก Synchronous PDF Streaming: Directly stream formatted PDF documents in the browser tab.
- ๐ Event-Driven Background Queues: Offload heavy exports to queue workers with native Laravel events (
ExportStarted,ExportCompleted,ExportFailed). - ๐งฉ PDF Chunking & Merging: Automatically chunk large datasets into smaller PDF files and merge them via
PDFMerger. - ๐จ Customizable Blade Templates: Configurable PDF headers, page footers, print dates, authenticated user stamps, and page numbers (
Page X of Y). - ๐ Artisan Generator Command:
php artisan reportify:make {name}generates cleanReportableexport classes. - ๐ฎ Exportable Controller Trait:
HasReportifytrait enables 1-line export handling in controllers ($this->exportReport()). - ๐ Blade UI Component: Drop-in export action buttons
<x-reportify-buttons />and helper scripts<x-reportify-scripts />.
โก Installation
Install the package via Composer:
composer require saroven/laravel-reportify
Publish the configuration file and Blade views (optional):
php artisan vendor:publish --tag=reportify-config php artisan vendor:publish --tag=reportify-views
๐ Quick Start
1. Make any Controller Exportable
Implement the Reportable interface and use the HasReportify trait on your controller:
namespace App\Http\Controllers; use Illuminate\Http\Request; use Saroven\Reportify\Contracts\Reportable; use Saroven\Reportify\Traits\HasReportify; use App\Exports\UserExport; use App\Models\User; class UserController extends Controller implements Reportable { use HasReportify; public function index(Request $request) { // Intercept export requests (e.g. ?export=pdfStream or ?export=excel) if ($request->has('export')) { $view = in_array($request->get('export'), ['pdfStream', 'pdf']) ? 'reports.users-pdf' : null; return $this->exportReport($request, 'User Directory Report', view: $view, dataProvider: UserExport::class); } $users = User::latest('id')->paginate(10); return view('users.index', compact('users')); } public function getExportData(array $payload, string $exportType, int|string|null $userId = null): mixed { return User::query()->get(); } }
2. Generate Dedicated Export Classes
Generate a dedicated Reportable export class using the Artisan generator command:
php artisan reportify:make UserExport
This creates app/Exports/UserExport.php:
namespace App\Exports; use App\Models\User; use Saroven\Reportify\Contracts\Reportable; class UserExport implements Reportable { public function getExportData(array $payload, string $exportType, int|string|null $userId = null): mixed { $query = User::query(); if (!empty($payload['search'])) { $search = $payload['search']; $query->where(function ($q) use ($search) { $q->where('name', 'like', "%{$search}%") ->orWhere('email', 'like', "%{$search}%") ->orWhere('phone', 'like', "%{$search}%"); }); } // Return clean mapped attributes for spreadsheet and plain text exports return $query->latest('id')->get()->map(function (User $user) { return [ 'ID' => $user->id, 'Name' => $user->name, 'Email' => $user->email, 'Role' => $user->role, 'Department' => $user->department ?? '-', 'Phone' => $user->phone ?? '-', 'Status' => $user->status, 'Created At' => $user->created_at ? $user->created_at->format('Y-m-d H:i:s') : '-', ]; }); } }
Dispatch background exports manually using ProcessReportJob:
use App\Exports\UserExport; use Saroven\Reportify\Jobs\ProcessReportJob; public function export(Request $request) { ProcessReportJob::dispatch( requestData: $request->all(), type: 'users-report', title: 'Users Export', user: auth()->id(), view: 'reports.users-pdf', additionalData: ['orientation' => 'L'], dataProvider: UserExport::class ); return back()->with('success', 'Export process started successfully.'); }
3. Synchronous PDF Streaming
Stream a generated PDF directly to the browser for inline preview or printing:
use Saroven\Reportify\Facades\Reportify; public function print(Request $request) { $users = User::where('status', 'active')->get(); return Reportify::streamPdf( request: $request->all(), response: $users, title: 'Active Users List', type: 'active-users', view: 'reports.users-pdf', additionalData: [ 'orientation' => 'P', 'paper_size' => 'A4', 'headerHtml' => '<h2>Active Users Report</h2>' ] ); }
4. PDF Chunking & Large Dataset Processing (pdfChunk)
When exporting large datasets (e.g. 5,000 to 50,000+ records), rendering everything in a single mPDF memory buffer can trigger memory limit crashes or mPDF backtrack errors. Reportify solves this with PDF Chunking & Merging:
use Saroven\Reportify\Facades\Reportify; // Export large dataset by automatically chunking & merging PDF parts $pdfPath = Reportify::exportPdfChunk( request: $request->all(), response: $largeUserCollection, context: 'exports/pdf', title: 'Large User Directory Export', view: 'reports.users-pdf', additionalData: ['orientation' => 'P'] );
In Controllers via HasReportify:
Simply pass ?export=pdfChunk in request parameters:
if ($request->has('export')) { $view = in_array($request->get('export'), ['pdfStream', 'pdf', 'pdfChunk']) ? 'reports.users-pdf' : null; return $this->exportReport($request, 'User Directory Report', view: $view, dataProvider: UserExport::class); }
How PDF Chunking Works:
- Splits records into batches based on
config('reportify.chunk_size', 2000). - Generates standalone PDF parts in temporary storage without memory overflow.
- Merges all chunked PDF parts into a single output PDF using mPDF's page template importer.
- Automatically cleans up temporary chunk files from storage.
4. Direct Multi-Format Exports
Export directly using the Reportify Facade or reportify() global helper:
use Saroven\Reportify\Facades\Reportify; // Export Excel (.xlsx) $excelPath = Reportify::exportExcel($request->all(), $data, 'exports/excel', 'Users List', 'reports.users-table'); // Export CSV (.csv) $csvPath = Reportify::exportCsv($request->all(), $data, 'exports/csv', 'Users List'); // Export Text File (.txt with custom delimiter) $txtPath = Reportify::exportTxt($request->all(), $data, 'exports/txt', 'Users List', null, ['separator' => '|']); // Export Multi-Part ZIP Package (.zip) $zipPath = Reportify::prepareZip('pdf', $request->all(), $largeData, 'exports/zips', 'User Statements', 'reports.statement-pdf');
5. Listen to Export Events (Building a Download Manager)
Reportify dispatches native Laravel events during the export processing lifecycle (ExportStarted, ExportCompleted, ExportFailed). You can listen to these events in AppServiceProvider.php to track job statuses and build a Download Manager:
namespace App\Providers; use App\Models\Download; use Illuminate\Support\ServiceProvider; use Illuminate\Support\Facades\Event; use Saroven\Reportify\Events\ExportStarted; use Saroven\Reportify\Events\ExportCompleted; use Saroven\Reportify\Events\ExportFailed; class AppServiceProvider extends ServiceProvider { public function boot(): void { // 1. Export Started -> Record initial 'processing' status Event::listen(function (ExportStarted $event) { Download::create([ 'user_id' => $event->userId ?: null, 'title' => $event->title, 'format' => strtoupper($event->exportFormat), 'status' => 'processing', ]); }); // 2. Export Completed -> Update status to 'completed' with file path Event::listen(function (ExportCompleted $event) { $download = Download::where('title', $event->title) ->where('format', strtoupper($event->exportFormat)) ->where('status', 'processing') ->latest('id') ->first(); if ($download) { $download->update([ 'file_path' => $event->filePath, 'status' => 'completed', ]); } else { Download::create([ 'user_id' => $event->userId ?: null, 'title' => $event->title, 'format' => strtoupper($event->exportFormat), 'file_path' => $event->filePath, 'status' => 'completed', ]); } }); // 3. Export Failed -> Update status to 'failed' with error details Event::listen(function (ExportFailed $event) { $download = Download::where('title', $event->title) ->where('format', strtoupper($event->exportFormat)) ->where('status', 'processing') ->latest('id') ->first(); if ($download) { $download->update([ 'status' => 'failed', 'error' => $event->errorMessage, ]); } }); } }
Then create a DownloadController to serve the generated files:
namespace App\Http\Controllers; use App\Models\Download; use Illuminate\Support\Facades\Storage; class DownloadController extends Controller { public function index() { $downloads = Download::latest('id')->paginate(10); return view('downloads.index', compact('downloads')); } public function download(Download $download) { $disk = config('reportify.storage_disk', 'public'); return Storage::disk($disk)->download($download->file_path); } }
6. Add Export Buttons & Scripts to Blade Layouts
Include drop-in action buttons in your views:
<!-- Render export dropdown buttons --> <x-reportify-buttons :pdfStream="['url' => '#', 'onClick' => 'exportLinkRedirectWithUrlParams(event, {type: `pdfStream`})']" :pdf="['url' => '#', 'onClick' => 'exportLinkRedirectWithUrlParams(event, {type: `pdf`})']" :excel="['url' => '#', 'onClick' => 'exportLinkRedirectWithUrlParams(event, {type: `excel`})']" :csv="['url' => '#', 'onClick' => 'exportLinkRedirectWithUrlParams(event, {type: `csv`})']" :txt="['url' => '#', 'onClick' => 'exportLinkRedirectWithUrlParams(event, {type: `txt`})']" />
Include <x-reportify-scripts /> in your master layout template (layouts/app.blade.php) for automatic query parameter preservation:
@yield('content')
<x-reportify-scripts />
</body>
</html>
โ๏ธ Configuration Reference (config/reportify.php)
return [ // Storage disk where export files are saved (default: 'public') 'storage_disk' => env('REPORTIFY_STORAGE_DISK', 'public'), // Set to true to force inline synchronous execution on servers without queue daemons 'force_sync' => (bool) env('REPORTIFY_FORCE_SYNC', false), // Base directory path for output files 'export_directory' => 'exports', // Batch chunk size for query processing and PDF merging 'chunk_size' => (int) env('REPORTIFY_CHUNK_SIZE', 2000), // mPDF engine configuration 'mpdf' => [ 'backtrack_limit' => '1000000000', 'recursion_limit' => '1000000000', 'default_paper_size' => 'A4', 'default_orientation' => 'P', 'author' => env('APP_NAME', 'Laravel'), ], // Default Blade templates 'views' => [ 'pdf_header' => 'reportify::pdf-header', 'pdf_footer' => 'reportify::pdf-footer', 'empty_pdf' => 'reportify::empty-pdf', ], ];
๐งช Testing
Run the test suite using Pest PHP:
vendor/bin/pest
๐ License
The MIT License (MIT). See LICENSE.md for details.