Symfony 快速開始¶
本指南說明如何在 Symfony 7.2 專案中整合 nextpdf/symfony,包含 Bundle 配置、 依賴注入,以及透過 Symfony Messenger 實現的非同步 PDF 生成。
前置條件: - Symfony 7.2.x 專案(需要
symfony/flex) - PHP 8.5+ - 已完成 安裝指南 中的基本設定
Backport 相容性說明¶
PHP 8.5 語法需求:
nextpdf/symfony使用 PHP 8.5 語法特性。若你的 Symfony 應用程式執行於 PHP 8.1,請改用
nextpdf/backport套件, 並參閱 PHP 相容性說明。
步驟一:安裝套件¶
透過 Symfony Flex 的 Recipe 機制,安裝後會自動: - 在 config/bundles.php 中註冊 NextPdfBundle - 建立 config/packages/next_pdf.yaml 預設設定檔
步驟二:Bundle 設定¶
config/packages/next_pdf.yaml(Flex 自動生成,可依需求調整):
next_pdf:
fonts:
path: '%kernel.project_dir%/var/nextpdf/fonts'
cache: '%kernel.project_dir%/var/cache/nextpdf/fonts'
spectrum:
enabled: '%env(bool:SPECTRUM_ENABLED)%'
socket: '%env(SPECTRUM_SOCKET)%'
metadata:
creator: '%env(APP_NAME)%'
storage:
# Flysystem adapter service ID
adapter: 'oneup_flysystem.local_filesystem_adapter'
對應的環境變數(.env):
步驟三:使用 PdfFactory(依賴注入)¶
nextpdf/symfony 在 DI Container 中自動登記 PdfFactory 服務,可直接透過建構子注入:
<?php
declare(strict_types=1);
namespace App\Controller;
use NextPDF\Symfony\Contracts\PdfFactory;
use NextPDF\Symfony\Http\PdfResponse;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
final class InvoiceController extends AbstractController
{
public function __construct(
private readonly PdfFactory $pdfFactory,
) {}
#[Route('/invoices/{id}/pdf', name: 'invoice_pdf', methods: ['GET'])]
public function download(int $id): Response
{
$invoice = $this->getUser(); // 依實際邏輯取得資料
$document = $this->pdfFactory->create();
$document->addPage()
->setFont(family: 'NotoSans', size: 12)
->text("發票編號:INV-{$id}", x: 20, y: 30)
->text('金額:NT$ 1,000', x: 20, y: 45);
// PdfResponse 自動設定 Content-Type: application/pdf
return new PdfResponse(
document: $document,
filename: "invoice-{$id}.pdf",
disposition: 'inline', // 或 'attachment'
);
}
}
步驟四:Twig 模板整合¶
透過 nextpdf/artisan 的 Chrome CDP 渲染,可將 Twig 模板轉換為 PDF:
<?php
declare(strict_types=1);
namespace App\Controller;
use NextPDF\Symfony\Contracts\PdfFactory;
use NextPDF\Symfony\Http\PdfResponse;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
final class ContractController extends AbstractController
{
public function __construct(
private readonly PdfFactory $pdfFactory,
) {}
#[Route('/contracts/{id}/preview', name: 'contract_preview')]
public function preview(int $id): Response
{
$html = $this->renderView('pdfs/contract.html.twig', [
'contract' => ['id' => $id, 'client' => 'Acme Corp.'],
'date' => new \DateTimeImmutable(),
]);
// 從 HTML 字串建立 PDF(需 nextpdf/artisan)
$document = $this->pdfFactory->createFromHtml($html);
return new PdfResponse(
document: $document,
filename: "contract-{$id}.pdf",
);
}
}
對應的 Twig 模板 templates/pdfs/contract.html.twig:
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<style>
body { font-family: 'Noto Sans TC', sans-serif; margin: 40px; }
h1 { color: #1E3A8A; }
.meta { color: #6B7280; font-size: 12px; }
</style>
</head>
<body>
<h1>合約編號:{{ contract.id }}</h1>
<p class="meta">日期:{{ date|date('Y-m-d') }}</p>
<p>乙方:{{ contract.client }}</p>
{# PLACEHOLDER:CONTENT:contract-body — Full contract body content #}
</body>
</html>
步驟五:Messenger 非同步生成¶
對於需要時間的大型 PDF,使用 Symfony Messenger 進行非同步處理:
Message 類別¶
<?php
declare(strict_types=1);
namespace App\Message;
final readonly class GenerateReportMessage
{
public function __construct(
public readonly int $userId,
public readonly string $reportMonth, // 格式:'YYYY-MM'
public readonly string $outputDisk = 'default',
) {}
}
MessageHandler¶
<?php
declare(strict_types=1);
namespace App\MessageHandler;
use App\Message\GenerateReportMessage;
use NextPDF\Symfony\Contracts\PdfFactory;
use Psr\Log\LoggerInterface;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
#[AsMessageHandler]
final class GenerateReportHandler
{
public function __construct(
private readonly PdfFactory $pdfFactory,
private readonly LoggerInterface $logger,
) {}
public function __invoke(GenerateReportMessage $message): void
{
$this->logger->info('開始生成報表 PDF', [
'user_id' => $message->userId,
'report_month' => $message->reportMonth,
]);
$document = $this->pdfFactory->create();
$document->addPage()
->setFont(family: 'NotoSans', size: 20)
->text("月度報表 {$message->reportMonth}", x: 20, y: 30);
// <!-- PLACEHOLDER:CONTENT:report-pages — Report content building logic -->
$outputPath = sprintf(
'reports/user-%d/%s.pdf',
$message->userId,
$message->reportMonth,
);
$document->saveToStorage(
disk: $message->outputDisk,
path: $outputPath,
);
$this->logger->info('報表 PDF 生成完成', ['path' => $outputPath]);
}
}
Messenger 路由設定¶
config/packages/messenger.yaml:
framework:
messenger:
transports:
async:
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
options:
use_notify: true
check_delayed_interval: 1000
retry_strategy:
max_retries: 3
delay: 1000
multiplier: 2
max_delay: 0
routing:
'App\Message\GenerateReportMessage': async
分派 Message¶
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Message\GenerateReportMessage;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Routing\Attribute\Route;
final class ReportController extends AbstractController
{
public function __construct(
private readonly MessageBusInterface $bus,
) {}
#[Route('/reports/generate', name: 'report_generate', methods: ['POST'])]
public function generate(): JsonResponse
{
$this->bus->dispatch(new GenerateReportMessage(
userId: $this->getUser()->getId(),
reportMonth: date('Y-m'),
));
return $this->json(['status' => 'queued']);
}
}
測試¶
Symfony 的 DI Container 讓單元測試和功能測試都非常直觀:
<?php
declare(strict_types=1);
namespace App\Tests\Controller;
use NextPDF\Symfony\Testing\PdfFactoryFake;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
final class InvoiceControllerTest extends WebTestCase
{
public function test_invoice_pdf_returns_correct_headers(): void
{
$client = static::createClient();
// 用 Fake 替換真實 PdfFactory(避免實際 PDF 生成)
static::getContainer()->set(
PdfFactory::class,
new PdfFactoryFake(),
);
$client->request('GET', '/invoices/1/pdf');
self::assertResponseIsSuccessful();
self::assertResponseHeaderSame('Content-Type', 'application/pdf');
}
}
Service 定義(進階)¶
若需要手動定義服務(不使用 Flex),可在 config/services.yaml 中加入:
services:
NextPDF\Symfony\Contracts\PdfFactory:
class: NextPDF\Symfony\PdfFactory
arguments:
$config: '@next_pdf.process_config'
$logger: '@logger'
# 自動注入別名
NextPDF\Symfony\PdfFactory: '@NextPDF\Symfony\Contracts\PdfFactory'
下一步¶
- 安裝指南 — 商業套件(Pro / Enterprise)整合
- 快速開始(Core) — 純 PHP 範例
- 架構說明 — PdfFactory vs createStandalone()
- Core API 參考