Laravel 13 PDF 生成完全指南

PDF 生成是许多企业应用的核心功能。本文将深入探讨 Laravel 13 中 PDF 生成的各种方法和最佳实践。

安装配置

安装 DomPDF

1
composer require barryvdh/laravel-dompdf

发布配置

1
php artisan vendor:publish --provider="Barryvdh\DomPDF\ServiceProvider"

配置选项

1
2
3
4
5
6
7
8
9
10
11
12
13
// config/dompdf.php
return [
'show_warnings' => false,
'public_path' => base_path('public'),
'convert_entities' => true,
'options' => [
'isRemoteEnabled' => true,
'isHtml5ParserEnabled' => true,
'isFontSubsettingEnabled' => true,
'defaultFont' => 'sans-serif',
'chroot' => [public_path(), storage_path()],
],
];

基础 PDF 生成

简单 PDF

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use Barryvdh\DomPDF\Facade\Pdf;

public function generatePdf()
{
$pdf = Pdf::loadView('pdf.invoice', ['data' => $data]);
return $pdf->download('invoice.pdf');
}

public function streamPdf()
{
$pdf = Pdf::loadView('pdf.invoice', ['data' => $data]);
return $pdf->stream('invoice.pdf');
}

public function savePdf()
{
$pdf = Pdf::loadView('pdf.invoice', ['data' => $data]);
$pdf->save(storage_path('app/invoices/invoice.pdf'));
}

设置纸张大小

1
2
3
4
5
$pdf = Pdf::loadView('pdf.document')
->setPaper('a4', 'landscape');

$pdf = Pdf::loadView('pdf.document')
->setPaper([0, 0, 612, 792]); // 自定义尺寸(点)

PDF 视图模板

