Laravel'de Fatura PDF Oluşturma
E-ticaret, SaaS, freelance faturalandırma — PDF oluşturma ihtiyacı her yerde karşımıza çıkıyor. Laravel'de barryvdh/laravel-dompdf paketi ile Blade template'inden PDF oluşturmak birkaç dakikalık iş.
Kurulum
composer require barryvdh/laravel-dompdf
Config dosyasını publish etmek isterseniz:
php artisan vendor:publish --provider="Barryvdh\DomPdf\ServiceProvider"
config/dompdf.php dosyasında kağıt boyutu, font dizini gibi ayarları değiştirebilirsiniz. Çoğu projede default ayarlar yeterli.
Fatura Modeli
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Invoice extends Model
{
protected $fillable = [
'invoice_number', 'customer_name', 'customer_email',
'customer_address', 'subtotal', 'tax_rate', 'tax_amount',
'total', 'status', 'due_date', 'notes',
];
protected function casts(): array
{
return [
'subtotal' => 'decimal:2',
'tax_amount' => 'decimal:2',
'total' => 'decimal:2',
'tax_rate' => 'integer',
'due_date' => 'date',
];
}
public function items()
{
return $this->hasMany(InvoiceItem::class);
}
// Otomatik fatura numarası
public static function generateNumber(): string
{
$year = date('Y');
$last = static::whereYear('created_at', $year)->max('id') ?? 0;
return sprintf('INV-%s-%04d', $year, $last + 1);
}
}
Blade Template
PDF'in görünümünü Blade ile tasarlıyoruz. HTML + inline CSS — dompdf harici CSS dosyalarını destekler ama inline CSS ile sorun yaşamazsınız.
resources/views/invoices/pdf.blade.php:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: DejaVu Sans, sans-serif;
font-size: 12px;
color: #333;
line-height: 1.6;
}
.header {
display: flex;
justify-content: space-between;
margin-bottom: 30px;
border-bottom: 2px solid #3b82f6;
padding-bottom: 20px;
}
.company-name {
font-size: 24px;
font-weight: bold;
color: #1e40af;
}
.invoice-info {
text-align: right;
}
.invoice-title {
font-size: 20px;
color: #1e40af;
}
table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
}
th {
background-color: #3b82f6;
color: white;
padding: 10px;
text-align: left;
}
td {
padding: 8px 10px;
border-bottom: 1px solid #e5e7eb;
}
tr:nth-child(even) {
background-color: #f9fafb;
}
.totals {
float: right;
width: 250px;
}
.totals table td {
border: none;
}
.total-row {
font-size: 16px;
font-weight: bold;
color: #1e40af;
}
.footer {
position: fixed;
bottom: 0;
width: 100%;
text-align: center;
font-size: 10px;
color: #9ca3af;
border-top: 1px solid #e5e7eb;
padding-top: 10px;
}
</style>
</head>
<body>
<table width="100%">
<tr>
<td>
<div class="company-name">ProjeMan</div>
<div>[email protected]</div>
<div>projeman.net</div>
</td>
<td style="text-align: right;">
<div class="invoice-title">FATURA</div>
<div><strong>No:</strong> {{ $invoice->invoice_number }}</div>
<div><strong>Tarih:</strong> {{ $invoice->created_at->format('d.m.Y') }}</div>
<div><strong>Vade:</strong> {{ $invoice->due_date->format('d.m.Y') }}</div>
</td>
</tr>
</table>
<div style="margin: 20px 0; padding: 15px; background-color: #f3f4f6; border-radius: 5px;">
<strong>Müşteri:</strong><br>
{{ $invoice->customer_name }}<br>
{{ $invoice->customer_email }}<br>
{!! nl2br(e($invoice->customer_address)) !!}
</div>
<table>
<thead>
<tr>
<th>#</th>
<th>Açıklama</th>
<th style="text-align: center;">Miktar</th>
<th style="text-align: right;">Birim Fiyat</th>
<th style="text-align: right;">Toplam</th>
</tr>
</thead>
<tbody>
@foreach ($invoice->items as $index => $item)
<tr>
<td>{{ $index + 1 }}</td>
<td>{{ $item->description }}</td>
<td style="text-align: center;">{{ $item->quantity }}</td>
<td style="text-align: right;">{{ number_format($item->unit_price, 2, ',', '.') }} ₺</td>
<td style="text-align: right;">{{ number_format($item->total, 2, ',', '.') }} ₺</td>
</tr>
@endforeach
</tbody>
</table>
<div class="totals">
<table>
<tr>
<td>Ara Toplam:</td>
<td style="text-align: right;">{{ number_format($invoice->subtotal, 2, ',', '.') }} ₺</td>
</tr>
<tr>
<td>KDV (%{{ $invoice->tax_rate }}):</td>
<td style="text-align: right;">{{ number_format($invoice->tax_amount, 2, ',', '.') }} ₺</td>
</tr>
<tr class="total-row">
<td>Genel Toplam:</td>
<td style="text-align: right;">{{ number_format($invoice->total, 2, ',', '.') }} ₺</td>
</tr>
</table>
</div>
<div style="clear: both;"></div>
@if ($invoice->notes)
<div style="margin-top: 30px; padding: 10px; background-color: #fef3c7; border-radius: 5px;">
<strong>Notlar:</strong><br>
{{ $invoice->notes }}
</div>
@endif
<div class="footer">
Bu fatura ProjeMan tarafından oluşturulmuştur. | projeman.net
</div>
</body>
</html>
Birkaç dikkat noktası:
- Font olarak
DejaVu Sanskullanıyoruz — Türkçe karakterleri (ş, ğ, ı, ö, ü, ç) doğru gösterir. Arial veya Helvetica Türkçe karakterlerde sorun çıkarır. flexboxdompdf'te sınırlı destek görür, tablo layout daha güvenli.position: fixedfooter'ı her sayfanın altına sabitler.
Controller
namespace App\Http\Controllers;
use App\Models\Invoice;
use Barryvdh\DomPdf\Facade\Pdf;
class InvoiceController extends Controller
{
public function download(Invoice $invoice)
{
$pdf = Pdf::loadView('invoices.pdf', compact('invoice'))
->setPaper('a4')
->setOption('isRemoteEnabled', true);
return $pdf->download("fatura-{$invoice->invoice_number}.pdf");
}
public function preview(Invoice $invoice)
{
$pdf = Pdf::loadView('invoices.pdf', compact('invoice'))
->setPaper('a4');
return $pdf->stream("fatura-{$invoice->invoice_number}.pdf");
}
public function sendByEmail(Invoice $invoice)
{
$pdf = Pdf::loadView('invoices.pdf', compact('invoice'))
->setPaper('a4')
->output();
Mail::to($invoice->customer_email)->send(
new InvoiceMail($invoice, $pdf)
);
return back()->with('success', 'Fatura e-posta ile gönderildi.');
}
}
Üç farklı kullanım:
download()— Dosya olarak indirirstream()— Browser'da önizleme açar (indirmeden)output()— Raw PDF çıktısını alır, mail'e eklemek için ideal
Mail ile PDF Gönderme
namespace App\Mail;
use App\Models\Invoice;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Attachment;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
class InvoiceMail extends Mailable
{
public function __construct(
public Invoice $invoice,
private string $pdfContent
) {}
public function envelope(): Envelope
{
return new Envelope(
subject: "Fatura #{$this->invoice->invoice_number}",
);
}
public function content(): Content
{
return new Content(
view: 'emails.invoice',
);
}
public function attachments(): array
{
return [
Attachment::fromData(fn () => $this->pdfContent, "fatura-{$this->invoice->invoice_number}.pdf")
->withMime('application/pdf'),
];
}
}
Route'lar
Route::get('invoices/{invoice}/download', [InvoiceController::class, 'download'])
->name('invoices.download');
Route::get('invoices/{invoice}/preview', [InvoiceController::class, 'preview'])
->name('invoices.preview');
Route::post('invoices/{invoice}/send', [InvoiceController::class, 'sendByEmail'])
->name('invoices.send');
Performans ve İpuçları
- Ağır PDF'ler için Queue kullanın — 10+ sayfalık raporlar oluşturmak birkaç saniye sürebilir. Job'a atın, hazır olunca bildirim gönderin.
- Cache'leyin — Aynı fatura PDF'i tekrar tekrar oluşturmayın.
storage/app/invoices/dizinine kaydedin, varsa direkt serve edin. - Logo eklemek —
isRemoteEnabledtrue olmalı veya logo dosyasını base64 olarak embed edin:
$logo = base64_encode(file_get_contents(public_path('images/logo.png')));
// Template'de: <img src="data:image/png;base64,{{ $logo }}">
- Sayfa sonu kontrolü — Uzun tablolarda CSS
page-break-inside: avoidkullanın
PDF oluşturmak her projenin bir noktasında ihtiyaç duyduğu bir özellik. Fatura, rapor, sertifika, teklif mektubu — hepsi aynı pattern: Blade template + dompdf. Template'i bir kez güzel tasarlayın, geri kalan sadece veriyi doldurmak.