In this guide, we’ll explore how to implement a robust PDF generation and download system using Laravel for the backend and Vue.js for the frontend. We’ll specifically focus on generating purchase order PDFs using the wkhtmltopdf library through Laravel Snappy.
Prerequisites
- Laravel 8+
- Vue.js 2/3
- wkhtmltopdf installed on your server
- Laravel Snappy package
Setting Up Laravel Snappy
First, install the Laravel Snappy package
composer require barryvdh/laravel-snappy
I’m assuming that you are using a Linux OS, In ubuntu I used which wkhtmltopdf
to see the binary path.
Configure your config/snappy.php
return [
'pdf' => [
'enabled' => true,
'binary' => '/usr/bin/wkhtmltopdf',
'timeout' => 120,
'options' => [
'enable-local-file-access' => true,
'encoding' => 'UTF-8',
'page-size' => 'A4'
],
]
];
Creating the PDF Template
Create a blade template for your PDF (resources/views/pdf_templates/purchase_order.blade.php
)
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Purchase Order</title>
<style>
/* Inline styles for better PDF rendering */
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
}
/* ... more styles ... */
</style>
</head>
<body>
<!-- Your PDF content structure -->
<div class="header">
<h1>Purchase Order #{{ $purchaseOrder->po_number }}</h1>
</div>
<!-- ... more content ... -->
</body>
</html>
Backend Implementation
Create your controller methods:
class PurchaseOrderController extends Controller
{
public function downloadInvoiceSnappyPdf(PurchaseOrder $purchaseOrder)
{
try {
$pdf = SnappyPdf::loadView('pdf_templates.purchase_order', [
'purchaseOrder' => $purchaseOrder
])
->setOption('encoding', 'UTF-8')
->setOption('margin-top', 10);
return $pdf->download($purchaseOrder->po_number . '.pdf');
} catch (\Exception $e) {
return response()->json(['error' => 'Failed to generate PDF'], 500);
}
}
}
Frontend Implementation
Vue Service ( In short you need to make an Api Call, In my case I’m using service files for example: PurchaseOrderService.js, InvoiceService.js)
Create a service to handle API calls:
// services/PurchaseOrderService.js
export const exportPurchaseOrderInvoice = async (id) => {
const response = await axios.get(`/api/purchase-orders/${id}/invoice/pdf`, {
responseType: 'blob'
});
return response.data;
};
Vue Component
Implement the download functionality in your component: (You can point this to click event)
// components/PurchaseOrder.vue
export default {
methods: {
async downloadPdf() {
try {
const response = await exportPurchaseOrderInvoice(this.getSelectedItem.id);
const blob = new Blob([response], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `${this.getSelectedItem.po_number}.pdf`;
link.click();
URL.revokeObjectURL(url);
} catch {
this.$notification.$emit('error', 'Failed to download PDF');
}
}
}
}
Optimizing the Implementation
For better code organization and reusability, we can create a composable:
Introduction To Composition Api For Newbie
// composables/useDownloader.js
import { ref } from 'vue';
export function useDownloader() {
const isDownloading = ref(false);
const downloadFile = async (getData, filename) => {
isDownloading.value = true;
try {
const response = await getData();
const url = URL.createObjectURL(new Blob([response]));
const link = document.createElement('a');
link.href = url;
link.download = filename;
link.click();
URL.revokeObjectURL(url);
} finally {
isDownloading.value = false;
}
};
return { downloadFile, isDownloading };
}
It’s more likely creating a hook or a helper function in Laravel.
// TopButtons.vue
import { useDownloader } from '@/composables/useDownloader';
import { exportPurchaseOrderInvoice } from '@/services/PurchaseOrderService';
export default {
name: 'TopButtons',
props: {
showBuilder: Boolean,
loading: Boolean,
instance: Object
},
setup(props) {
const { downloadFile, isDownloading } = useDownloader();
// Method to handle PDF download
const downloadPdf = async () => {
try {
await downloadFile(
// Pass the function that gets the data
() => exportPurchaseOrderInvoice(props.instance.getSelectedItem.id),
// Set the filename using PO number
`${props.instance.getSelectedItem.po_number}.pdf`
);
} catch (error) {
// Handle any errors
console.error('PDF download failed:', error);
}
};
return {
downloadPdf,
isDownloading
};
},
// In template
template: `
<div>
<v-btn
color="#F3F4FA"
:loading="isDownloading"
@click="downloadPdf"
>
<v-icon size="22">
{{ mdiPdfBox }}
</v-icon>
</v-btn>
</div>
`
});
Common Issues and Solutions
1. wkhtmltopdf Installation Issues
If you encounter issues with wkhtmltopdf, ensure proper installation:
2. PDF Rendering Issues
- Ensure all assets are accessible
- Use absolute paths for images
- Include all styles inline
- Avoid external dependencies
3. File Download Issues
- Set proper response headers
- Handle binary data correctly
- Clean up object URLs to prevent memory leaks
Best Practices
- Error Handling: Implement comprehensive error handling both on frontend and backend
- Loading States: Show loading indicators during PDF generation
- Clean Code: Use composables or utilities for reusable functionality
- Performance: Clean up resources after download
- Security: Validate user permissions before generating PDFs
Additional Resources
Happy coding! 🚀