基础模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
{{-- resources/views/pdf/invoice.blade.php --}}
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>发票 #{{ $invoice->id }}</title>
<style>
body {
font-family: sans-serif;
font-size: 12px;
}
.header {
text-align: center;
margin-bottom: 30px;
}
.company-info {
margin-bottom: 20px;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
th {
background-color: #f5f5f5;
}
.total {
text-align: right;
font-weight: bold;
font-size: 14px;
margin-top: 20px;
}
.footer {
position: fixed;
bottom: 0;
width: 100%;
text-align: center;
font-size: 10px;
color: #999;
}
@page {
margin: 20mm;
}
</style>
</head>
<body>
<div class="header">
<h1>发票</h1>
<p>发票编号: #{{ $invoice->id }}</p>
<p>日期: {{ $invoice->created_at->format('Y-m-d') }}</p>
</div>

<div class="company-info">
<strong>{{ config('app.name') }}</strong><br>
地址: {{ config('company.address') }}<br>
电话: {{ config('company.phone') }}
</div>

<div class="customer-info">
<strong>客户信息</strong><br>
姓名: {{ $invoice->customer->name }}<br>
邮箱: {{ $invoice->customer->email }}
</div>

<table>
<thead>
<tr>
<th>商品名称</th>
<th>数量</th>
<th>单价</th>
<th>小计</th>
</tr>
</thead>
<tbody>
@foreach($invoice->items as $item)
<tr>
<td>{{ $item->name }}</td>
<td>{{ $item->quantity }}</td>
<td>¥{{ number_format($item->price, 2) }}</td>
<td>¥{{ number_format($item->subtotal, 2) }}</td>
</tr>
@endforeach
</tbody>
</table>

<div class="total">
总计: ¥{{ number_format($invoice->total, 2) }}
</div>

<div class="footer">
感谢您的惠顾!
</div>
</body>
</html>

分页控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<style>
.page-break {
page-break-after: always;
}

.avoid-break {
page-break-inside: avoid;
}
</style>

<div class="avoid-break">
<h2>不会分页的内容</h2>
<p>这段内容不会被分页打断</p>
</div>

<div class="page-break"></div>

<h2>新页面内容</h2>

高级 PDF 功能

添加页眉页脚

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$pdf = Pdf::loadView('pdf.document')
->setOption('isHtml5ParserEnabled', true)
->setOption('isRemoteEnabled', true);

$pdf->getDomPDF()->setCallbacks([
'event' => 'end_page',
'f' => function ($info) use ($pdf) {
$pdf->getCanvas()->page_text(
$info['canvas_width'] - 50,
$info['canvas_height'] - 30,
"第 {PAGE_NUM} 页,共 {PAGE_COUNT} 页",
null,
10
);
},
]);

添加水印

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$pdf = Pdf::loadView('pdf.document');

$canvas = $pdf->getCanvas();
$canvas->page_text(
$canvas->get_width() / 2,
$canvas->get_height() / 2,
'机密文件',
null,
60,
[0.8, 0.8, 0.8],
0.1,
0,
-45
);

插入图片

1
2
3
<img src="{{ public_path('images/logo.png') }}" alt="Logo">

<img src="data:image/png;base64,{{ base64_encode(file_get_contents($imagePath)) }}" alt="Image">

PDF 服务类

完整 PDF 服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
<?php

namespace App\Services;

use Barryvdh\DomPDF\Facade\Pdf;
use Illuminate\Support\Facades\Storage;

class PdfService
{
protected string $defaultPaper = 'a4';
protected string $defaultOrientation = 'portrait';

public function generate(string $view, array $data = [], array $options = []): \Barryvdh\DomPDF\PDF
{
$pdf = Pdf::loadView($view, $data);

if (isset($options['paper'])) {
$pdf->setPaper($options['paper'], $options['orientation'] ?? $this->defaultOrientation);
}

if (isset($options['header']) || isset($options['footer'])) {
$this->addHeaderFooter($pdf, $options);
}

return $pdf;
}

public function download(string $view, array $data, string $filename, array $options = []): \Illuminate\Http\Response
{
return $this->generate($view, $data, $options)->download($filename);
}

public function stream(string $view, array $data, string $filename, array $options = []): \Illuminate\Http\Response
{
return $this->generate($view, $data, $options)->stream($filename);
}

public function save(string $view, array $data, string $path, array $options = []): bool
{
$pdf = $this->generate($view, $data, $options);

$directory = dirname($path);
if (!Storage::exists($directory)) {
Storage::makeDirectory($directory);
}

Storage::put($path, $pdf->output());

return true;
}

public function generateInvoice($invoice): string
{
$pdf = $this->generate('pdf.invoice', [
'invoice' => $invoice,
'company' => config('company'),
], [
'paper' => 'a4',
'orientation' => 'portrait',
]);

$path = "invoices/{$invoice->id}/invoice.pdf";
Storage::put($path, $pdf->output());

return $path;
}

public function generateReport($data): string
{
$pdf = $this->generate('pdf.report', [
'data' => $data,
'generatedAt' => now(),
], [
'paper' => 'a4',
'orientation' => 'landscape',
]);

$filename = 'report_' . now()->format('Y-m-d_His') . '.pdf';
$path = "reports/{$filename}";
Storage::put($path, $pdf->output());

return $path;
}

protected function addHeaderFooter($pdf, array $options): void
{
$canvas = $pdf->getCanvas();

$canvas->setCallbacks([
'event' => 'end_page',
'f' => function ($info) use ($canvas, $options) {
if (isset($options['footer'])) {
$canvas->page_text(
$info['canvas_width'] / 2,
$info['canvas_height'] - 30,
$options['footer'],
null,
10
);
}

$canvas->page_text(
$info['canvas_width'] - 50,
$info['canvas_height'] - 30,
"第 {PAGE_NUM} 页",
null,
10
);
},
]);
}
}

批量 PDF 生成

合并多个 PDF

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<?php

namespace App\Services;

use Barryvdh\DomPDF\Facade\Pdf;
use setasign\Fpdi\Fpdi;

class PdfMerger
{
public function merge(array $paths, string $outputPath): string
{
$pdf = new Fpdi();

foreach ($paths as $path) {
$pageCount = $pdf->setSourceFile($path);

for ($i = 1; $i <= $pageCount; $i++) {
$tplId = $pdf->importPage($i);
$pdf->addPage();
$pdf->useTemplate($tplId);
}
}

$pdf->Output($outputPath, 'F');

return $outputPath;
}

public function mergeInvoices(array $invoiceIds): string
{
$paths = [];

foreach ($invoiceIds as $id) {
$invoice = Invoice::find($id);
$paths[] = $this->generateInvoicePdf($invoice);
}

$outputPath = storage_path('app/invoices/merged_' . time() . '.pdf');
return $this->merge($paths, $outputPath);
}
}

PDF 队列处理

异步生成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<?php

namespace App\Jobs;

use App\Models\Report;
use App\Services\PdfService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Storage;

class GeneratePdfReport implements ShouldQueue
{
use Queueable;

public int $timeout = 300;

public function __construct(
protected Report $report
) {}

public function handle(PdfService $pdfService): void
{
$data = $this->prepareData();

$path = $pdfService->save(
'pdf.report',
['data' => $data],
"reports/{$this->report->id}/report.pdf"
);

$this->report->update([
'pdf_path' => $path,
'generated_at' => now(),
]);

$this->notifyUser();
}

protected function prepareData(): array
{
return [
'title' => $this->report->title,
'content' => $this->report->content,
'charts' => $this->generateCharts(),
'tables' => $this->generateTables(),
];
}

protected function notifyUser(): void
{
$this->report->user->notify(new ReportGenerated($this->report));
}
}

PDF 测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?php

namespace Tests\Feature;

use Tests\TestCase;
use App\Models\Invoice;
use Barryvdh\DomPDF\Facade\Pdf;

class PdfGenerationTest extends TestCase
{
public function test_invoice_pdf_can_be_generated()
{
$invoice = Invoice::factory()->create();

$response = $this->get(route('invoices.pdf', $invoice));

$response->assertStatus(200);
$response->assertHeader('content-type', 'application/pdf');
}

public function test_pdf_contains_correct_data()
{
$invoice = Invoice::factory()->create();

$pdf = Pdf::loadView('pdf.invoice', ['invoice' => $invoice]);
$content = $pdf->output();

$this->assertStringContainsString($invoice->customer->name, $content);
$this->assertStringContainsString($invoice->id, $content);
}
}

总结

Laravel 13 的 PDF 生成提供了:

  • 灵活的视图模板支持
  • 自定义纸张和方向
  • 页眉页脚添加
  • 水印功能
  • 批量合并处理
  • 异步队列生成
  • 完善的测试支持

掌握 PDF 生成技巧可以满足各种文档输出需求。