Building Queue-Driven PDF Generation with Laravel Jobs and React Progress Tracking
Introduction
PDF generation can be a resource-intensive process that blocks your application's main thread, leading to poor user experience. Whether you're generating invoices, reports, or certificates, processing these requests asynchronously with real-time progress updates provides a much smoother experience for users.
In this tutorial, we'll build a complete system that queues PDF generation tasks in Laravel and tracks their progress in a React frontend. Users will see real-time updates as their documents are being processed, and they'll be notified when the PDF is ready for download.
Setting Up the Laravel Backend
First, let's create the necessary database migrations and models. We'll need a table to track PDF generation jobs and their status.
php artisan make:migration create_pdf_generations_tableUpdate the migration file:
id();
$table->string('user_id');
$table->string('type'); // invoice, report, certificate, etc.
$table->string('status')->default('pending'); // pending, processing, completed, failed
$table->integer('progress')->default(0);
$table->json('data'); // Store the data needed for PDF generation
$table->string('file_path')->nullable();
$table->string('error_message')->nullable();
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('pdf_generations');
}
};Create the model:
php artisan make:model PdfGeneration 'array'
];
public function updateProgress($progress, $status = null)
{
$this->update([
'progress' => $progress,
'status' => $status ?: $this->status
]);
// Broadcast progress update
broadcast(new \App\Events\PdfProgressUpdated($this));
}
}Creating the PDF Generation Job
Now let's create a queued job that will handle the actual PDF generation:
php artisan make:job GeneratePdfJobpdfGeneration = $pdfGeneration;
}
public function handle()
{
try {
// Update status to processing
$this->pdfGeneration->updateProgress(10, 'processing');
// Simulate data preparation
$data = $this->prepareData();
$this->pdfGeneration->updateProgress(30);
// Generate PDF based on type
$pdf = $this->generatePdf($data);
$this->pdfGeneration->updateProgress(70);
// Save PDF to storage
$filename = $this->savePdf($pdf);
$this->pdfGeneration->updateProgress(90);
// Update final status
$this->pdfGeneration->update([
'status' => 'completed',
'progress' => 100,
'file_path' => $filename
]);
broadcast(new \App\Events\PdfProgressUpdated($this->pdfGeneration));
} catch (Exception $e) {
$this->pdfGeneration->update([
'status' => 'failed',
'error_message' => $e->getMessage()
]);
broadcast(new \App\Events\PdfProgressUpdated($this->pdfGeneration));
}
}
private function prepareData()
{
// Simulate data processing time
sleep(2);
return array_merge($this->pdfGeneration->data, [
'generated_at' => now()->format('Y-m-d H:i:s')
]);
}
private function generatePdf($data)
{
// Simulate PDF generation time
sleep(3);
$view = match($this->pdfGeneration->type) {
'invoice' => 'pdfs.invoice',
'report' => 'pdfs.report',
default => 'pdfs.default'
};
return Pdf::loadView($view, compact('data'));
}
private function savePdf($pdf)
{
$filename = 'pdfs/' . uniqid() . '_' . $this->pdfGeneration->type . '.pdf';
Storage::put($filename, $pdf->output());
return $filename;
}
}Broadcasting Progress Updates
Create an event to broadcast progress updates to the frontend:
php artisan make:event PdfProgressUpdatedpdfGeneration = $pdfGeneration;
}
public function broadcastOn()
{
return new PrivateChannel('pdf-generation.' . $this->pdfGeneration->user_id);
}
public function broadcastWith()
{
return [
'id' => $this->pdfGeneration->id,
'status' => $this->pdfGeneration->status,
'progress' => $this->pdfGeneration->progress,
'type' => $this->pdfGeneration->type,
'file_path' => $this->pdfGeneration->file_path,
'error_message' => $this->pdfGeneration->error_message
];
}
}API Controller
Create a controller to handle PDF generation requests and status checks:
validate([
'type' => 'required|string|in:invoice,report,certificate',
'data' => 'required|array'
]);
$pdfGeneration = PdfGeneration::create([
'user_id' => Auth::id(),
'type' => $request->type,
'data' => $request->data
]);
// Dispatch the job
GeneratePdfJob::dispatch($pdfGeneration);
return response()->json([
'message' => 'PDF generation started',
'job_id' => $pdfGeneration->id
]);
}
public function status($id)
{
$pdfGeneration = PdfGeneration::where('user_id', Auth::id())
->findOrFail($id);
return response()->json($pdfGeneration);
}
public function download($id)
{
$pdfGeneration = PdfGeneration::where('user_id', Auth::id())
->where('status', 'completed')
->findOrFail($id);
if (!$pdfGeneration->file_path || !Storage::exists($pdfGeneration->file_path)) {
return response()->json(['error' => 'File not found'], 404);
}
return Storage::download($pdfGeneration->file_path);
}
}React Frontend Implementation
Now let's build the React component that will handle PDF generation requests and display progress:
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import Echo from 'laravel-echo';
const PdfGenerator = ({ userId }) => {
const [jobs, setJobs] = useState([]);
const [isGenerating, setIsGenerating] = useState(false);
useEffect(() => {
// Listen for progress updates
const echo = new Echo({
broadcaster: 'pusher',
// Add your Echo configuration here
});
echo.private(`pdf-generation.${userId}`)
.listen('PdfProgressUpdated', (e) => {
updateJobProgress(e.id, e);
});
return () => {
echo.disconnect();
};
}, [userId]);
const updateJobProgress = (jobId, data) => {
setJobs(prevJobs =>
prevJobs.map(job =>
job.id === jobId ? { ...job, ...data } : job
)
);
};
const generatePdf = async (type, data) => {
setIsGenerating(true);
try {
const response = await axios.post('/api/pdf/generate', {
type,
data
});
// Add new job to the list
const newJob = {
id: response.data.job_id,
type,
status: 'pending',
progress: 0
};
setJobs(prev => [...prev, newJob]);
} catch (error) {
console.error('Error generating PDF:', error);
} finally {
setIsGenerating(false);
}
};
const downloadPdf = async (jobId) => {
try {
const response = await axios.get(`/api/pdf/download/${jobId}`, {
responseType: 'blob'
});
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', `document_${jobId}.pdf`);
document.body.appendChild(link);
link.click();
link.remove();
window.URL.revokeObjectURL(url);
} catch (error) {
console.error('Error downloading PDF:', error);
}
};
return (
PDF Generation Jobs
{jobs.map(job => (
))}
);
};
const JobProgress = ({ job, onDownload }) => {
const getStatusColor = (status) => {
switch (status) {
case 'completed': return 'green';
case 'failed': return 'red';
case 'processing': return 'blue';
default: return 'gray';
}
};
return (
{job.type}
{job.status}
{job.progress}%
{job.status === 'completed' && (
SSyed Suhaib ZiaFull Stack Developer