PDF fatura oluşturmak için DomPDF ve wkhtmltopdf ile Snappy kullandım. Her ikisi de çalışıyor, ancak her biri istediğim görünümü sağlamıyor. DomPDF’nin CSS desteği 2012 civarında takılı kaldı ve Snappy, fontları Windows XP’de gibi render ediyor.
Beni iki kütüphaneden de uzaklaştıran, bir müşterinin faturalarının marka kılavuzuna uymasını istemesiydi. Inter fontu, aksan rengi için belirli bir hex, toplam satırında yuvarlak köşeler, satır öğelerinin altında hafif bir gölge ve sol üstte küçük bir logo gerekiyordu. DomPDF, Inter fontunu Times New Roman olarak renderladı. Snappy fontu doğru render etti ama border-radius’u bozdu. İkisinin de CSS grid desteği yoktu, bu yüzden billing ve shipping adreslerinin yan yana olduğu iki sütunlu header için istemediğim bir float hack yazmam gerekiyordu.
Aslında istediğim, bir Laravel Blade şablonunu Chrome’un render ettiği gibi görüntülemekti ve daha sonra e-posta atabileceğim, bir gösterge panosuna gömebileceğim veya PDF’ye koyabileceğim bir PNG almak istiyordum. Bu makale, bunu kendi sunucumda başsız bir tarayıcı çalıştırmadan nasıl başardığımı anlatıyor.
Neden yaygın Laravel fatura kütüphaneleri yetersiz?
Neden yaygın Laravel fatura kütüphaneleri yetersiz?
PHP PDF kütüphanelerinin durumu, nazikçe ifade etmek gerekirse, tarihsel kalitede. DomPDF aktif olarak bakımda ancak render motoru CSS 2.1’in alt kümesi ve kısmi CSS 3 desteğiyle sınırlı; bu nedenle flexbox, grid, güvenilir webfontları ve göz ardı edilen veya render’ı çöken gölge/filtre özellikleri yok. “Burada bir sayı tablosu var” çıktısı için işe yarıyor. Tasarım ekibinizin görüşleri olduğunda işe yaramıyor.
Snappy (wkhtmltopdf aracılığıyla), bir süre daha sadık render yapıyordu, ancak wkhtmltopdf 2023’te arşivlendi. Hala çalışıyor ama güncellenmiyor. Ayrıca, zirve zamanlarında, font render’ı Chrome ile belirgin şekilde farklıydı; bu, çünkü tasarımcınız şablonu Chrome’da önizlemişti.
Modern hissiyat taşıyan bir seçenek, PHP’den exec çağrısıyla Puppeteer veya Playwright çalıştırmak ya da Browsershot gibi bir hizmet kullanmaktır. Bu işe yarıyor ama artık Laravel uygulamanızın yanı sıra bir Node.js yüklemesi, bir Chromium binary’si maintain etmeniz gerekiyor ve PHP’den buna exec çağrısı yapmak, debug etmekten pek hoşlanmadığım bir hata kategorisidir. Browsershot özellikle harika bir yazılım, ancak her ortamda CI ve üretim konteynerleriniz dahil, yüklemeniz, güncellemeniz ve çalışır durumda tutmanız gereken bir altyapıdır.
Beni bu sorundan kurtaran yaklaşım, faturayı kendi altyapımda render etmeye çalışmayı bırakmaktı. Blade şablonunun normalde yaptığı gibi HTML’yi render etmesine izin verin. O HTML’i bir API’ye gönderin. PNG’yi geri alın. Fatura tasarımı tamamen Blade ve CSS içinde yaşıyor; bu, başka bir Laravel görünümü gibi ve sunucumda hiçbir şey değişmek zorunda değil.
Kurulum
Kurulum
Ödeme olarak işaretlendiğinde bir PNG fatura oluşturan bir faturalama sistemi için çalışır bir uygulamayı adım adım açıklayacağım. Fatura, şirket detaylarıyla, bir satır öğesi tablosuyla, toplamlar bloğuyla ve ödeme koşullarıyla bir footer içerir. Her zaman bekleyebileceğiniz gibi.
Şu şablonla başlayın: resources/views/invoices/image.blade.php:
{{ $business['address'] }}
{{ $business['email'] }} - {{ $business['phone'] }}
Bill to
{{ $customer['name'] }}
{{ $customer['address'] }}
Description
Qty
Unit
Amount
@foreach ($items as $item)
{{ $item['description'] }}
{{ $item['qty'] }}
£{{ number_format($item['unit'], 2) }}
£{{ number_format($item['qty'] * $item['unit'], 2) }}
@endforeach
Subtotal£{{ number_format($totals['subtotal'], 2) }}
VAT ({{ $totals['vat_rate'] }}%)£{{ number_format($totals['vat'], 2) }}
Total£{{ number_format($totals['total'], 2) }}
Artık bir hizmet sınıfı, bir alan Faturasını alır, Blade şablonunu render eder, API’ye gönderir ve PNG baytlarını döndürür:
config('invoicing.business'),
'customer' => [
'name' => $invoice->customer->name,
'address' => $invoice->customer->formatted_address,
],
'invoice' => [
'number' => $invoice->number,
'issued_at' => $invoice->issued_at->format('j F Y'),
'due_at' => $invoice->due_at->format('j F Y'),
],
'items' => $invoice->items->map(fn ($i) => [
'description' => $i->description,
'qty' => $i->quantity,
'unit' => $i->unit_price,
])->all(),
'totals' => [
'subtotal' => $invoice->subtotal,
'vat_rate' => $invoice->vat_rate,
'vat' => $invoice->vat_amount,
'total' => $invoice->total,
],
])->render();
$response = Http::withToken($this->apiKey)
->timeout(30)
->post('https://api.html2img.com/v1/render', [
'html' => $html,
'viewport_width' => 800,
'viewport_height' => 1100,
'device_scale_factor' => 2,
'wait_for_selector' => '.totals-row.total',
'full_page' => true,
]);
if ($response->failed()) {
throw new RuntimeException(
"Invoice render failed: {$response->status()} {$response->body()}"
);
}
return $response->body();
}
}
Bunu bir hizmet sağlayıcısında bağlayın veya doğrudan çözümleyin:
$renderer = new InvoiceImageRenderer(config('services.html2img.key'));
$png = $renderer->render($invoice);
Storage::disk('s3')->put("invoices/{$invoice->number}.png", $png);
Render çağrısındaki birkaç detayı belirtmek gerekir. full_page: true bu yöntemin faturalar için çalışmasını sağlıyor, çünkü bir faturanın yüksekliği, kaç satır öğesi olduğuna bağlı ve klipslemek veya uzatmak istemezsiniz. Görünüm genişliği 800 ile sabit kalır (bir belge için okunabilir bir genişlik) ve yükseklik, içerik gerektirdikçe değişir. wait_for_selector: '.totals-row.total' render işleminin tüm içerik ağacının monte edilmesinden sonra gerçekleşmesini garanti eder, bu, Google Font henüz yüklenmediyse önemlidir. device_scale_factor: 2 1600 piksel genişliğinde bir PNG verir, bu da retina ekranlarda veya basıldığında keskin görünmesini sağlar.
Blade, {{ $var }} kullanıldığında HTML kaçışını sizin için yapar, bu nedenle kullanıcıdan sağlanan veriler (müşteri isimleri ve adresler gibi) yerleşimi bozamaz veya markup enjekte edemez. Zengin metin kaynağından satır açıklamalarını alıyorsanız, {!! $var !!} kullanın, bu da sadece sunucu tarafında temizlendiğinden emin olduktan sonra yapılmalıdır.
Bilinmesi Gereken Tuzaklar
Bilinmesi Gereken Tuzaklar
Fontlar, ilk çalıştırmada en büyük sürpriz kaynağıdır. @import, render içinde bir ağ gidiş dönüşü ekler. Binlerce fatura üretmek için bir üretim hattında, ya font dosyasını kendiniz barındırmalı ve @font-face ile kamuya açık bir URL aracılığıyla referans vermelisiniz ya da woff2’yi base64 ile encode edip inline kullanmalısınız. wait_for_selector seçeneği yardımcı olur, ancak eğer seçiciniz font yüklenene kadar görünüyorsa, hala bir an için geri dönüş alsınız. Üzerine küçük bir ms_delay eklemek (300-500ms) makul bir tam güvence sağlar.
Sayıların locale’den bağımsız formatlanması gerekir. number_format() varsayılan olarak ABD geleneklerine göre formatlar. Birleşik Krallık faturaları için number_format($amount, 2, '.', ',') kullanırım. Avrupa faturalarında, virgül ondalık ayırıcı olduğunda, argümanları değiştirin. Bunu üretimde yanlış yapmak utanç verici.
Faturaları, mümkünse bir web isteği içinde senkron olarak render etmeyin. Kuyruklayın. Laravel’in iş sistemini bu doğal olarak ele alır ve API, render işlemini asenkron yapmanız ve tamamlandığında bilgilendirilmeniz için webhook’ları destekler. Toplu işlemler için, webhook akışının substantially daha hızlı olduğu çünkü her render’da engelleme yapılmadığıdır.
Bir operasyonel not: fatura şablonunu versiyonlayın. Tasarımı değiştirdiğinizde, yeniden render edilen eski faturalar, orijinal versiyonlarından farklı görünecek ve bu muhasebe karışıklığına neden olabilir. Veya oluşturma zamanında render edilen PNG’yi S3’e snapshot olarak alıp servis edin ya da fatura üzerinde bir template_version sütunu dahil edin ve buna göre doğru Blade görünümünü seçin. Ben ilkini yapıyorum.
Laravel entegrasyonu hakkında daha derinlemesine bilgi için Laravel kullanım kılavuzuna başvurabilirsiniz; bu, HTTP istemci ayarı ve render ediciyi API’yi vurmadan test etme yöntemini kapsar. Daha uzun belgeler veya toplu çalışmalar için webhook parametre belgeleri, asenkron modelin nasıl olacağını açıklar. Faturalar ötesinde belge tarzı şablonları örnekleri görmek isterseniz, fatura ve makbuz örneği birkaç varyant içerir.
Görünümü render et, HTML’i gönder, PNG’yi kaydet. Blade şablonunuz, faturanın nasıl göründüğünün gerçek kaynağıdır; uygulamanızın geri kalanının gerçek kaynağı olduğu gibi.
Kaynak: Orijinal Makale


