Реализация генерации PDF
Генерация PDF применяется для счетов, договоров, отчётов, справок. Два подхода: HTML-to-PDF (рендеринг через headless браузер или библиотеку) и построение из примитивов (TCPDF, FPDF). HTML-to-PDF проще для сложных макетов.
Laravel: Browsershot (Puppeteer)
Browsershot использует headless Chrome для рендеринга HTML в PDF — поддерживает CSS Grid, Flexbox, переменные, шрифты.
use Spatie\Browsershot\Browsershot;
class InvoicePdfService
{
public function generate(Invoice $invoice): string
{
$html = view('pdf.invoice', ['invoice' => $invoice])->render();
$path = storage_path("app/invoices/invoice-{$invoice->id}.pdf");
Browsershot::html($html)
->format('A4')
->margins(15, 15, 15, 15) // мм
->showBackground()
->emulateMedia('print')
->waitUntilNetworkIdle() // дождаться загрузки шрифтов
->save($path);
return $path;
}
}
// Controller
public function download(Invoice $invoice): Response
{
$path = $this->invoicePdfService->generate($invoice);
return response()->download(
$path,
"invoice-{$invoice->number}.pdf",
['Content-Type' => 'application/pdf']
);
}
<!-- resources/views/pdf/invoice.blade.php -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap');
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Inter', sans-serif; font-size: 12px; color: #1a1a1a; }
.header { display: flex; justify-content: space-between; margin-bottom: 40px; }
.invoice-number { font-size: 24px; font-weight: 700; }
table { width: 100%; border-collapse: collapse; margin-top: 20px; }
th { background: #f3f4f6; padding: 8px; text-align: left; font-weight: 600; }
td { padding: 8px; border-bottom: 1px solid #e5e7eb; }
.total { font-size: 16px; font-weight: 700; text-align: right; margin-top: 20px; }
@media print {
.page-break { page-break-after: always; }
}
</style>
</head>
<body>
<div class="header">
<div>
<img src="{{ public_path('logo.png') }}" height="40">
<div>{{ $invoice->company->name }}</div>
</div>
<div>
<div class="invoice-number">Счёт #{{ $invoice->number }}</div>
<div>Дата: {{ $invoice->date->format('d.m.Y') }}</div>
</div>
</div>
<table>
<thead>
<tr><th>Описание</th><th>Кол-во</th><th>Цена</th><th>Сумма</th></tr>
</thead>
<tbody>
@foreach($invoice->items as $item)
<tr>
<td>{{ $item->description }}</td>
<td>{{ $item->quantity }}</td>
<td>{{ number_format($item->price, 2) }} ₽</td>
<td>{{ number_format($item->total, 2) }} ₽</td>
</tr>
@endforeach
</tbody>
</table>
<div class="total">Итого: {{ number_format($invoice->total, 2) }} ₽</div>
</body>
</html>
Node.js: Puppeteer
import puppeteer from 'puppeteer';
import Handlebars from 'handlebars';
async function generateInvoicePdf(invoice: Invoice): Promise<Buffer> {
const templateSource = await fs.readFile('./templates/invoice.html', 'utf-8');
const template = Handlebars.compile(templateSource);
const html = template(invoice);
const browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox'],
});
try {
const page = await browser.newPage();
await page.setContent(html, { waitUntil: 'networkidle0' });
return await page.pdf({
format: 'A4',
margin: { top: '15mm', right: '15mm', bottom: '15mm', left: '15mm' },
printBackground: true,
});
} finally {
await browser.close();
}
}
TCPDF: PHP-нативная генерация (без браузера)
Подходит для простых документов без сложного CSS:
use TCPDF;
class ContractPdfService
{
public function generate(Contract $contract): string
{
$pdf = new TCPDF('P', 'mm', 'A4', true, 'UTF-8');
$pdf->SetCreator('MyApp');
$pdf->SetAuthor($contract->company->name);
$pdf->SetTitle('Договор №' . $contract->number);
$pdf->SetFont('dejavusans', '', 10);
$pdf->AddPage();
$html = view('pdf.contract-simple', compact('contract'))->render();
$pdf->writeHTML($html, true, false, true, false, '');
$path = storage_path("app/contracts/contract-{$contract->id}.pdf");
$pdf->Output($path, 'F');
return $path;
}
}
Асинхронная генерация в очереди
class GenerateInvoicePdfJob implements ShouldQueue
{
public int $timeout = 120;
public function __construct(private Invoice $invoice) {}
public function handle(InvoicePdfService $service): void
{
$path = $service->generate($this->invoice);
// Загрузить в S3
$s3Key = "invoices/{$this->invoice->user_id}/{$this->invoice->id}.pdf";
Storage::disk('s3')->put($s3Key, file_get_contents($path));
$this->invoice->update(['pdf_key' => $s3Key, 'pdf_generated_at' => now()]);
unlink($path);
// Уведомить пользователя
$this->invoice->user->notify(new InvoiceReadyNotification($this->invoice));
}
}
Срок реализации
PDF-генерация через Browsershot/Puppeteer для Laravel или Node.js (счета, договоры): 2–3 дня. С асинхронной очередью и сохранением в S3: 3–4 дня.







