跳轉到

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 相容性說明


步驟一:安裝套件

composer require nextpdf/symfony

透過 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):

SPECTRUM_ENABLED=false
SPECTRUM_SOCKET=tcp://localhost:9000
APP_NAME="My App"

步驟三:使用 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'

下一步